feat(word): Word_Save/Word_SaveAs 真实现(COM 落盘),替换路径回显占位

This commit is contained in:
yu 2026-11-06 11:52:56 +08:00
parent b18d5b1f0b
commit 4b5c73b752
2 changed files with 52 additions and 1 deletions

View File

@ -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);
});

View File

@ -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<string> {
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;
});