import { Type } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import { OfficeError } from "../core/errors.ts"; import { cleanupPreviewDirectory } from "../core/temp.ts"; import { wordCommentAdd, wordComments, wordApplyStyle, wordConvertLegacy, wordConvertPdf, wordCreate, wordDiff, wordEdit, wordInspect, wordRead, wordRender, wordReplace, wordTable, wordTrackChanges, wordValidate, } from "./operations.ts"; import { WordRuntime } from "./runtime.ts"; import { chooseStyle } from "./style-dialog.ts"; import { StylePresetSchema, StyleSchema } from "./style-schema.ts"; const OptionalPath = Type.Optional(Type.String({ description: "Path relative to ctx.cwd or an absolute Windows path" })); const Overwrite = Type.Optional(Type.Boolean({ description: "Explicitly allow replacing an existing output; default false" })); const PreviewDirectory = Type.Optional(Type.String({ description: "Optional preview directory; default is a managed temporary directory" })); function textResult(text: string, details: unknown) { return { content: [{ type: "text" as const, text }], details }; } function jsonResult(label: string, details: unknown) { return textResult(`${label}\n${JSON.stringify(details, null, 2)}`, details); } function makeRuntime(pi: ExtensionAPI, projectRoot: string, cwd: string, signal?: AbortSignal): WordRuntime { return new WordRuntime({ projectRoot, cwd, exec: (command, args, options) => pi.exec(command, args, { ...(options?.cwd ? { cwd: options.cwd } : {}), ...(options?.timeout !== undefined ? { timeout: options.timeout } : {}), ...((signal ?? options?.signal) ? { signal: (signal ?? options?.signal)! } : {}), }), ...(signal ? { signal } : {}), }); } async function interactiveCreate(pi: ExtensionAPI, projectRoot: string, ctx: ExtensionCommandContext): Promise { if (!ctx.hasUI) throw new OfficeError("/word-pdf-create 需要交互式 UI。", "INTERACTIVE_UI_REQUIRED"); const outputPath = await ctx.ui.input("输出 DOCX 路径", "例如 报告.docx", ctx.signal ? { signal: ctx.signal } : undefined); if (outputPath === undefined) throw new OfficeError("文档创建已取消。", "CREATE_CANCELLED"); const normalizedOutputPath = outputPath.trim(); if (!normalizedOutputPath) throw new OfficeError("必须提供输出 DOCX 路径。", "OUTPUT_REQUIRED"); const titleValue = await ctx.ui.input("文档标题", "留空则不设置标题", ctx.signal ? { signal: ctx.signal } : undefined); if (titleValue === undefined) throw new OfficeError("文档创建已取消。", "CREATE_CANCELLED"); const title = titleValue.trim() || undefined; const typeChoice = await ctx.ui.select("文档类型", ["通用", "报告", "合同/正式函件", "新闻稿"], ctx.signal ? { signal: ctx.signal } : undefined); if (typeChoice === undefined) throw new OfficeError("文档创建已取消。", "CREATE_CANCELLED"); const documentType = ({ "通用": "general", "报告": "report", "合同/正式函件": "contract", "新闻稿": "news" } as const)[typeChoice] ?? "general"; const selectedStyle = await chooseStyle(ctx); const markdown = await ctx.ui.editor("文档内容(Markdown)", title ? `# ${title}\n\n` : ""); if (markdown === undefined) throw new OfficeError("文档创建已取消。", "CREATE_CANCELLED"); const result = await wordCreate(makeRuntime(pi, projectRoot, ctx.cwd, ctx.signal), { outputPath: normalizedOutputPath, ...(title !== undefined ? { title } : {}), documentType, ...selectedStyle, ...(markdown !== undefined ? { markdown } : {}), }); ctx.ui.notify(`文档已创建:${result.outputPath}\n样式:${result.style?.label ?? "默认"}\n预览:${result.previewDirectory}`, "info"); } export function registerWordTools(pi: ExtensionAPI, projectRoot: string): void { pi.registerTool({ name: "word_create", label: "Word Create", description: "Create a styled DOCX using the editorial, business, or formal preset, or validated custom typography and margins. No default tables; validates and renders Word page previews.", parameters: Type.Object({ outputPath: Type.String(), title: Type.Optional(Type.String()), author: Type.Optional(Type.String()), documentType: Type.Optional(Type.Union([Type.Literal("general"), Type.Literal("report"), Type.Literal("contract"), Type.Literal("news")])), markdown: Type.Optional(Type.String()), text: Type.Optional(Type.String()), stylePreset: StylePresetSchema, style: StyleSchema, overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const selectedStyle = params.stylePreset === undefined && params.style === undefined && ctx.hasUI ? await chooseStyle(ctx) : {}; const result = await wordCreate(makeRuntime(pi, projectRoot, ctx.cwd, signal), { ...params, ...selectedStyle }); return jsonResult("Word 文档已创建、验证并渲染。", result); }, }); pi.registerCommand("word-pdf-create", { description: "Interactively create a DOCX and choose a preset or custom typography, margins, and page options", handler: async (_args, ctx) => { try { await interactiveCreate(pi, projectRoot, ctx); } catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "error"); } }, }); pi.registerTool({ name: "word_apply_style", label: "Word Apply Style", description: "Apply an editorial, business, formal, or custom style to an existing DOCX while preserving its content and tables. Creates a validated, rendered copy by default.", parameters: Type.Object({ inputPath: Type.String(), documentType: Type.Optional(Type.Union([Type.Literal("general"), Type.Literal("report"), Type.Literal("contract"), Type.Literal("news")])), stylePreset: StylePresetSchema, style: StyleSchema, outputPath: OptionalPath, overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const selectedStyle = params.stylePreset === undefined && params.style === undefined && ctx.hasUI ? await chooseStyle(ctx) : {}; const result = await wordApplyStyle(makeRuntime(pi, projectRoot, ctx.cwd, signal), { ...params, ...selectedStyle }); return jsonResult("Word 文档排版已应用、验证并渲染。", result); }, }); pi.registerTool({ name: "word_read", label: "Word Read", description: "Read DOCX content as locator-annotated Markdown or a lossless JSON AST. Read-only.", parameters: Type.Object({ inputPath: Type.String(), view: Type.Optional(Type.Union([Type.Literal("accepted"), Type.Literal("current"), Type.Literal("baseline")])), comments: Type.Optional(Type.Boolean()), from: Type.Optional(Type.String()), to: Type.Optional(Type.String()), ast: Type.Optional(Type.Boolean()), }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordRead(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return textResult(result.content, result); }, }); pi.registerTool({ name: "word_inspect", label: "Word Inspect", description: "Inspect DOCX validation, protection, revision state, tables, sections, text boxes, and complex shapes using Word COM. Read-only.", parameters: Type.Object({ inputPath: Type.String() }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordInspect(makeRuntime(pi, projectRoot, ctx.cwd, signal), params.inputPath); return jsonResult("Word 文档检查结果:", result); }, }); pi.registerTool({ name: "word_replace", label: "Word Replace", description: "Replace text while preserving existing DOCX structure and formatting. Checks text boxes through Word COM and never overwrites by default.", parameters: Type.Object({ inputPath: Type.String(), pattern: Type.String(), replacement: Type.String(), outputPath: OptionalPath, all: Type.Optional(Type.Boolean()), regex: Type.Optional(Type.Boolean()), ignoreCase: Type.Optional(Type.Boolean()), track: Type.Optional(Type.Boolean()), overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordReplace(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("Word 替换已完成、验证并渲染。", result); }, }); pi.registerTool({ name: "word_edit", label: "Word Edit", description: "Edit one bun-docx locator with literal text or Markdown while preserving untouched document structure.", parameters: Type.Object({ inputPath: Type.String(), locator: Type.String(), text: Type.Optional(Type.String()), markdown: Type.Optional(Type.String()), outputPath: OptionalPath, track: Type.Optional(Type.Boolean()), overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordEdit(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("Word 定位编辑已完成、验证并渲染。", result); }, }); pi.registerTool({ name: "word_comment_add", label: "Word Comment Add", description: "Add a Word comment at a locator or anchored phrase and produce a validated copy.", parameters: Type.Object({ inputPath: Type.String(), locator: Type.Optional(Type.String()), anchor: Type.Optional(Type.String()), text: Type.String(), author: Type.Optional(Type.String()), outputPath: OptionalPath, overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordCommentAdd(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("Word 批注已添加。", result); }, }); pi.registerTool({ name: "word_comments", label: "Word Comments", description: "List, reply to, resolve, reopen, or delete Word comments. Mutations create a validated copy by default.", parameters: Type.Object({ inputPath: Type.String(), action: Type.Union([Type.Literal("list"), Type.Literal("reply"), Type.Literal("resolve"), Type.Literal("reopen"), Type.Literal("delete")]), locator: Type.Optional(Type.String()), text: Type.Optional(Type.String()), author: Type.Optional(Type.String()), outputPath: OptionalPath, overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordComments(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("Word 批注操作结果:", result); }, }); pi.registerTool({ name: "word_track_changes", label: "Word Track Changes", description: "List, enable, disable, accept, or reject Word tracked changes. Mutations create a validated copy by default.", parameters: Type.Object({ inputPath: Type.String(), action: Type.Union([Type.Literal("list"), Type.Literal("on"), Type.Literal("off"), Type.Literal("accept"), Type.Literal("reject")]), locator: Type.Optional(Type.String()), all: Type.Optional(Type.Boolean()), outputPath: OptionalPath, overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordTrackChanges(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("Word 修订操作结果:", result); }, }); pi.registerTool({ name: "word_table", label: "Word Table", description: "Create or safely modify Word tables only when the user explicitly requests a table, then validate and render the result.", parameters: Type.Object({ inputPath: Type.String(), action: Type.Union([Type.Literal("create"), Type.Literal("insert-row"), Type.Literal("delete-row"), Type.Literal("set-widths"), Type.Literal("format")]), locator: Type.Optional(Type.String()), placement: Type.Optional(Type.Union([Type.Literal("before"), Type.Literal("after"), Type.Literal("start"), Type.Literal("end")])), rows: Type.Optional(Type.Integer({ minimum: 1 })), columns: Type.Optional(Type.Integer({ minimum: 1 })), position: Type.Optional(Type.Integer({ minimum: 0 })), cells: Type.Optional(Type.Array(Type.String())), widths: Type.Optional(Type.String()), tableWidth: Type.Optional(Type.String()), borders: Type.Optional(Type.Union([Type.Literal("single"), Type.Literal("double"), Type.Literal("none")])), layout: Type.Optional(Type.Union([Type.Literal("autofit"), Type.Literal("fixed")])), shade: Type.Optional(Type.String()), verticalAlign: Type.Optional(Type.Union([Type.Literal("top"), Type.Literal("center"), Type.Literal("bottom")])), horizontalAlign: Type.Optional(Type.Union([Type.Literal("left"), Type.Literal("center"), Type.Literal("right"), Type.Literal("justify")])), outputPath: OptionalPath, overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordTable(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("Word 表格操作已完成。", result); }, }); pi.registerTool({ name: "word_diff", label: "Word Diff", description: "Read-only locator-normalized diff between a DOCX and a DOCX or saved read baseline.", parameters: Type.Object({ inputPath: Type.String(), againstPath: Type.String() }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordDiff(makeRuntime(pi, projectRoot, ctx.cwd, signal), params.inputPath, params.againstPath); return textResult(result.diff, result); }, }); pi.registerTool({ name: "word_validate", label: "Word Validate", description: "Schema-validate a DOCX without changing it.", parameters: Type.Object({ inputPath: Type.String() }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordValidate(makeRuntime(pi, projectRoot, ctx.cwd, signal), params.inputPath); return jsonResult("Word 验证结果:", result); }, }); pi.registerTool({ name: "word_render", label: "Word Render", description: "Render DOCX pages through Microsoft Word and bundled PDFium for visual inspection. Read-only for the input document.", parameters: Type.Object({ inputPath: Type.String(), outputDirectory: OptionalPath }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordRender(makeRuntime(pi, projectRoot, ctx.cwd, signal), params.inputPath, params.outputDirectory); return jsonResult("Word 页面已渲染。检查图片后调用 office_cleanup_previews。", result); }, }); pi.registerTool({ name: "word_convert_pdf", label: "Word Convert PDF", description: "Convert DOCX to PDF using Microsoft Word COM, validate the PDF header, and render page previews.", parameters: Type.Object({ inputPath: Type.String(), outputPath: OptionalPath, overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordConvertPdf(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("Word 已通过 Microsoft Word 转换为 PDF。", result); }, }); pi.registerTool({ name: "word_convert_legacy", label: "Word Convert Legacy", description: "Convert legacy .doc to .docx only after explicit user confirmation, using Microsoft Word COM.", parameters: Type.Object({ inputPath: Type.String(), outputPath: OptionalPath, confirmed: Type.Boolean({ description: "Must be true only after the user explicitly approved legacy conversion" }), overwrite: Overwrite, previewDirectory: PreviewDirectory, }, { additionalProperties: false }), async execute(_id, params, signal, _update, ctx) { const result = await wordConvertLegacy(makeRuntime(pi, projectRoot, ctx.cwd, signal), params); return jsonResult("旧版 Word 文档已转换。", result); }, }); pi.registerTool({ name: "office_cleanup_previews", label: "Office Cleanup Previews", description: "Delete only a managed pi-word-pdf preview directory after page inspection is complete.", parameters: Type.Object({ previewDirectory: Type.String() }, { additionalProperties: false }), async execute(_id, params) { cleanupPreviewDirectory(params.previewDirectory); return jsonResult("预览目录已清理。", { removed: params.previewDirectory }); }, }); }