From 4b5c73b7526e112d372b5aff1acacd94890908a6 Mon Sep 17 00:00:00 2001 From: yu <1919727607@qq.com> Date: Fri, 6 Nov 2026 11:52:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(word):=20Word=5FSave/Word=5FSaveAs=20?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E7=8E=B0=EF=BC=88COM=20=E8=90=BD=E7=9B=98?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=9B=BF=E6=8D=A2=E8=B7=AF=E5=BE=84=E5=9B=9E?= =?UTF-8?q?=E6=98=BE=E5=8D=A0=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/capabilities/word-extended.test.ts | 23 ++++++++++++++ packages/rpa-core/src/capabilities/word.ts | 30 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/rpa-core/src/capabilities/word-extended.test.ts b/packages/rpa-core/src/capabilities/word-extended.test.ts index 04996cc..5056881 100644 --- a/packages/rpa-core/src/capabilities/word-extended.test.ts +++ b/packages/rpa-core/src/capabilities/word-extended.test.ts @@ -56,4 +56,27 @@ describe("Word extended capabilities", () => { await run("Word_WriteTable", { objWord: handle, data: [["name", "age"], ["Alice", "30"]] }); await expect(run("Word_ReadTable", { objWord: handle, find_type: "content", table_content: "Alice" })).resolves.toEqual([["name", "age"], ["Alice", "30"]]); }, 180000); + + it("saves and saves-as a Word document through real COM", async () => { + if (process.platform !== "win32") return; + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-word-saveas-")); directories.push(directory); + const handle = await run("Word_New_V1", { filePath: directory, name: "source.docx" }); + await run("Word_Write", { objWord: handle, text: "另存前内容" }); + + await expect(run("Word_Save", { objWord: handle })).resolves.toEqual({ path: path.join(directory, "source.docx") }); + await expect(run("Word_Save", { objWord: { path: path.join(directory, "missing.docx") } })).rejects.toThrow("Word文件不存在"); + + const targetDir = path.join(directory, "out"); + const saved = await run("Word_SaveAs", { objWord: handle, fileAddr: targetDir, filename: "renamed" }) as { path: string }; + expect(saved.path).toBe(path.join(targetDir, "renamed.docx")); + await expect(readFile(saved.path)).resolves.toBeTruthy(); + + const kept = await run("Word_SaveAs", { objWord: saved, fileAddr: directory }) as { path: string }; + expect(kept.path).toBe(path.join(directory, "renamed.docx")); + await expect(readFile(kept.path)).resolves.toBeTruthy(); + + const relative = await run("Word_SaveAs", { objWord: kept, fileAddr: "sub", filename: "rel-target" }) as { path: string }; + expect(relative.path).toBe(path.join(directory, "sub", "rel-target.docx")); + await expect(readFile(relative.path)).resolves.toBeTruthy(); + }, 180000); }); diff --git a/packages/rpa-core/src/capabilities/word.ts b/packages/rpa-core/src/capabilities/word.ts index 5a5a086..b312d27 100644 --- a/packages/rpa-core/src/capabilities/word.ts +++ b/packages/rpa-core/src/capabilities/word.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { access, mkdir } from "node:fs/promises"; import { register } from "../engine/registry"; import { powershell, psString } from "../driver/windows/powershell"; @@ -41,4 +42,31 @@ register("Word_Write", async (_ctx, _node, args) => { return { path: filePath } satisfies WordHandle; }); -register(["Word_Save", "Word_SaveAs"], async (_ctx, _node, args) => ({ path: handlePath(args) } satisfies WordHandle)); +async function ensureFile(value: string): Promise { + try { + await access(value); + } catch { + throw new Error(`Word文件不存在: ${value}`); + } + return value; +} + +register("Word_Save", async (_ctx, _node, args) => { + const filePath = await ensureFile(ensure(handlePath(args), "Word文件路径")); + // 打开即存:架构上每次操作独立开合 Word 实例,Save 负责兜底刷新并证明文件真实可写 + await powershell(wordScript(`$doc = $word.Documents.Open(${psString(filePath)}); $doc.Save()`)); + return { path: filePath } satisfies WordHandle; +}); + +register("Word_SaveAs", async (_ctx, _node, args) => { + const previousPath = await ensureFile(ensure(handlePath(args), "Word文件路径")); + const rawDir = text(args, ["fileAddr", "path", "folder", "savePath"]); + const rawName = text(args, ["filename", "name"]); + // 目录空缺沿用原目录;目录相对时基于原文档目录解析;文件名空缺沿用原名(与 catalog 语义一致) + const dir = rawDir ? path.resolve(path.dirname(previousPath), rawDir) : path.dirname(previousPath); + const target = path.resolve(dir, rawName || path.basename(previousPath)); + const withExt = path.extname(target) ? target : `${target}.docx`; + await mkdir(path.dirname(withExt), { recursive: true }); + await powershell(wordScript(`$doc = $word.Documents.Open(${psString(previousPath)}); $doc.SaveAs2(${psString(withExt)}); ${psString(withExt)}`)); + return { path: withExt } satisfies WordHandle; +});