import { promises as fs } from "node:fs"; import path from "node:path"; import { getHandler, register } from "../engine/registry"; import { RuntimeError } from "../engine/types"; import { powershell, psString } from "./desktop"; interface WordHandle { path: string; } function text(args: Record, keys: string[], fallback = ""): string { for (const key of keys) { const current = args[key]; if (current !== undefined && current !== null && current !== "") return String(current); } return fallback; } function bool(value: unknown): boolean { return [true, 1, "1", "true", "True", "是", "开启"].includes(value as never); } function handlePath(args: Record, nodeId: string, componentId: string): string { const raw = args.objWord ?? args.filePath ?? args.path; const candidate = raw && typeof raw === "object" ? (raw as Partial).path : raw; const result = String(candidate || "").trim(); if (!result || result === "word对象") throw new RuntimeError("validation_error", "Word文档路径为空", { nodeId, componentId }); return path.resolve(result); } function script(filePath: string, body: string, save = true): string { return `$word=$null;$doc=$null;try{$word=New-Object -ComObject Word.Application;$word.Visible=$false;$word.DisplayAlerts=0;$doc=$word.Documents.Open(${psString(filePath)});${body};${save ? "$doc.Save();" : ""}}finally{if($doc -ne $null){try{$doc.Close()}catch{};try{[void][Runtime.InteropServices.Marshal]::ReleaseComObject($doc)}catch{}};if($word -ne $null){try{$word.Quit()}catch{};try{[void][Runtime.InteropServices.Marshal]::ReleaseComObject($word)}catch{}};[GC]::Collect();[GC]::WaitForPendingFinalizers()}`; } register("Word_New_V1", async (ctx, node, args, component) => { const base = getHandler("Word_New"); if (!base) throw new RuntimeError("capability_not_implemented", "基础 Word 新建能力不可用", { nodeId: node.id, componentId: node.componentId }); return base(ctx, node, args, component); }); register("Word_GetPath", (_ctx, node, args) => handlePath(args, node.id, node.componentId)); register("Word_ReadAll", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); const encoded = await powershell(script(filePath, "[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($doc.Content.Text))", false)); return Buffer.from(encoded.trim(), "base64").toString("utf8"); }); register("Word_Replace", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); const oldText = text(args, ["oldText"]); if (!oldText) throw new RuntimeError("validation_error", "原内容为空", { nodeId: node.id, componentId: node.componentId }); const newText = text(args, ["newText"]); await powershell(script(filePath, `$find=$doc.Content.Find;[void]$find.Execute(${psString(oldText)},$false,$false,$false,$false,$false,$true,1,$false,${psString(newText)},2)`)); return { path: filePath } satisfies WordHandle; }); register("Word_Enter", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); await powershell(script(filePath, "$range=$doc.Content;$range.Collapse(0);$range.InsertParagraphAfter()")); return { path: filePath } satisfies WordHandle; }); register("Word_InsertPage", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); await powershell(script(filePath, "$range=$doc.Content;$range.Collapse(0);$range.InsertBreak(7)")); return { path: filePath } satisfies WordHandle; }); register("Word_SetFontName", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); await powershell(script(filePath, `$doc.Content.Font.Name=${psString(text(args, ["font"], "宋体"))}`)); return { path: filePath } satisfies WordHandle; }); register("Word_SetFontSize", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); const size = Number(text(args, ["fontSize"], "9")); if (!(size > 0)) throw new RuntimeError("validation_error", "字体大小无效", { nodeId: node.id, componentId: node.componentId }); await powershell(script(filePath, `$doc.Content.Font.Size=${size}`)); return { path: filePath } satisfies WordHandle; }); register("Word_SetFontStyle", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); await powershell(script(filePath, `$doc.Content.Font.Bold=${bool(args.isBold) ? 1 : 0};$doc.Content.Font.Italic=${bool(args.isItalic) ? 1 : 0};$doc.Content.Font.Underline=${bool(args.isUnderline) ? 1 : 0}`)); return { path: filePath } satisfies WordHandle; }); register("Word_SetFontColor", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); const hex = text(args, ["fontColor"], "#000000").replace("#", ""); if (!/^[0-9a-fA-F]{6}$/.test(hex)) throw new RuntimeError("validation_error", "字体颜色格式无效", { nodeId: node.id, componentId: node.componentId }); const red = Number.parseInt(hex.slice(0, 2), 16); const green = Number.parseInt(hex.slice(2, 4), 16); const blue = Number.parseInt(hex.slice(4, 6), 16); await powershell(script(filePath, `$doc.Content.Font.Color=${red + green * 256 + blue * 65536}`)); return { path: filePath } satisfies WordHandle; }); register("Word_SetAlign", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); const align = text(args, ["align"], "left").toLowerCase(); const value = align.includes("center") || align === "1" ? 1 : align.includes("right") || align === "2" ? 2 : align.includes("justify") || align === "3" ? 3 : 0; await powershell(script(filePath, `$doc.Content.ParagraphFormat.Alignment=${value}`)); return { path: filePath } satisfies WordHandle; }); register("Word_InsertPicture", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); const image = path.resolve(text(args, ["imageaddr"])); await fs.access(image); await powershell(script(filePath, `$range=$doc.Content;$range.Collapse(0);[void]$doc.InlineShapes.AddPicture(${psString(image)},$false,$true,$range)`)); return { path: filePath } satisfies WordHandle; }); register("Word_WriteTable", async (_ctx, node, args) => { const filePath = handlePath(args, node.id, node.componentId); let data: unknown = args.data; if (typeof data === "string") { try { data = JSON.parse(data); } catch { data = [[data]]; } } const rows = Array.isArray(data) ? data.map((row) => Array.isArray(row) ? row : [row]) : [[data]]; const columns = Math.max(1, ...rows.map((row) => row.length)); const encoded = Buffer.from(JSON.stringify(rows), "utf8").toString("base64"); await powershell(script(filePath, `$json=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(${psString(encoded)}));$data=$json|ConvertFrom-Json;$range=$doc.Content;$range.Collapse(0);$table=$doc.Tables.Add($range,${rows.length},${columns});for($r=0;$r-lt${rows.length};$r++){for($c=0;$c-lt${columns};$c++){if($c-lt$data[$r].Count){$table.Cell($r+1,$c+1).Range.Text=[string]$data[$r][$c]}}};$table.Borders.Enable=${bool(args.is_border ?? "1") ? 1 : 0}`)); return { path: filePath } satisfies WordHandle; }); register("Word_Close", (_ctx, node, args) => ({ path: handlePath(args, node.id, node.componentId), closed: true }));