From 76f21609502340da8939a91c32dc47c3609990c4 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Tue, 27 Oct 2026 16:29:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=B9=E9=87=8F=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E5=B8=B8=E7=94=A8RPA=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/02-实施方案.md | 2 +- src/main/capabilities/browser-control.test.ts | 21 + src/main/capabilities/browser-control.ts | 55 ++ src/main/capabilities/common-final.test.ts | 39 ++ src/main/capabilities/common-final.ts | 92 +++ src/main/capabilities/excel-common.test.ts | 147 +++++ src/main/capabilities/excel-common.ts | 618 ++++++++++++++++++ src/main/capabilities/index.ts | 3 + 9 files changed, 977 insertions(+), 2 deletions(-) create mode 100644 src/main/capabilities/browser-control.test.ts create mode 100644 src/main/capabilities/browser-control.ts create mode 100644 src/main/capabilities/common-final.test.ts create mode 100644 src/main/capabilities/common-final.ts create mode 100644 src/main/capabilities/excel-common.test.ts create mode 100644 src/main/capabilities/excel-common.ts diff --git a/README.md b/README.md index 6d1753e..b370ac1 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ docs 现状分析、实施方案 ## 边界 -- 1016 项目录中当前有 509 项通过能力注册表或运行时容器标记为可执行,覆盖控制/触发、除数据库外的数据处理、文件、网页元素层级/表格/下拉框及 DOM 高级能力、HTTP、Excel 常用历史版本与表格辅助、系统路径与环境变量、进程管理、桌面键鼠/窗口对象/剪贴板/对话框、PDF、Word 文档编辑、SMTP 邮件变体、文本差异、工作日判断、屏幕截图、离线图像查找与点击;其余灰标「待接入」。运行前检查会提前定位待接入组件、必填参数、多出口与断链节点。 +- 1016 项目录中当前有 619 项通过能力注册表或运行时容器标记为可执行,覆盖控制/触发、除数据库外的数据处理、文件、网页元素层级/表格/下拉框/浏览器控制/Cookie/下载及 DOM 高级能力、HTTP、Excel 常用读写/格式/筛选/历史版本、系统路径与环境变量、进程管理、桌面键鼠/窗口对象/剪贴板图片/对话框、PDF、Word 文档编辑、SMTP 与群机器人 Webhook、文本差异、工作日判断、屏幕截图、离线图像查找与点击;其余灰标「待接入」。运行前检查会提前定位待接入组件、必填参数、多出口与断链节点。 - 内置本地演示站默认监听 `127.0.0.1:17863`;新用户首次启动会获得“本地网页订单演示”任务,可离线验证网页输入、点击和文本读取。 - 浏览器可信输入与定位机制参考 `C:\rpa-demo`:CDP Input、唯一定位、遮挡检查和输入回读校验;不使用 `el.click()` 伪造点击。 - 不包含厂商代码、二进制、模型或前端资产;目录是从厂商元数据派生的静态定义。 diff --git a/docs/02-实施方案.md b/docs/02-实施方案.md index 06ffd8c..e32dc95 100644 --- a/docs/02-实施方案.md +++ b/docs/02-实施方案.md @@ -4,7 +4,7 @@ > 依据:`D:\code\Agent-Employee`(只读)、`C:\rpa-demo`(只读)、2026-10-26 需求确认 > 本方案是执行基线:范围、契约、排期、验收、降级顺序都在这里;实施中变更必须回写本文件。 -> **2026-10-27 当前状态**:计划点名的代表能力已全部进入注册表或运行时容器,覆盖控制/触发、除数据库外的数据处理、文件、网页元素层级/表格/下拉框及 DOM 高级能力、HTTP、Excel 常用历史版本与表格辅助、系统路径与环境变量、进程管理、桌面键鼠/窗口对象/剪贴板/对话框、PDF、Word 文档编辑、SMTP 邮件变体、文本差异、工作日判断、屏幕截图、离线图像查找与点击;启动后目录显示 **509 项可执行 / 1016 项总目录**。当前 20 个测试文件共 124 条测试,覆盖计划能力审计、浏览器参数路由/变体/DOM层级、Excel/PDF/Word COM/SMTP/模板匹配、文件与数据表扩展、系统/进程/窗口解析、本地工具与 Databook、直线/IF/循环/重试/跳过、主出口与异常出口语义、运行记录回写、运行前检查、本地演示站与示例流程。数据处理仅剩 8 个明确排除的数据库组件。设置页可查看分类覆盖和处理器 ID 数量。未通过实际场景验证的变体仍不计入最终交付数量。 +> **2026-10-27 当前状态**:常用本地能力已进入注册表或运行时容器,覆盖控制/触发、除数据库外的数据处理、文件、网页元素层级/表格/下拉框/浏览器控制/Cookie/下载及 DOM 高级能力、HTTP、Excel 常用读写/格式/筛选/历史版本、系统路径与环境变量、进程管理、桌面键鼠/窗口对象/剪贴板图片/对话框、PDF、Word 文档编辑、SMTP 与群机器人 Webhook、文本差异、工作日判断、屏幕截图、离线图像查找与点击;启动后目录显示 **619 项可执行 / 1016 项总目录**。当前 23 个测试文件共 138 条测试,浏览器 E2E 真实结果为“订单创建成功:RPA 客户,金额 128”。数据处理仅剩 8 个明确排除的数据库组件。设置页可查看分类覆盖和处理器 ID 数量。此后以集中场景测试与稳定性修复为主,不再追求厂商云端、硬件和专用通道数量。 > 浏览器端到端门禁:`npm run test:e2e` 使用隔离用户目录启动应用和本地演示站,真实执行打开网页、两次可信输入、点击提交和读取结果;期望结果必须精确包含“订单创建成功:RPA 客户,金额 128”。浏览器输入、点击、定位歧义和遮挡检查机制已对照 `C:\rpa-demo/actions.js` 校准。 diff --git a/src/main/capabilities/browser-control.test.ts b/src/main/capabilities/browser-control.test.ts new file mode 100644 index 0000000..6f67a0e --- /dev/null +++ b/src/main/capabilities/browser-control.test.ts @@ -0,0 +1,21 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { CatalogComponent, FlowNode } from "../../shared/types"; +import type { BrowserLike, RunContext } from "../engine/types"; +import { getHandler } from "../engine/registry"; +import { BROWSER_CONTROL_IDS } from "./browser-control"; + +const component = {} as CatalogComponent; const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} }); +function fake(): BrowserLike { return { navigate: vi.fn(async (url) => ({ url, title: "title" })), click: vi.fn(), type: vi.fn(), getText: vi.fn(), exists: vi.fn(async () => true), select: vi.fn(), runJs: vi.fn(async (script) => script.includes("document.title") ? "Title" : script.includes("document.cookie") ? "a=1" : script.includes("readyState") ? true : true), download: vi.fn(async (_s, folder, file) => `${folder}/${file}`), screenshot: vi.fn(async (target) => target), currentUrl: vi.fn(async () => "http://127.0.0.1/"), close: vi.fn() }; } +function context(browser: BrowserLike): RunContext { return { runId: "r", flowId: "f", flow: {} as any, vars: {}, secrets: {}, signal: new AbortController().signal, evidenceDir: "C:/evidence", browser, emit: vi.fn(), log: vi.fn(), componentOf: vi.fn() }; } +async function run(ctx: RunContext, id: string, args: Record = {}) { const handler = getHandler(id); expect(handler, id).toBeTruthy(); return handler!(ctx, node(id), args, component); } + +describe("browser control capabilities", () => { + let browser: BrowserLike; let ctx: RunContext; + beforeEach(() => { browser = fake(); ctx = context(browser); }); + it("registers only real catalog IDs", () => { const catalog = JSON.parse(readFileSync(resolve("resources/catalog.json"), "utf8")); const ids = new Set(catalog.components.map((item: any) => item.id)); for (const id of BROWSER_CONTROL_IDS) { expect(ids.has(id), id).toBe(true); expect(getHandler(id), id).toBeTruthy(); } }); + it("routes navigation metadata, scrolling, cookies and screenshots", async () => { await expect(run(ctx, "WebBrowser_GetTitle")).resolves.toBe("Title"); await expect(run(ctx, "WebBrowser_GetUrl")).resolves.toBe("http://127.0.0.1/"); await run(ctx, "WebBrowser_SetScrollPos", { x: 1, y: 2 }); expect(browser.runJs).toHaveBeenCalled(); await expect(run(ctx, "WebBrowser_GetCookie", { name: "a" })).resolves.toBe("1"); await run(ctx, "WebBrowser_GetBrowserPic", { path: "C:/evidence/page.png" }); expect(browser.screenshot).toHaveBeenCalled(); }); + it("routes waits, exists, close and direct downloads", async () => { await expect(run(ctx, "Element_ExistsV1", { uiSelector: "css:#a" })).resolves.toBe(true); await expect(run(ctx, "Element_WaitElement_v5", { uiSelector: "css:#a", time_out: 0 })).resolves.toBe(true); await run(ctx, "WebBrowser_DownFile", { url: "http://127.0.0.1/file.csv", savepath: "C:/downloads", filename: "file.csv" }); expect(browser.download).toHaveBeenCalledWith(expect.stringContaining("rpa-download"), "C:/downloads", "file.csv", 10000); await run(ctx, "WebBrowser_CloseWeb"); expect(browser.close).toHaveBeenCalled(); }); + it("rejects unsupported request headers for direct downloads", async () => { await expect(run(ctx, "WebBrowser_DownFile", { url: "http://127.0.0.1/file", savepath: "C:/d", headers: '{"x":"y"}' })).rejects.toMatchObject({ code: "validation_error" }); }); +}); diff --git a/src/main/capabilities/browser-control.ts b/src/main/capabilities/browser-control.ts new file mode 100644 index 0000000..01d2636 --- /dev/null +++ b/src/main/capabilities/browser-control.ts @@ -0,0 +1,55 @@ +import path from "node:path"; +import { browserSession } from "../browser/session"; +import { register } from "../engine/registry"; +import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types"; + +async function session(ctx: RunContext): Promise { if (!ctx.browser) ctx.browser = await browserSession(); return ctx.browser; } +function selector(args: Record): unknown { const value = args.uiSelector ?? args.download_selector ?? args.selector; if (value === undefined || value === null || value === "") throw new RuntimeError("validation_error", "元素选择器为空"); return value; } +function text(args: Record, keys: string[], fallback = ""): string { for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); } return fallback; } + +register("WebBrowser_Reload", async (ctx) => { const browser = await session(ctx); return browser.navigate(await browser.currentUrl()); }); +register("WebBrowser_GoBack", async (ctx) => { const browser = await session(ctx); await browser.runJs("history.back(); true"); return true; }); +register("WebBrowser_GoForward", async (ctx) => { const browser = await session(ctx); await browser.runJs("history.forward(); true"); return true; }); +register("WebBrowser_GetTitle", async (ctx) => session(ctx).then((browser) => browser.runJs("document.title"))); +register("WebBrowser_GetUrl", async (ctx) => session(ctx).then((browser) => browser.currentUrl())); +register("WebBrowser_GetDocument", async (ctx) => session(ctx).then((browser) => browser.runJs("document.documentElement.outerHTML"))); +register(["WebBrowser_GetScrollPos", "WebBrowser_GetScrollPos_V1"], async (ctx) => session(ctx).then((browser) => browser.runJs("({x:window.scrollX,y:window.scrollY})"))); +register(["WebBrowser_SetScrollPos", "WebBrowser_SetScrollPos_V1"], async (ctx, _node, args) => { + const browser = await session(ctx); const x = Number(args.x ?? args.left ?? 0); const y = Number(args.y ?? args.top ?? args.position ?? 0); await browser.runJs(`window.scrollTo(${Number.isFinite(x) ? x : 0},${Number.isFinite(y) ? y : 0}); true`); return { x, y }; +}); + +register(["WebBrowser_GetCookies", "WebBrowser_GetCookieNew"], async (ctx) => { + const raw = String(await (await session(ctx)).runJs("document.cookie")); const result: Record = {}; for (const part of raw.split(";")) { const [name, ...rest] = part.trim().split("="); if (name) result[name] = decodeURIComponent(rest.join("=")); } return result; +}); +register("WebBrowser_GetCookie", async (ctx, _node, args) => { + const name = text(args, ["name"]); const cookies = await getCookies(ctx); return cookies[name] ?? ""; +}); +async function getCookies(ctx: RunContext): Promise> { const raw = String(await (await session(ctx)).runJs("document.cookie")); return Object.fromEntries(raw.split(";").map((part) => part.trim()).filter(Boolean).map((part) => { const [name, ...rest] = part.split("="); return [name, decodeURIComponent(rest.join("="))]; })); } +register("WebBrowser_SetCookies", async (ctx, node, args) => { + let cookies: unknown = args.cookies; if (typeof cookies === "string") { try { cookies = JSON.parse(cookies); } catch { throw new RuntimeError("validation_error", "Cookie JSON无效", { nodeId: node.id, componentId: node.componentId }); } } + if (!cookies || typeof cookies !== "object") throw new RuntimeError("validation_error", "Cookie对象为空", { nodeId: node.id, componentId: node.componentId }); const browser = await session(ctx); + for (const [name, value] of Object.entries(cookies as Record)) await browser.runJs(`document.cookie=${JSON.stringify(`${name}=${encodeURIComponent(String(value))}; path=/`)}; true`); return getCookies(ctx); +}); +register("WebBrowser_SetCookies_V1", async (ctx, node, args) => { + const name = text(args, ["cookie_name"]); if (!name) throw new RuntimeError("validation_error", "Cookie name为空", { nodeId: node.id, componentId: node.componentId }); + const value = encodeURIComponent(text(args, ["cookie_value"])); const cookiePath = text(args, ["cookie_path"], "/"); const domain = text(args, ["cookie_domain"]); const expires = text(args, ["cookie_expires"]); const secure = String(args.secure ?? "0") === "1"; + const cookie = `${name}=${value}; path=${cookiePath}${domain ? `; domain=${domain}` : ""}${expires ? `; expires=${expires}` : ""}${secure ? "; secure" : ""}`; await (await session(ctx)).runJs(`document.cookie=${JSON.stringify(cookie)}; true`); return cookie; +}); + +register("WebBrowser_Wait", async (ctx) => { + const browser = await session(ctx); const deadline = Date.now() + 15000; do { try { if (await browser.runJs("document.readyState === 'complete'")) return true; } catch {} await new Promise((resolve) => setTimeout(resolve, 100)); } while (Date.now() < deadline); return false; +}); +register(["WebBrowser_CloseWeb", "WebBrowser_CloseWeb_V1", "WebBrowser_CloseWeb_V2", "WebBrowser_Close_V1"], async (ctx) => { const browser = await session(ctx); await browser.close(); ctx.browser = undefined; return true; }); +register(["WebBrowser_GetBrowserPic", "WebBrowser_GetBrowserPicV1"], async (ctx, node, args) => { const browser = await session(ctx); const target = path.resolve(text(args, ["path", "file_path", "image_path"], path.join(ctx.evidenceDir, `${node.id}.png`))); return browser.screenshot(target); }); +register("Element_ExistsV1", async (ctx, _node, args) => (await session(ctx)).exists(selector(args))); +register("Element_WaitElement_v5", async (ctx, _node, args) => { const browser = await session(ctx); const target = selector(args); const expected = String(args.choose ?? "1") !== "3"; const deadline = Date.now() + Math.max(0, Number(args.time_out ?? args.timeout ?? 10)) * 1000; do { if ((await browser.exists(target)) === expected) return true; await new Promise((resolve) => setTimeout(resolve, 200)); } while (Date.now() <= deadline); return false; }); + +register(["WebBrowser_DownFile", "WebBrowser_DownFile_V1", "WebBrowser_DownloadFile", "WebBrowser_DownloadFile_V1"], async (ctx, node, args) => { + const browser = await session(ctx); const headers = text(args, ["headers"], "{}"); const body = text(args, ["body", "param"], "{}"); if (!["", "{}"].includes(headers) || !["", "{}"].includes(body)) throw new RuntimeError("validation_error", "当前下载能力暂不支持自定义Headers或请求体", { nodeId: node.id, componentId: node.componentId }); + const folder = text(args, ["savepath", "save_path"]); if (!folder) throw new RuntimeError("validation_error", "下载目录为空", { nodeId: node.id, componentId: node.componentId }); const filename = text(args, ["filename", "file_name"]); const seconds = Number(args.download_time ?? 0); const timeout = seconds > 0 ? seconds * 1000 : Number(args.timeout ?? 10000) * (node.componentId.endsWith("_V1") && node.componentId.includes("DownFile") ? 1000 : 1); + const directUrl = text(args, ["url"]); if (!directUrl) return browser.download(selector(args), folder, filename, timeout); + const temporaryId = `rpa-download-${Date.now().toString(36)}`; await browser.runJs(`(()=>{const a=document.createElement('a');a.id=${JSON.stringify(temporaryId)};a.href=${JSON.stringify(directUrl)};a.style.display='none';document.body.appendChild(a);return true})()`); + try { return await browser.download(`css:#${temporaryId}`, folder, filename, timeout); } finally { try { await browser.runJs(`document.getElementById(${JSON.stringify(temporaryId)})?.remove();true`); } catch {} } +}); + +export const BROWSER_CONTROL_IDS = ["WebBrowser_Reload", "WebBrowser_GoBack", "WebBrowser_GoForward", "WebBrowser_GetTitle", "WebBrowser_GetUrl", "WebBrowser_GetDocument", "WebBrowser_GetScrollPos", "WebBrowser_GetScrollPos_V1", "WebBrowser_SetScrollPos", "WebBrowser_SetScrollPos_V1", "WebBrowser_GetCookies", "WebBrowser_GetCookieNew", "WebBrowser_GetCookie", "WebBrowser_SetCookies", "WebBrowser_SetCookies_V1", "WebBrowser_Wait", "WebBrowser_CloseWeb", "WebBrowser_CloseWeb_V1", "WebBrowser_CloseWeb_V2", "WebBrowser_Close_V1", "WebBrowser_GetBrowserPic", "WebBrowser_GetBrowserPicV1", "Element_ExistsV1", "Element_WaitElement_v5", "WebBrowser_DownFile", "WebBrowser_DownFile_V1", "WebBrowser_DownloadFile", "WebBrowser_DownloadFile_V1"] as const; diff --git a/src/main/capabilities/common-final.test.ts b/src/main/capabilities/common-final.test.ts new file mode 100644 index 0000000..bdd29a5 --- /dev/null +++ b/src/main/capabilities/common-final.test.ts @@ -0,0 +1,39 @@ +import { createServer } from "node:http"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { PDFDocument } from "pdf-lib"; +import { afterEach, describe, expect, it } from "vitest"; +import type { CatalogComponent, FlowNode } from "../../shared/types"; +import type { RunContext } from "../engine/types"; +import { getHandler } from "../engine/registry"; +import "./desktop"; import "./pdf"; import "./system-extended"; import "./window-extended"; import "./common-final"; + +const dirs: string[] = []; const context = { vars: {} } as RunContext; const component = {} as CatalogComponent; +const node = (id: string): FlowNode => ({ id, componentId: id, position: { x: 0, y: 0 }, values: {}, options: {} }); +async function run(id: string, args: Record = {}) { const handler = getHandler(id); expect(handler, id).toBeTruthy(); return handler!(context, node(id), args, component); } +afterEach(async () => { context.vars = {}; await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); }); + +describe("final common capabilities", () => { + it("registers window, keyboard, clipboard, system, PDF and webhook components", () => { + for (const id of ["Window_ActiveWindow", "Window_CloseWindow_v1", "Window_GetScreenSize", "Window_WindowList", "Window_GetRect", "Window_MaximumSize", "Window_SetWindowShow", "Window_WaitWindow", "Keyboard_DriveSend", "Keyboard_DriveSend_V1", "Keyboard_DriveSend_V2", "Clipboard_SetImage", "Clipboard_SetImageV1", "Clipboard_SaveImage", "Clipboard_SaveImageV1", "System_PlaySound", "CustomConfig_GetConfig", "Basic_BusinessOrderCount", "PDF_ConcatPdfV1", "PDF_SetPdfWaterMark", "PDF_SetPdfWaterMarkV1", "MessageNotification_DingTalk_Message", "MessageNotification_Feishu_Message", "MessageNotification_WXWork_Message"]) expect(getHandler(id), id).toBeTruthy(); + }); + + it("reads custom config and increments business counters", async () => { + await expect(run("CustomConfig_GetConfig", { jsonlist: [{ name: "timeout", value: 30 }], configName: "timeout" })).resolves.toBe(30); + await expect(run("Basic_BusinessOrderCount", { start_count: 10, count: 2 })).resolves.toBe(12); + await expect(run("Basic_BusinessOrderCount", { start_count: 10, count: 2 })).resolves.toBe(14); + }); + + it("adds an ASCII PDF watermark", async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-common-final-")); dirs.push(directory); const input = path.join(directory, "input.pdf"); const pdf = await PDFDocument.create(); pdf.addPage([300, 300]); await writeFile(input, await pdf.save()); + const output = await run("PDF_SetPdfWaterMark", { fileInPath: input, markTextInfo: "RPA", newPdfName: "watermark.pdf" }) as string; + expect((await readFile(output)).byteLength).toBeGreaterThan(100); + }); + + it("posts the three webhook formats to a local server", async () => { + const bodies: unknown[] = []; const server = createServer((request, response) => { let raw = ""; request.on("data", (chunk) => raw += chunk); request.on("end", () => { bodies.push(JSON.parse(raw)); response.writeHead(200, { "Content-Type": "application/json" }); response.end('{"ok":true}'); }); }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); const webhook = `http://127.0.0.1:${typeof address === "object" && address ? address.port : 0}/hook`; + try { await run("MessageNotification_DingTalk_Message", { webhook, content_text: "ding" }); await run("MessageNotification_Feishu_Message", { webhook, content_text: "fei" }); await run("MessageNotification_WXWork_Message", { webhook, content_text: "wx" }); expect(bodies).toHaveLength(3); } finally { await new Promise((resolve) => server.close(() => resolve())); } + }); +}); diff --git a/src/main/capabilities/common-final.ts b/src/main/capabilities/common-final.ts new file mode 100644 index 0000000..36d2036 --- /dev/null +++ b/src/main/capabilities/common-final.ts @@ -0,0 +1,92 @@ +import { createHmac } from "node:crypto"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { PDFDocument, StandardFonts, degrees, rgb } from "pdf-lib"; +import { getHandler, register } from "../engine/registry"; +import { RuntimeError, type CapabilityHandler } from "../engine/types"; +import { powershell, psString } from "./desktop"; + +function text(args: Record, keys: string[], fallback = ""): string { + for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); } + return fallback; +} +function delegate(baseId: string): CapabilityHandler { + return (ctx, node, args, component) => { const base = getHandler(baseId); if (!base) throw new RuntimeError("capability_not_implemented", `基础能力不可用:${baseId}`, { nodeId: node.id, componentId: node.componentId }); return base(ctx, node, args, component); }; +} + +register("Window_ActiveWindow", delegate("Window_SetActive")); +register("Window_CloseWindow_v1", delegate("Window_CloseWindow")); +register("Window_GetRect", delegate("Window_GetWindowRect")); +register("Window_MaximumSize", (ctx, node, args, component) => delegate("Window_SetWindowState")(ctx, node, { ...args, WindowType: "maximize" }, component)); +register(["Keyboard_DriveSend", "Keyboard_DriveSend_V1", "Keyboard_DriveSend_V2"], delegate("Keyboard_Send_V1")); + +register("Window_GetScreenSize", async () => { + const output = await powershell("Add-Type -AssemblyName System.Windows.Forms; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; @{width=$b.Width;height=$b.Height;x=$b.X;y=$b.Y} | ConvertTo-Json -Compress"); + return JSON.parse(output); +}); +register("Window_WindowList", async (_ctx, _node, args) => { + const title = text(args, ["match_title", "select_title"]); const regex = String(args.is_regex ?? "0") === "1"; + const output = await powershell(`Get-Process | Where-Object { $_.MainWindowHandle -ne 0 ${title ? (regex ? `-and $_.MainWindowTitle -match ${psString(title)}` : `-and $_.MainWindowTitle -like ${psString(`*${title}*`)}`) : ""} } | ForEach-Object { @{pid=$_.Id;title=$_.MainWindowTitle;hwnd=$_.MainWindowHandle.ToInt64()} } | ConvertTo-Json -Compress`); + if (!output) return []; const parsed = JSON.parse(output); return Array.isArray(parsed) ? parsed : [parsed]; +}); +register("Window_SetWindowShow", async (_ctx, node, args) => { + const title = text(args, ["窗口对象", "window_obj", "title"]); if (!title || title === "窗口对象") throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); + const show = !text(args, ["WindowType"], "show").toLowerCase().includes("hide"); + await powershell(`$p=Get-Process|Where-Object{$_.MainWindowTitle -like ${psString(`*${title}*`)}}|Select-Object -First 1;if(!$p){throw '找不到窗口'};Add-Type @'\nusing System;using System.Runtime.InteropServices;public static class W{[DllImport("user32.dll")]public static extern bool ShowWindow(IntPtr h,int n);}\n'@;[W]::ShowWindow($p.MainWindowHandle,${show ? 5 : 0})|Out-Null`); + return show; +}); +register("Window_WaitWindow", async (_ctx, node, args) => { + const title = text(args, ["match_title", "select_title", "window_obj", "target_window"]); if (!title) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); + const expected = text(args, ["switchType"], "0") !== "1"; const deadline = Date.now() + Math.max(0, Number(args.second ?? 5)) * 1000; + do { const exists = (await powershell(`$p=Get-Process|Where-Object{$_.MainWindowTitle -like ${psString(`*${title}*`)}}|Select-Object -First 1;[bool]$p`)).toLowerCase() === "true"; if (exists === expected) return true; await new Promise((resolve) => setTimeout(resolve, 200)); } while (Date.now() <= deadline); + return false; +}); + +const setClipboardImage: CapabilityHandler = async (_ctx, node, args) => { + const file = path.resolve(text(args, ["path"])); await fs.access(file).catch(() => { throw new RuntimeError("validation_error", `图片不存在:${file}`, { nodeId: node.id, componentId: node.componentId }); }); + await powershell(`Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$image=[System.Drawing.Image]::FromFile(${psString(file)});[System.Windows.Forms.Clipboard]::SetImage($image);$image.Dispose()`); return file; +}; +register(["Clipboard_SetImage", "Clipboard_SetImageV1"], setClipboardImage); +const saveClipboardImage: CapabilityHandler = async (_ctx, node, args) => { + const folder = path.resolve(text(args, ["path"])); const name = text(args, ["filename"], "clipboard.png"); const target = path.join(folder, path.extname(name) ? name : `${name}.png`); await fs.mkdir(folder, { recursive: true }); + await powershell(`Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;if(-not [System.Windows.Forms.Clipboard]::ContainsImage()){throw '剪贴板中没有图片'};$image=[System.Windows.Forms.Clipboard]::GetImage();$image.Save(${psString(target)},[System.Drawing.Imaging.ImageFormat]::Png);$image.Dispose()`).catch((error) => { throw new RuntimeError("desktop_command_failed", (error as Error).message, { nodeId: node.id, componentId: node.componentId }); }); return target; +}; +register(["Clipboard_SaveImage", "Clipboard_SaveImageV1"], saveClipboardImage); + +register("System_PlaySound", async (_ctx, node, args) => { + const audio = text(args, ["audio"]); if (audio) { const file = path.resolve(audio); await fs.access(file); await powershell(`$player=New-Object System.Media.SoundPlayer ${psString(file)};$player.PlaySync()`); return file; } + await powershell("[System.Media.SystemSounds]::Beep.Play()"); return true; +}); +register("CustomConfig_GetConfig", (_ctx, _node, args) => { + let configs: unknown = args.jsonlist; if (typeof configs === "string") { try { configs = JSON.parse(configs); } catch { configs = []; } } + const name = text(args, ["configName"]); if (Array.isArray(configs)) { const found = configs.find((item) => item && typeof item === "object" && [String((item as any).name ?? ""), String((item as any).configName ?? ""), String((item as any).key ?? "")].includes(name)); return found && typeof found === "object" && "value" in found ? (found as any).value : found ?? null; } + return configs && typeof configs === "object" ? (configs as Record)[name] : null; +}); +register("Basic_BusinessOrderCount", (ctx, _node, args) => { + const key = "__businessOrderCount"; const start = Number(args.start_count ?? 0); const step = Number(args.count ?? 1); const current = Number(ctx.vars[key] ?? start); const next = current + step; ctx.vars[key] = next; return next; +}); + +register("PDF_ConcatPdfV1", delegate("PDF_ConcatPdf")); +register(["PDF_SetPdfWaterMark", "PDF_SetPdfWaterMarkV1"], async (_ctx, node, args) => { + const input = path.resolve(text(args, ["fileInPath"])); const output = path.join(path.dirname(input), text(args, ["newPdfName"], "PDF加水印.pdf")); const content = text(args, ["markTextInfo"], "Watermark"); + if (!/^[\x20-\x7E]*$/.test(content)) throw new RuntimeError("validation_error", "当前文本水印仅支持 ASCII;中文水印需嵌入字体", { nodeId: node.id, componentId: node.componentId }); + const document = await PDFDocument.load(await fs.readFile(input), { ignoreEncryption: true }); const font = await document.embedFont(StandardFonts.Helvetica); + for (const page of document.getPages()) { const { width, height } = page.getSize(); page.drawText(content, { x: width * 0.2, y: height * 0.5, size: 32, font, color: rgb(0.65, 0.65, 0.65), opacity: 0.35, rotate: degrees(-30) }); } + await fs.writeFile(output, await document.save()); return output; +}); + +async function webhook(kind: "dingtalk" | "feishu" | "wecom", args: Record, nodeId: string, componentId: string) { + let url = text(args, ["webhook"]); if (!/^https?:\/\//i.test(url)) throw new RuntimeError("validation_error", "Webhook 地址无效", { nodeId, componentId }); + const secret = text(args, ["key"]); if (kind === "dingtalk" && secret) { const timestamp = Date.now(); const sign = encodeURIComponent(createHmac("sha256", secret).update(`${timestamp}\n${secret}`).digest("base64")); url += `${url.includes("?") ? "&" : "?"}timestamp=${timestamp}&sign=${sign}`; } + const custom = text(args, ["content_custom"]); const messageType = text(args, ["message_type"], "0"); const plain = text(args, ["content_text", "test_message"]); const markdown = text(args, ["content_markdown"]); + let body: unknown; + if (custom) { try { body = JSON.parse(custom); } catch { throw new RuntimeError("validation_error", "自定义消息 JSON 无效", { nodeId, componentId }); } } + else if (kind === "feishu") body = messageType === "1" ? { msg_type: "interactive", card: { elements: [{ tag: "markdown", content: markdown || plain }] } } : { msg_type: "text", content: { text: plain } }; + else if (kind === "dingtalk") body = messageType === "1" ? { msgtype: "markdown", markdown: { title: "RPA通知", text: markdown || plain } } : { msgtype: "text", text: { content: plain }, at: { atMobiles: text(args, ["mentioned_mobile_list"]).split(/[,,]/).filter(Boolean) } }; + else body = messageType === "1" ? { msgtype: "markdown", markdown: { content: markdown || plain } } : { msgtype: "text", text: { content: plain, mentioned_mobile_list: text(args, ["mentioned_mobile_list"]).split(/[,,]/).filter(Boolean) } }; + const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const responseText = await response.text(); if (!response.ok) throw new RuntimeError("http_error", `Webhook 返回 ${response.status}: ${responseText}`, { nodeId, componentId }); + try { return JSON.parse(responseText); } catch { return responseText; } +} +register("MessageNotification_DingTalk_Message", (_ctx, node, args) => webhook("dingtalk", args, node.id, node.componentId)); +register("MessageNotification_Feishu_Message", (_ctx, node, args) => webhook("feishu", args, node.id, node.componentId)); +register("MessageNotification_WXWork_Message", (_ctx, node, args) => webhook("wecom", args, node.id, node.componentId)); diff --git a/src/main/capabilities/excel-common.test.ts b/src/main/capabilities/excel-common.test.ts new file mode 100644 index 0000000..8a90917 --- /dev/null +++ b/src/main/capabilities/excel-common.test.ts @@ -0,0 +1,147 @@ +import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import ExcelJS from "exceljs"; +import { afterEach, describe, expect, it } from "vitest"; +import type { Catalog, CatalogComponent, FlowNode } from "../../shared/types"; +import type { RunContext } from "../engine/types"; +import { getHandler } from "../engine/registry"; +import "./index"; +import { EXCEL_COMMON_IDS } from "./excel-common"; + +const directories: string[] = []; +const context = {} as RunContext; +const component = {} as CatalogComponent; +const node = (componentId: string): FlowNode => ({ + id: `excel_common_${componentId}`, + componentId, + position: { x: 0, y: 0 }, + values: {}, + options: {} +}); + +async function temporaryWorkbook(): Promise { + const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-excel-common-")); + directories.push(directory); + const filePath = path.join(directory, "common.xlsx"); + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet("Data"); + sheet.addRows([ + ["name", "score", "group", "parts"], + ["Bob", 2, "B", "x,y"], + ["Alice", 3, "A", "m,n"], + ["Bob", 2, "B", "p,q"] + ]); + await workbook.xlsx.writeFile(filePath); + return filePath; +} + +async function run(componentId: string, args: Record): Promise { + const handler = getHandler(componentId); + expect(handler, `${componentId} should be registered`).toBeTruthy(); + return handler!(context, node(componentId), args, component); +} + +async function readWorkbook(filePath: string): Promise { + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.readFile(filePath); + return workbook; +} + +afterEach(async () => { + await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); +}); + +describe("common Excel capabilities", () => { + it("registers at least 25 real catalog IDs without conflicting with existing modules", async () => { + const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8")) as Catalog; + const catalogIds = new Set(catalog.components.map((item) => item.id)); + expect(EXCEL_COMMON_IDS.length).toBeGreaterThanOrEqual(25); + for (const id of EXCEL_COMMON_IDS) { + expect(catalogIds.has(id), `${id} should exist in catalog.json`).toBe(true); + expect(getHandler(id), `${id} should be registered`).toBeTruthy(); + } + + const sourceFiles = (await readdir("src/main/capabilities")) + .filter((file) => file.endsWith(".ts") && !file.endsWith(".test.ts") && file !== "excel-common.ts"); + const existingSource = (await Promise.all(sourceFiles.map((file) => readFile(path.join("src/main/capabilities", file), "utf8")))).join("\n"); + for (const id of EXCEL_COMMON_IDS) expect(existingSource.includes(`"${id}"`), `${id} must not already be registered`).toBe(false); + }); + + it("writes and reads rows, columns, and ranges in a real workbook", async () => { + const filePath = await temporaryWorkbook(); + await run("Excel_Cs_WriteRow", { workbook: filePath, sheet: "Data", row: 6, column: "A", contents: ["Carol", 9, "C"], save: 1 }); + await run("Excel_Cs_WriteColumn", { workbook: filePath, sheet: "Data", row: 2, column: "E", contents: [10, 20, 30], save: 1 }); + await run("Excel_Cs_WriteRangeV2", { workbook: filePath, sheet: "Data", row: 8, column: "A", contents: [["x", 1], ["y", 2]], save: 1 }); + + await expect(run("Excel_Cs_ReadRow", { workbook: filePath, sheet: "Data", rowIndex: 6 })).resolves.toEqual(["Carol", 9, "C", "", ""]); + await expect(run("Excel_GetColData", { objExcel: filePath, sheet: "Data", cellPos: "E2" })).resolves.toEqual([10, 20, 30, "", "", "", "", ""]); + await expect(run("Excel_Cs_ReadRange", { workbook: filePath, sheet: "Data", startRow: 8, startColumn: "A", lastRow: 9, lastColumn: "B" })) + .resolves.toEqual([["x", 1], ["y", 2]]); + await expect(run("Excel_Cs_ReadColumnCount", { workbook: filePath, sheet: "Data", row: 2 })).resolves.toBe(5); + }); + + it("finds, replaces, sorts, filters, clears filters, and removes duplicates", async () => { + const filePath = await temporaryWorkbook(); + await expect(run("Excel_Cs_Find", { workbook: filePath, sheet: "Data", what: "Alice", matchFirst: 1 })) + .resolves.toEqual(["A3"]); + await run("Excel_Cs_Search", { workbook: filePath, sheet: "Data", mode: "替换", input: "Alice", replace: "Alicia", wholeWord: 1, save: 1 }); + await run("Excel_Cs_Sort", { workbook: filePath, sheet: "Data", sortRange: "A1:D4", sortOrder: "2", header: 1, save: 1 }); + await run("Excel_Cs_Filter", { workbook: filePath, sheet: "Data", column: "C", criteria: "B", condition: "equal", startRow: 1, endRow: 4, onHeader: 1, save: 1 }); + + let workbook = await readWorkbook(filePath); + let sheet = workbook.getWorksheet("Data")!; + expect(sheet.getCell("A2").value).toBe("Bob"); + expect(sheet.getRow(3).hidden || sheet.getRow(4).hidden).toBe(true); + + await run("Excel_Cs_ClearExcelFilter", { workbook: filePath, sheet: "Data", save: 1 }); + await run("Excel_Cs_DeleteDuplicates", { workbook: filePath, sheet: "Data", columns: ["A", "B", "C"], header: 1, save: 1 }); + workbook = await readWorkbook(filePath); + sheet = workbook.getWorksheet("Data")!; + expect(sheet.getRows(1, sheet.rowCount)!.some((row) => row.hidden)).toBe(false); + expect(sheet.rowCount).toBe(3); + expect(sheet.getColumn("A").values).toContain("Alicia"); + }); + + it("copies, pastes, fills, splits, and deletes real ranges", async () => { + const filePath = await temporaryWorkbook(); + await run("Excel_CopyRange", { objExcel: filePath, sheet: "Data", cellPosRange: "A2:B3" }); + await run("Excel_Cs_PastRange", { workbook: filePath, sheet: "Data", column: "F", row: 2, save: 1 }); + await run("Excel_Cs_AutoFill", { workbook: filePath, sheet: "Data", sourceRange: "B2:B3", destination: "B2:B6", save: 1 }); + await run("Excel_Cs_ColumnSplitV2", { workbook: filePath, sheet: "Data", column: "D", destination: "J1", comma: 1, tab: 0, startRow: 2, endRow: 4, save: 1 }); + await run("Excel_Cs_DeleteRangeV3", { workbook: filePath, sheet: "Data", excelRng: "C2:C4", save: 1 }); + + const workbook = await readWorkbook(filePath); + const sheet = workbook.getWorksheet("Data")!; + expect(sheet.getCell("F2").value).toBe("Bob"); + expect(sheet.getCell("G3").value).toBe(3); + expect(sheet.getCell("B6").value).toBe(6); + expect(sheet.getCell("J2").value).toBe("x"); + expect(sheet.getCell("K2").value).toBe("y"); + expect(sheet.getCell("C2").value).toBeNull(); + }); + + it("manages sheets and persists active sheet state", async () => { + const filePath = await temporaryWorkbook(); + await run("Excel_CreateSheet", { objExcel: filePath, sheet: "Report", isSave: true }); + await expect(run("Excel_Cs_GetName", { workbook: filePath })).resolves.toEqual(["Data", "Report"]); + await expect(run("Excel_Cs_Active", { workbook: filePath, mode: "按名称", sheet: "Report", save: 1 })).resolves.toBe("Report"); + await run("Excel_DeleteSheet", { objExcel: filePath, sheet: "Data", isSave: true }); + await run("Excel_Cs_Close", { workbook: filePath, save: 1 }); + + const workbook = await readWorkbook(filePath); + expect(workbook.worksheets.map((sheet) => sheet.name)).toEqual(["Report"]); + }); + + it("sets and reads cell and range colors", async () => { + const filePath = await temporaryWorkbook(); + await run("Excel_Cs_SetCellBackground", { workbook: filePath, sheet: "Data", column: "A", row: 2, color: "#12AB34", save: 1 }); + await run("Excel_Cs_SetRangeFontV2", { workbook: filePath, sheet: "Data", range: "A2:B3", color: "#445566", save: 1 }); + await expect(run("Excel_Cs_ReadCellBackground", { workbook: filePath, sheet: "Data", column: "A", row: 2 })).resolves.toBe("FF12AB34"); + + const workbook = await readWorkbook(filePath); + const sheet = workbook.getWorksheet("Data")!; + expect((sheet.getCell("A2").fill as ExcelJS.FillPattern).fgColor?.argb).toBe("FF12AB34"); + expect(sheet.getCell("B3").font.color?.argb).toBe("FF445566"); + }); +}); diff --git a/src/main/capabilities/excel-common.ts b/src/main/capabilities/excel-common.ts new file mode 100644 index 0000000..1da3318 --- /dev/null +++ b/src/main/capabilities/excel-common.ts @@ -0,0 +1,618 @@ +import ExcelJS from "exceljs"; +import path from "node:path"; +import { register } from "../engine/registry"; +import { RuntimeError, type CapabilityHandler } from "../engine/types"; + +type WorkbookSource = { workbook: ExcelJS.Workbook; filePath: string }; +type CopiedRange = { values: unknown[][]; styles: Partial[][] }; + +const workbookCache = new Map(); +let copiedRange: CopiedRange | undefined; + +function value(args: Record, keys: string[], fallback: unknown = ""): unknown { + for (const key of keys) { + const candidate = args[key]; + if (candidate !== undefined && candidate !== null && candidate !== "") return candidate; + } + return fallback; +} + +function text(args: Record, keys: string[], fallback = ""): string { + return String(value(args, keys, fallback)); +} + +function truthy(candidate: unknown, fallback = false): boolean { + if (candidate === undefined || candidate === null || candidate === "") return fallback; + return [true, 1, "1", "true", "True", "TRUE", "yes", "是", "开启"].includes(candidate as never); +} + +function numberValue(candidate: unknown, fallback: number): number { + const result = Number(candidate); + return Number.isFinite(result) ? result : fallback; +} + +function requiredPath(candidate: unknown, nodeId: string, componentId: string): string { + const raw = typeof candidate === "object" && candidate !== null ? (candidate as { path?: unknown }).path : candidate; + const filePath = String(raw ?? "").trim(); + if (!filePath) { + throw new RuntimeError("validation_error", "Excel path is empty", { + nodeId, + componentId, + recommendation: "Configure an Excel workbook path" + }); + } + return path.resolve(filePath); +} + +async function loadWorkbook(args: Record, nodeId: string, componentId: string): Promise { + const filePath = requiredPath(value(args, ["workbook", "objExcel", "objExcelWorkBook", "file", "path"]), nodeId, componentId); + let workbook = workbookCache.get(filePath); + if (!workbook) { + workbook = new ExcelJS.Workbook(); + await workbook.xlsx.readFile(filePath); + workbookCache.set(filePath, workbook); + } + return { workbook, filePath }; +} + +function worksheetOf(workbook: ExcelJS.Workbook, args: Record): ExcelJS.Worksheet { + const name = text(args, ["sheet", "sheetName"]); + const sheet = name ? workbook.getWorksheet(name) : workbook.worksheets[0]; + if (!sheet) throw new Error(`Sheet not found: ${name || "first sheet"}`); + return sheet; +} + +async function saveWhenRequested(source: WorkbookSource, args: Record, fallback = false): Promise { + if (truthy(value(args, ["save", "isSave"]), fallback)) await source.workbook.xlsx.writeFile(source.filePath); +} + +function columnNumber(candidate: unknown): number { + const raw = String(candidate ?? "A").trim().toUpperCase(); + if (/^\d+$/.test(raw)) return Math.max(1, Number(raw)); + if (!/^[A-Z]+$/.test(raw)) throw new Error(`Invalid column: ${raw}`); + let result = 0; + for (const character of raw) result = result * 26 + character.charCodeAt(0) - 64; + return result; +} + +function columnLetter(index: number): string { + let current = Math.max(1, Math.trunc(index)); + let result = ""; + while (current > 0) { + current -= 1; + result = String.fromCharCode(65 + current % 26) + result; + current = Math.floor(current / 26); + } + return result; +} + +function cellValue(candidate: ExcelJS.CellValue): unknown { + if (candidate && typeof candidate === "object" && "result" in candidate) return candidate.result ?? ""; + if (candidate && typeof candidate === "object" && "text" in candidate) return candidate.text; + return candidate ?? ""; +} + +function listValue(candidate: unknown): unknown[] { + if (Array.isArray(candidate)) return candidate; + const raw = String(candidate ?? "").trim(); + if (!raw) return []; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : [parsed]; + } catch { + try { + const parsed = JSON.parse(raw.replace(/'/g, '"')); + return Array.isArray(parsed) ? parsed : [parsed]; + } catch { + return raw.split(",").map((item) => item.trim()); + } + } +} + +function tableValue(candidate: unknown): unknown[][] { + return listValue(candidate).map((item) => Array.isArray(item) ? item : [item]); +} + +function rangeBounds(sheet: ExcelJS.Worksheet, range: string): { top: number; left: number; bottom: number; right: number } { + const normalized = range.trim() || `A1:${columnLetter(Math.max(1, sheet.columnCount))}${Math.max(1, sheet.rowCount)}`; + const [start, end = start] = normalized.split(":").map((part) => part.trim()); + const first = sheet.getCell(start); + const last = sheet.getCell(end); + return { + top: Math.min(Number(first.row), Number(last.row)), + left: Math.min(Number(first.col), Number(last.col)), + bottom: Math.max(Number(first.row), Number(last.row)), + right: Math.max(Number(first.col), Number(last.col)) + }; +} + +function explicitRange(args: Record, sheet: ExcelJS.Worksheet): string { + const direct = text(args, ["excelRng", "range", "cellPosRange", "cellPos", "sortRange"]); + if (direct) return direct; + const top = numberValue(value(args, ["startRow", "row"]), 1); + const left = columnNumber(value(args, ["startColumn", "column"])); + const bottom = numberValue(value(args, ["lastRow", "endRow"]), top); + const right = columnNumber(value(args, ["lastColumn", "endColumn"], left)); + return `${columnLetter(left)}${top}:${columnLetter(right)}${bottom || Math.max(1, sheet.rowCount)}`; +} + +function cellsInRange(sheet: ExcelJS.Worksheet, range: string): ExcelJS.Cell[] { + const bounds = rangeBounds(sheet, range); + const cells: ExcelJS.Cell[] = []; + for (let row = bounds.top; row <= bounds.bottom; row += 1) { + for (let column = bounds.left; column <= bounds.right; column += 1) cells.push(sheet.getCell(row, column)); + } + return cells; +} + +function cloneStyle(style: Partial): Partial { + return JSON.parse(JSON.stringify(style)) as Partial; +} + +function colorArgb(candidate: unknown): string { + if (Array.isArray(candidate)) { + const [red = 0, green = 0, blue = 0] = candidate.map((item) => Math.max(0, Math.min(255, Number(item)))); + return `FF${red.toString(16).padStart(2, "0")}${green.toString(16).padStart(2, "0")}${blue.toString(16).padStart(2, "0")}`.toUpperCase(); + } + const raw = String(candidate ?? "#000000").trim(); + if (raw.startsWith("[")) { + try { return colorArgb(JSON.parse(raw)); } catch { return "FF000000"; } + } + const hex = raw.replace(/^#/, "").toUpperCase(); + if (/^[0-9A-F]{8}$/.test(hex)) return hex; + return `FF${/^[0-9A-F]{6}$/.test(hex) ? hex : "000000"}`; +} + +function comparison(left: unknown, right: unknown): number { + if (typeof left === "number" && typeof right === "number") return left - right; + return String(left ?? "").localeCompare(String(right ?? ""), "zh-CN", { numeric: true, sensitivity: "base" }); +} + +function registerMany(ids: string[], handler: CapabilityHandler): void { + register(ids, handler); +} + +const writeRow: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const start = text(args, ["cellPos"]); + const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row", "rowIndex"]), 1); + const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"])); + listValue(value(args, ["contents", "value"])).forEach((item, index) => { sheet.getCell(row, column + index).value = item as ExcelJS.CellValue; }); + await saveWhenRequested(source, args); + return `${columnLetter(column)}${row}`; +}; + +const writeColumn: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const start = text(args, ["cellPos"]); + const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row"]), 1); + const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"])); + listValue(value(args, ["contents", "value"])).forEach((item, index) => { sheet.getCell(row + index, column).value = item as ExcelJS.CellValue; }); + await saveWhenRequested(source, args); + return `${columnLetter(column)}${row}`; +}; + +const writeRange: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const row = numberValue(value(args, ["row", "startRow"]), 1); + const column = columnNumber(value(args, ["column", "startColumn"])); + tableValue(value(args, ["contents", "value"])).forEach((items, rowOffset) => { + items.forEach((item, columnOffset) => { sheet.getCell(row + rowOffset, column + columnOffset).value = item as ExcelJS.CellValue; }); + }); + await saveWhenRequested(source, args); + return `${columnLetter(column)}${row}`; +}; + +const readRow: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const start = text(args, ["cellPos"]); + const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row", "rowIndex"]), 1); + const firstColumn = start ? Number(sheet.getCell(start).col) : 1; + const result: unknown[] = []; + for (let column = firstColumn; column <= Math.max(firstColumn, sheet.columnCount); column += 1) result.push(cellValue(sheet.getCell(row, column).value)); + return result; +}; + +const readColumn: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const start = text(args, ["cellPos"]); + const row = start ? Number(sheet.getCell(start).row) : 1; + const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"])); + const result: unknown[] = []; + for (let current = row; current <= Math.max(row, sheet.rowCount); current += 1) result.push(cellValue(sheet.getCell(current, column).value)); + return result; +}; + +const readRange: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const bounds = rangeBounds(sheet, explicitRange(args, sheet)); + const result: unknown[][] = []; + for (let row = bounds.top; row <= bounds.bottom; row += 1) { + const values: unknown[] = []; + for (let column = bounds.left; column <= bounds.right; column += 1) values.push(cellValue(sheet.getCell(row, column).value)); + result.push(values); + } + return result; +}; + +const readColumnCount: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const row = numberValue(value(args, ["row"]), 1); + let last = 0; + for (let column = 1; column <= sheet.columnCount; column += 1) { + if (cellValue(sheet.getCell(row, column).value) !== "") last = column; + } + return last; +}; + +const findOrReplace: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const needle = text(args, ["what", "input"]); + const replacement = text(args, ["replacement", "replace"]); + const mode = text(args, ["mode"], "0"); + const replaceMode = mode === "1" || mode.includes("替换") || replacement !== ""; + const exact = truthy(value(args, ["wholeWord"])) || text(args, ["lookAt"]) === "1"; + const matchCase = truthy(value(args, ["matchCase"])); + const range = text(args, ["range"]); + const cells = cellsInRange(sheet, range || `A1:${columnLetter(Math.max(1, sheet.columnCount))}${Math.max(1, sheet.rowCount)}`); + const matches: string[] = []; + for (const cell of cells) { + const original = String(cellValue(cell.value)); + const haystack = matchCase ? original : original.toLowerCase(); + const target = matchCase ? needle : needle.toLowerCase(); + const matched = exact ? haystack === target : haystack.includes(target); + if (!matched) continue; + matches.push(cell.address); + if (replaceMode) { + const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + cell.value = exact ? replacement : original.replace(new RegExp(escaped, matchCase ? "g" : "gi"), replacement); + } + if (truthy(value(args, ["matchFirst"]))) break; + } + await saveWhenRequested(source, args); + return matches; +}; + +const sortRange: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const range = text(args, ["sortRange", "range"]) || `A1:${columnLetter(Math.max(1, sheet.columnCount))}${Math.max(1, sheet.rowCount)}`; + const bounds = rangeBounds(sheet, range); + const hasHeader = truthy(value(args, ["header"])); + const dataStart = bounds.top + (hasHeader ? 1 : 0); + const rows: unknown[][] = []; + for (let row = dataStart; row <= bounds.bottom; row += 1) { + const values: unknown[] = []; + for (let column = bounds.left; column <= bounds.right; column += 1) values.push(cellValue(sheet.getCell(row, column).value)); + rows.push(values); + } + let sortOffset = 0; + const rule = value(args, ["rule"]); + if (rule) { + try { + const parsed = typeof rule === "string" ? JSON.parse(rule) as Record : rule as Record; + sortOffset = Math.max(0, columnNumber(value(parsed, ["column", "key"], bounds.left)) - bounds.left); + } catch { /* use first column */ } + } + const descending = ["2", "-1", "desc", "descending", "降序"].includes(text(args, ["sortOrder"]).toLowerCase()); + rows.sort((left, right) => comparison(left[sortOffset], right[sortOffset]) * (descending ? -1 : 1)); + rows.forEach((values, rowOffset) => values.forEach((item, columnOffset) => { + sheet.getCell(dataStart + rowOffset, bounds.left + columnOffset).value = item as ExcelJS.CellValue; + })); + await saveWhenRequested(source, args); + return rows; +}; + +function criterionMatches(candidate: unknown, criterion: unknown, condition: string): boolean { + const left = String(candidate ?? ""); + const right = String(criterion ?? ""); + if (["contains", "包含", "1"].includes(condition)) return left.includes(right); + if (["notContains", "不包含", "2"].includes(condition)) return !left.includes(right); + if (["greater", ">", "3"].includes(condition)) return Number(candidate) > Number(criterion); + if (["less", "<", "4"].includes(condition)) return Number(candidate) < Number(criterion); + if (["notEqual", "!=", "5"].includes(condition)) return left !== right; + return left === right; +} + +const filterRows: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + let column = columnNumber(value(args, ["column"], "A")); + let criterion = value(args, ["criteria"], ""); + let condition = text(args, ["condition"], "equal"); + const rule = value(args, ["rule"]); + if (rule) { + try { + const parsed = typeof rule === "string" ? JSON.parse(rule) as Record : rule as Record; + column = columnNumber(value(parsed, ["column"], column)); + criterion = value(parsed, ["criteria", "value"], criterion); + condition = text(parsed, ["condition", "operator"], condition); + } catch { /* use direct fields */ } + } + const startRow = numberValue(value(args, ["startRow"]), 1); + const endRowRaw = numberValue(value(args, ["endRow"]), sheet.rowCount); + const endRow = endRowRaw < 0 ? sheet.rowCount : endRowRaw; + const headerRow = truthy(value(args, ["onHeader"])) ? startRow : 0; + for (let row = startRow; row <= endRow; row += 1) { + if (row === headerRow) continue; + sheet.getRow(row).hidden = !criterionMatches(cellValue(sheet.getCell(row, column).value), criterion, condition); + } + sheet.autoFilter = { from: { row: startRow, column: 1 }, to: { row: endRow, column: Math.max(1, sheet.columnCount) } }; + await saveWhenRequested(source, args); + return { column: columnLetter(column), criterion, condition }; +}; + +const clearFilter: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + for (let row = 1; row <= sheet.rowCount; row += 1) sheet.getRow(row).hidden = false; + sheet.autoFilter = undefined; + await saveWhenRequested(source, args); + return true; +}; + +const deleteDuplicates: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const columns = listValue(value(args, ["columns"], "A")).map(columnNumber); + const hasHeader = truthy(value(args, ["header"])); + const seen = new Set(); + const duplicates: number[] = []; + for (let row = hasHeader ? 2 : 1; row <= sheet.rowCount; row += 1) { + const key = JSON.stringify(columns.map((column) => cellValue(sheet.getCell(row, column).value))); + if (seen.has(key)) duplicates.push(row); + else seen.add(key); + } + duplicates.reverse().forEach((row) => sheet.spliceRows(row, 1)); + await saveWhenRequested(source, args); + return duplicates.length; +}; + +function copyCells(sheet: ExcelJS.Worksheet, range: string): CopiedRange { + const bounds = rangeBounds(sheet, range); + const values: unknown[][] = []; + const styles: Partial[][] = []; + for (let row = bounds.top; row <= bounds.bottom; row += 1) { + const valueRow: unknown[] = []; + const styleRow: Partial[] = []; + for (let column = bounds.left; column <= bounds.right; column += 1) { + const cell = sheet.getCell(row, column); + valueRow.push(cell.value); + styleRow.push(cloneStyle(cell.style)); + } + values.push(valueRow); + styles.push(styleRow); + } + return { values, styles }; +} + +const copyRange: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const range = explicitRange(args, sheet); + copiedRange = copyCells(sheet, range); + const destinationColumn = value(args, ["startColumn2"]); + const destinationRow = value(args, ["startRow2"]); + if (destinationColumn !== "" && destinationRow !== "") { + const startColumn = columnNumber(destinationColumn); + const startRow = numberValue(destinationRow, 1); + copiedRange.values.forEach((items, rowOffset) => items.forEach((item, columnOffset) => { + const cell = sheet.getCell(startRow + rowOffset, startColumn + columnOffset); + cell.value = item as ExcelJS.CellValue; + cell.style = cloneStyle(copiedRange!.styles[rowOffset][columnOffset]); + })); + await saveWhenRequested(source, args); + } + return copiedRange.values; +}; + +const pasteRange: CapabilityHandler = async (_ctx, node, args) => { + if (!copiedRange) throw new RuntimeError("validation_error", "No copied Excel range is available", { nodeId: node.id, componentId: node.componentId }); + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const start = text(args, ["Start"]); + const row = start ? Number(sheet.getCell(start).row) : numberValue(value(args, ["row"]), 1); + const column = start ? Number(sheet.getCell(start).col) : columnNumber(value(args, ["column"])); + const transpose = truthy(value(args, ["transpose"])); + const values = transpose ? copiedRange.values[0].map((_, index) => copiedRange!.values.map((items) => items[index])) : copiedRange.values; + values.forEach((items, rowOffset) => items.forEach((item, columnOffset) => { + const cell = sheet.getCell(row + rowOffset, column + columnOffset); + cell.value = item as ExcelJS.CellValue; + if (!transpose) cell.style = cloneStyle(copiedRange!.styles[rowOffset][columnOffset]); + })); + await saveWhenRequested(source, args); + return `${columnLetter(column)}${row}`; +}; + +const createSheet: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const name = text(args, ["sheet", "sheetName", "name"], `Sheet${source.workbook.worksheets.length + 1}`); + if (source.workbook.getWorksheet(name)) throw new Error(`Sheet already exists: ${name}`); + source.workbook.addWorksheet(name); + await saveWhenRequested(source, args); + return name; +}; + +const deleteSheet: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + if (source.workbook.worksheets.length <= 1) throw new Error("Cannot delete the only worksheet"); + source.workbook.removeWorksheet(sheet.id); + await saveWhenRequested(source, args); + return sheet.name; +}; + +const activateSheet: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const mode = text(args, ["mode"]); + const index = mode.includes("序号") ? numberValue(value(args, ["index"]), 1) : 0; + const sheet = index ? source.workbook.worksheets[index - 1] : worksheetOf(source.workbook, args); + if (!sheet) throw new Error(`Sheet not found: ${index}`); + source.workbook.views = [{ + activeTab: source.workbook.worksheets.indexOf(sheet), + firstSheet: 0, + visibility: "visible", + x: 0, + y: 0, + width: 12000, + height: 8000 + }]; + await saveWhenRequested(source, args); + return sheet.name; +}; + +const getSheetNames: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + return source.workbook.worksheets.map((sheet) => sheet.name); +}; + +function setFontColor(cell: ExcelJS.Cell, argb: string): void { + cell.font = { ...(cell.font || {}), color: { argb } }; +} + +function setBackground(cell: ExcelJS.Cell, argb: string): void { + cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb } }; +} + +const setColor = (kind: "font" | "background", rangeMode: boolean): CapabilityHandler => async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const range = rangeMode + ? explicitRange(args, sheet) + : text(args, ["cellPos"]) || `${text(args, ["column"], "A")}${numberValue(value(args, ["row"]), 1)}`; + const argb = colorArgb(value(args, ["color", "cellColor"])); + for (const cell of cellsInRange(sheet, range)) { + if (kind === "font") setFontColor(cell, argb); + else setBackground(cell, argb); + } + await saveWhenRequested(source, args); + return argb; +}; + +const readBackground: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const cell = sheet.getCell(numberValue(value(args, ["row"]), 1), columnNumber(value(args, ["column"]))); + const fill = cell.fill as ExcelJS.FillPattern | undefined; + return fill?.type === "pattern" ? fill.fgColor?.argb ?? "" : ""; +}; + +const autoFill: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const sourceRange = text(args, ["sourceRange", "excelRng"]); + const destination = text(args, ["destination"]); + const sourceBounds = rangeBounds(sheet, sourceRange); + const destinationBounds = rangeBounds(sheet, destination); + const sourceValues = copyCells(sheet, sourceRange).values; + const vertical = sourceBounds.left === sourceBounds.right; + for (let row = destinationBounds.top; row <= destinationBounds.bottom; row += 1) { + for (let column = destinationBounds.left; column <= destinationBounds.right; column += 1) { + const sourceRow = (row - destinationBounds.top) % sourceValues.length; + const sourceColumn = (column - destinationBounds.left) % sourceValues[0].length; + let next = sourceValues[sourceRow][sourceColumn]; + const sequence = vertical ? sourceValues.map((items) => items[0]) : sourceValues[0]; + const offset = vertical ? row - sourceBounds.top : column - sourceBounds.left; + if (sequence.length >= 2 && sequence.every((item) => typeof item === "number")) { + next = Number(sequence[0]) + (Number(sequence[1]) - Number(sequence[0])) * offset; + } + sheet.getCell(row, column).value = next as ExcelJS.CellValue; + } + } + await saveWhenRequested(source, args); + return destination; +}; + +const splitColumn: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const sourceColumn = columnNumber(value(args, ["column"])); + const destination = text(args, ["destination"]); + const destinationColumn = destination ? Number(sheet.getCell(destination).col) : sourceColumn; + const startRow = numberValue(value(args, ["startRow"]), 1); + const endRow = numberValue(value(args, ["endRow"]), sheet.rowCount); + const delimiters: string[] = []; + if (truthy(value(args, ["tab"]), true)) delimiters.push("\\t"); + if (truthy(value(args, ["semicolon"]))) delimiters.push(";"); + if (truthy(value(args, ["comma"]))) delimiters.push(","); + if (truthy(value(args, ["space"]))) delimiters.push("\\s+"); + if (truthy(value(args, ["other"]))) delimiters.push(text(args, ["otherChar"]).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + const separator = new RegExp(delimiters.filter(Boolean).join("|") || "\\t"); + for (let row = startRow; row <= endRow; row += 1) { + String(cellValue(sheet.getCell(row, sourceColumn).value)).split(separator).forEach((item, offset) => { + sheet.getCell(row, destinationColumn + offset).value = item; + }); + } + await saveWhenRequested(source, args); + return `${columnLetter(destinationColumn)}${startRow}`; +}; + +const closeWorkbook: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + if (truthy(value(args, ["save", "isSave"]), true)) await source.workbook.xlsx.writeFile(source.filePath); + workbookCache.delete(source.filePath); + return source.filePath; +}; + +const deleteRange: CapabilityHandler = async (_ctx, node, args) => { + const source = await loadWorkbook(args, node.id, node.componentId); + const sheet = worksheetOf(source.workbook, args); + const range = explicitRange(args, sheet); + for (const cell of cellsInRange(sheet, range)) cell.value = null; + await saveWhenRequested(source, args); + return range; +}; + +export const EXCEL_COMMON_IDS = [ + "Excel_WriteColumn", "Excel_WriteColumnNew", "Excel_Cs_WriteColumn", + "Excel_WriteRow", "Excel_WriteRowNew", "Excel_Cs_WriteRow", "Excel_Cs_WriteRowV2", + "Excel_Cs_WriteRangeV2", "Excel_GetRowData", "Excel_GetRowDataNew", "Excel_Cs_ReadRow", + "Excel_GetColData", "Excel_Cs_ReadColumnCount", "Excel_Cs_ReadRange", + "Excel_Cs_Find", "Excel_Cs_FindV2", "Excel_Cs_Search", + "Excel_Cs_Sort", "Excel_Cs_SortV1", "Excel_Cs_Filter", "Excel_Cs_ExcelFilter", + "Excel_Cs_ClearExcelFilter", "Excel_Cs_EnableExcelFilter", "Excel_Cs_DeleteDuplicates", + "Excel_CopyRange", "Excel_Cs_CopyRange", "Excel_Cs_CopyRangeV2", "Excel_Cs_CopyRangeV3", + "Excel_PasteRange", "Excel_Cs_PastRange", "Excel_Cs_PastRangeV2", + "Excel_CreateSheet", "Excel_DeleteSheet", "Excel_Cs_Delete", "Excel_ActiveSheet", "Excel_Cs_Active", "Excel_Cs_GetName", + "Excel_SetCellFontColor", "Excel_Cs_SetCellFont", "Excel_SetRangeFontColor", "Excel_Cs_SetRangeFont", "Excel_Cs_SetRangeFontV2", + "Excel_SetCellColor", "Excel_Cs_SetCellBackground", "Excel_SetRangeColor", "Excel_Cs_SetRangeBackground", "Excel_Cs_SetRangeBackgroundV2", + "Excel_Cs_ReadCellBackground", "Excel_Cs_AutoFill", "Excel_Cs_AutoFillV2", + "Excel_Cs_ColumnSplit", "Excel_Cs_ColumnSplitV2", "Excel_Close", "Excel_Cs_Close", + "Excel_DeleteRange", "Excel_Cs_DeleteRange", "Excel_Cs_DeleteRangeV2", "Excel_Cs_DeleteRangeV3" +] as const; + +registerMany(["Excel_WriteRow", "Excel_WriteRowNew", "Excel_Cs_WriteRow", "Excel_Cs_WriteRowV2"], writeRow); +registerMany(["Excel_WriteColumn", "Excel_WriteColumnNew", "Excel_Cs_WriteColumn"], writeColumn); +register("Excel_Cs_WriteRangeV2", writeRange); +registerMany(["Excel_GetRowData", "Excel_GetRowDataNew", "Excel_Cs_ReadRow"], readRow); +register("Excel_GetColData", readColumn); +register("Excel_Cs_ReadColumnCount", readColumnCount); +register("Excel_Cs_ReadRange", readRange); +registerMany(["Excel_Cs_Find", "Excel_Cs_FindV2", "Excel_Cs_Search"], findOrReplace); +registerMany(["Excel_Cs_Sort", "Excel_Cs_SortV1"], sortRange); +registerMany(["Excel_Cs_Filter", "Excel_Cs_ExcelFilter"], filterRows); +registerMany(["Excel_Cs_ClearExcelFilter", "Excel_Cs_EnableExcelFilter"], clearFilter); +register("Excel_Cs_DeleteDuplicates", deleteDuplicates); +registerMany(["Excel_CopyRange", "Excel_Cs_CopyRange", "Excel_Cs_CopyRangeV2", "Excel_Cs_CopyRangeV3"], copyRange); +registerMany(["Excel_PasteRange", "Excel_Cs_PastRange", "Excel_Cs_PastRangeV2"], pasteRange); +register("Excel_CreateSheet", createSheet); +registerMany(["Excel_DeleteSheet", "Excel_Cs_Delete"], deleteSheet); +registerMany(["Excel_ActiveSheet", "Excel_Cs_Active"], activateSheet); +register("Excel_Cs_GetName", getSheetNames); +registerMany(["Excel_SetCellFontColor", "Excel_Cs_SetCellFont"], setColor("font", false)); +registerMany(["Excel_SetRangeFontColor", "Excel_Cs_SetRangeFont", "Excel_Cs_SetRangeFontV2"], setColor("font", true)); +registerMany(["Excel_SetCellColor", "Excel_Cs_SetCellBackground"], setColor("background", false)); +registerMany(["Excel_SetRangeColor", "Excel_Cs_SetRangeBackground", "Excel_Cs_SetRangeBackgroundV2"], setColor("background", true)); +register("Excel_Cs_ReadCellBackground", readBackground); +registerMany(["Excel_Cs_AutoFill", "Excel_Cs_AutoFillV2"], autoFill); +registerMany(["Excel_Cs_ColumnSplit", "Excel_Cs_ColumnSplitV2"], splitColumn); +registerMany(["Excel_Close", "Excel_Cs_Close"], closeWorkbook); +registerMany(["Excel_DeleteRange", "Excel_Cs_DeleteRange", "Excel_Cs_DeleteRangeV2", "Excel_Cs_DeleteRangeV3"], deleteRange); diff --git a/src/main/capabilities/index.ts b/src/main/capabilities/index.ts index ddd15e1..89e620a 100644 --- a/src/main/capabilities/index.ts +++ b/src/main/capabilities/index.ts @@ -6,6 +6,7 @@ import "./browser"; import "./browser-extended"; import "./browser-dom"; import "./browser-common"; +import "./browser-control"; import "./office"; import "./desktop"; import "./pdf"; @@ -17,9 +18,11 @@ import "./extended"; import "./system-extended"; import "./office-file-extended"; import "./office-aliases"; +import "./excel-common"; import "./window-extended"; import "./data-extended"; import "./utility-extended"; import "./data-table-common"; +import "./common-final"; export { capabilityCount, capabilityIds, getHandler, hasCapability, register } from "../engine/registry";