import { existsSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { PDF_PINS } from "../pdf/runtime.ts"; import { DEPENDENCY_PLAN, PYTHON_MODULES } from "./constants.ts"; import type { CapabilityCheck, OfficeStatusReport, ProbeExecutor } from "./types.ts"; const PYTHON_PROBE = String.raw` import importlib.metadata import importlib.util import json import sys modules = { "pypdf": "pypdf", "PIL": "Pillow", "docx": "python-docx", "win32com.client": "pywin32", "pdfplumber": "pdfplumber", "reportlab": "reportlab", "pypdfium2": "pypdfium2", "cryptography": "cryptography", } result = {"version": sys.version.split()[0], "executable": sys.executable, "modules": {}} for import_name, distribution in modules.items(): try: found = importlib.util.find_spec(import_name) is not None except (ImportError, ModuleNotFoundError, ValueError): found = False version = None if found: try: version = importlib.metadata.version(distribution) except importlib.metadata.PackageNotFoundError: pass result["modules"][import_name] = {"available": found, "version": version} print(json.dumps(result, ensure_ascii=False)) `.trim(); interface PythonProbeResult { version: string; executable: string; modules: Record; } export interface CollectStatusOptions { cwd: string; projectRoot: string; exec: ProbeExecutor; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; now?: () => Date; } function capability( id: string, label: string, state: CapabilityCheck["state"], requiredFor: string[], detail: string, extra: { path?: string; version?: string } = {}, ): CapabilityCheck { return { id, label, state, requiredFor, detail, ...extra }; } async function safeExec(exec: ProbeExecutor, command: string, args: string[], cwd: string) { try { return await exec(command, args, { cwd, timeout: 15_000 }); } catch { return undefined; } } function firstExisting(paths: string[]): string | undefined { return paths.find((candidate) => existsSync(candidate)); } function resolveDependencyRoot(projectRoot: string, packageName: string): string { try { const require = createRequire(path.join(projectRoot, "package.json")); return path.dirname(require.resolve(`${packageName}/package.json`)); } catch { return path.join(projectRoot, "node_modules", packageName); } } function installedPackageVersion(root: string): string | undefined { try { const value = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as { version?: unknown }; return typeof value.version === "string" ? value.version : undefined; } catch { return undefined; } } function pinnedVersions(): Map { return new Map(PDF_PINS.map((pin) => { const [name, version] = pin.split("=="); return [name!, version!]; })); } function wordCandidates(env: NodeJS.ProcessEnv): string[] { const roots = [env.ProgramFiles, env["ProgramFiles(x86)"], env.ProgramW6432].filter( (value): value is string => Boolean(value), ); return [...new Set(roots.flatMap((root) => [ path.join(root, "Microsoft Office", "Root", "Office16", "WINWORD.EXE"), path.join(root, "Microsoft Office", "Office16", "WINWORD.EXE"), ]))]; } function parsePython(stdout: string): PythonProbeResult | undefined { const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); for (let index = lines.length - 1; index >= 0; index -= 1) { try { const value = JSON.parse(lines[index]!) as PythonProbeResult; if (value && typeof value.version === "string" && typeof value.executable === "string") return value; } catch { // Ignore launcher banners and inspect an earlier line. } } return undefined; } async function probePython(exec: ProbeExecutor, cwd: string): Promise { const candidates = [ { command: "py.exe", args: ["-3", "-c", PYTHON_PROBE] }, { command: "python.exe", args: ["-c", PYTHON_PROBE] }, { command: "python", args: ["-c", PYTHON_PROBE] }, ]; for (const candidate of candidates) { const result = await safeExec(exec, candidate.command, candidate.args, cwd); if (result?.code === 0) { const parsed = parsePython(result.stdout); if (parsed) return parsed; } } return undefined; } async function locateCommand(exec: ProbeExecutor, cwd: string, names: string[]): Promise { for (const name of names) { const result = await safeExec(exec, "where.exe", [name], cwd); if (result?.code === 0) { const found = result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean); if (found) return found; } } return undefined; } export async function collectOfficeStatus(options: CollectStatusOptions): Promise { const platform = options.platform ?? process.platform; const env = options.env ?? process.env; const now = options.now ?? (() => new Date()); const capabilities: CapabilityCheck[] = []; const isWindows = platform === "win32"; capabilities.push(capability( "platform.windows", "Windows v0.1 运行平台", isWindows ? "available" : "unsupported", ["all Word operations", "supported v0.1 runtime"], isWindows ? "当前运行于 Windows。" : `当前平台 ${platform} 不在 v0.1 支持范围内。`, )); if (isWindows) { let wordPath = firstExisting(wordCandidates(env)); if (!wordPath) wordPath = await locateCommand(options.exec, options.cwd, ["WINWORD.EXE"]); capabilities.push(capability( "word.executable", "Microsoft Word 可执行文件", wordPath ? "available" : "missing", ["DOCX rendering", "Word-to-PDF", "shape fallback"], wordPath ? "已发现 Microsoft Word。" : "未在常见 Office 路径或 PATH 中发现 WINWORD.EXE。", wordPath ? { path: wordPath } : {}, )); const comResult = await safeExec(options.exec, "reg.exe", ["query", "HKCR\\Word.Application\\CLSID", "/ve"], options.cwd); capabilities.push(capability( "word.com-registration", "Microsoft Word COM 注册", comResult?.code === 0 ? "available" : "missing", ["high-fidelity PDF conversion", "text-box and shape fallback"], comResult?.code === 0 ? "Word.Application COM 类已注册;检测未启动 Word。" : "未检测到 Word.Application COM 注册。", )); const powershellPath = await locateCommand(options.exec, options.cwd, ["pwsh.exe", "powershell.exe"]); capabilities.push(capability( "runtime.powershell", "PowerShell", powershellPath ? "available" : "missing", ["Word COM helper scripts"], powershellPath ? "已发现 PowerShell。" : "未发现 pwsh.exe 或 powershell.exe。", powershellPath ? { path: powershellPath } : {}, )); const windowsRoot = env.WINDIR ?? "C:\\Windows"; const fontPath = firstExisting([ path.join(windowsRoot, "Fonts", "simsun.ttc"), path.join(windowsRoot, "Fonts", "simsunb.ttf"), path.join(windowsRoot, "Fonts", "msyh.ttc"), ]); capabilities.push(capability( "font.chinese", "中文字体(宋体/微软雅黑)", fontPath ? "available" : "missing", ["Chinese DOCX styling", "Chinese PDF overlays"], fontPath ? "已发现可用中文字体。" : "未发现预期的宋体或微软雅黑字体文件。", fontPath ? { path: fontPath } : {}, )); } else { for (const [id, label, requiredFor] of [ ["word.executable", "Microsoft Word 可执行文件", ["DOCX rendering", "Word-to-PDF"]], ["word.com-registration", "Microsoft Word COM 注册", ["shape fallback"]], ["runtime.powershell", "PowerShell", ["Word COM helper scripts"]], ["font.chinese", "中文字体(宋体/微软雅黑)", ["Chinese document output"]], ] as const) { capabilities.push(capability(id, label, "unsupported", [...requiredFor], "该检测项仅在 Windows v0.1 上启用。")); } } const python = await probePython(options.exec, options.cwd); capabilities.push(capability( "runtime.python", "Python 3", python ? "available" : "missing", ["PDF worker", "Word COM fallback"], python ? "已找到可执行的 Python 3 解释器。" : "未找到可执行的 Python 3 解释器。", python ? { path: python.executable, version: python.version } : {}, )); for (const module of PYTHON_MODULES) { const detected = python?.modules[module.importName]; capabilities.push(capability( `python.${module.distribution.toLowerCase()}`, `Python 模块 ${module.distribution}`, detected?.available ? "available" : "missing", [...module.requiredFor], detected?.available ? "全局 Python 环境中已发现;状态查询未做任何安装。" : "全局 Python 环境中未发现;只能在明确确认后安装到私有环境。", detected?.version ? { version: detected.version } : {}, )); } const privatePythonPath = path.join(options.projectRoot, ".runtime", "python", "Scripts", "python.exe"); let privatePython: PythonProbeResult | undefined; if (existsSync(privatePythonPath)) { const privateResult = await safeExec(options.exec, privatePythonPath, ["-c", PYTHON_PROBE], options.cwd); if (privateResult?.code === 0) privatePython = parsePython(privateResult.stdout); } const privatePins = pinnedVersions(); const privatePdfReady = Boolean(privatePython && PYTHON_MODULES .filter((module) => privatePins.has(module.distribution)) .every((module) => { const detected = privatePython?.modules[module.importName]; return detected?.available === true && detected.version === privatePins.get(module.distribution); })); capabilities.push(capability( "runtime.pdf-private", "项目私有 PDF Python 环境", privatePdfReady ? "available" : existsSync(privatePythonPath) ? "missing" : "not-installed", ["all PDF operations"], privatePdfReady ? "固定版本 PDF 依赖已安装在项目 .runtime 中。" : existsSync(privatePythonPath) ? "私有环境存在,但依赖不完整、版本不匹配或无法启动。" : "尚未创建;正常启动和状态查询不会自动安装。", privatePython ? { path: privatePython.executable, version: privatePython.version } : existsSync(privatePythonPath) ? { path: privatePythonPath } : {}, )); const bunRoot = resolveDependencyRoot(options.projectRoot, "bun"); const bunPath = firstExisting([ path.join(bunRoot, "bin", "bun.exe"), path.join(options.projectRoot, "node_modules", ".bin", "bun.exe"), ]); const bunVersion = bunPath ? installedPackageVersion(bunRoot) : undefined; capabilities.push(capability( "local.bun", "项目本地 Bun 1.3.13", bunPath && bunVersion === "1.3.13" ? "available" : "not-installed", ["bun-docx execution"], bunPath && bunVersion === "1.3.13" ? "已发现项目本地 Bun 1.3.13。" : bunPath ? `检测到 Bun ${bunVersion ?? "未知版本"},但需要 1.3.13。` : "项目未保留 node_modules;正常启动不会自动安装。", bunPath ? { path: bunPath, ...(bunVersion ? { version: bunVersion } : {}) } : { version: "1.3.13" }, )); const bunDocxRoot = resolveDependencyRoot(options.projectRoot, "bun-docx"); const bunDocxPath = firstExisting([ path.join(bunDocxRoot, "package.json"), path.join(options.projectRoot, "node_modules", ".bin", "bun-docx.cmd"), ]); const bunDocxVersion = installedPackageVersion(bunDocxRoot); capabilities.push(capability( "local.bun-docx", "项目本地 bun-docx 0.24.0", bunDocxPath && bunDocxVersion === "0.24.0" ? "available" : "not-installed", ["DOCX creation and structural editing"], bunDocxPath && bunDocxVersion === "0.24.0" ? "已发现项目本地 bun-docx 0.24.0。" : bunDocxPath ? `检测到 bun-docx ${bunDocxVersion ?? "未知版本"},但需要 0.24.0。` : "项目未保留 node_modules;正常启动不会自动安装。", bunDocxPath ? { path: bunDocxPath, ...(bunDocxVersion ? { version: bunDocxVersion } : {}) } : { version: "0.24.0" }, )); const nextSteps: string[] = []; if (!isWindows) nextSteps.push("在 Windows + Microsoft Word 环境中运行 v0.1。 "); if (!privatePdfReady) nextSteps.push("运行 office_setup 或 /word-pdf-setup,在一次明确确认后创建私有 PDF 环境。 "); if (capabilities.some((item) => item.id.startsWith("local.") && item.state === "not-installed")) nextSteps.push("运行项目本地 npm 安装以启用 bun-docx;状态查询不会自动安装。 "); nextSteps.push("Word 与 PDF 工具已实现;每个输出仍需检查返回的验证结果和页面预览。"); return { schemaVersion: 1, stage: "stage-3-word-pdf", generatedAt: now().toISOString(), platform, cwd: options.cwd, projectRoot: options.projectRoot, installationPerformed: false, capabilities, dependencyPlan: DEPENDENCY_PLAN, nextSteps: nextSteps.map((step) => step.trim()), }; }