From 560134171f70772f32b37041a80d641a264d2785 Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Wed, 28 Oct 2026 17:40:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E7=BD=91=E9=A1=B5?= =?UTF-8?q?=E6=89=B9=E4=BA=8C=E2=80=94=E2=80=94=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0/=E5=A4=9A=E6=A0=87=E7=AD=BE/=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E7=8A=B6=E6=80=81/=E5=81=9C=E6=AD=A2=E5=8A=A0=E8=BD=BD/?= =?UTF-8?q?=E7=9B=B8=E4=BC=BC=E5=85=83=E7=B4=A0=E6=95=B0=E7=BB=84/?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E8=87=AA=E5=8A=A8=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/browser/session.ts | 52 ++++++++++++++++++++ src/main/capabilities/browser.ts | 2 +- src/main/capabilities/index.ts | 1 + src/main/capabilities/web-batch2.ts | 74 +++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/main/capabilities/web-batch2.ts diff --git a/src/main/browser/session.ts b/src/main/browser/session.ts index c6f410a..4fbb6ee 100644 --- a/src/main/browser/session.ts +++ b/src/main/browser/session.ts @@ -768,6 +768,58 @@ export class BrowserSession implements BrowserLike { return result.items; } + maximize(): void { + if (!this.win.isDestroyed()) this.win.maximize(); + } + + minimize(): void { + if (!this.win.isDestroyed()) this.win.minimize(); + } + + stopLoading(): void { + if (!this.win.isDestroyed()) this.win.webContents.stop(); + } + + async uploadFileToInput(selector: string, filePath: string): Promise { + const doc = await this.cdp<{ root?: { nodeId?: number } }>("DOM.getDocument"); + const rootNodeId = doc.root?.nodeId; + if (!rootNodeId) throw new RuntimeError("browser_error", "无法定位页面 DOM 根节点"); + const css = selector.replace(/^css:/, "").trim(); + const found = await this.cdp<{ nodeId?: number }>("DOM.querySelector", { nodeId: rootNodeId, selector: css }); + if (!found.nodeId) { + throw new RuntimeError("element_not_found", "未找到文件输入元素:" + css, { recommendation: "确认 input[type=file] 存在且已渲染" }); + } + await this.cdp("DOM.setFileInputFiles", { files: [filePath], nodeId: found.nodeId }); + return found.nodeId; + } + + startAutoCloseDialogs(): boolean { + if (!this.win.isDestroyed() && !this.win.webContents.debugger.isAttached()) this.win.webContents.debugger.attach("1.3"); + this.cdp("Page.enable").catch(() => {}); + const flags = globalThis as { __rpaAutoCloseDialogs?: boolean; __rpaAutoCloseDialogListener?: unknown }; + if (flags.__rpaAutoCloseDialogs) return false; + flags.__rpaAutoCloseDialogs = true; + const listener = (_event: unknown, method: string) => { + if (method === "Page.javascriptDialogOpening") { + this.cdp("Page.handleJavaScriptDialog", { accept: true }).catch(() => {}); + } + }; + flags.__rpaAutoCloseDialogListener = listener; + this.win.webContents.debugger.on("message", listener); + return true; + } + + stopAutoCloseDialogs(): boolean { + const flags = globalThis as { __rpaAutoCloseDialogs?: boolean; __rpaAutoCloseDialogListener?: unknown }; + if (!flags.__rpaAutoCloseDialogs) return false; + if (flags.__rpaAutoCloseDialogListener) { + this.win.webContents.debugger.removeListener("message", flags.__rpaAutoCloseDialogListener as never); + } + flags.__rpaAutoCloseDialogs = false; + flags.__rpaAutoCloseDialogListener = undefined; + return true; + } + async currentUrl(): Promise { return this.win.webContents.getURL(); } diff --git a/src/main/capabilities/browser.ts b/src/main/capabilities/browser.ts index 57fd1b3..2060917 100644 --- a/src/main/capabilities/browser.ts +++ b/src/main/capabilities/browser.ts @@ -18,7 +18,7 @@ function selectorOf(args: Record): unknown { return selector; } -register(["WebBrowser_CreateV3", "WebBrowser_Create", "WebBrowser_CreateBackground"], async (ctx, node, args) => { +register(["WebBrowser_CreateV3", "WebBrowser_CreateV2", "WebBrowser_Create", "WebBrowser_CreateBackground"], async (ctx, node, args) => { const browser = await session(ctx); const url = String(args.url ?? ""); if (url) { diff --git a/src/main/capabilities/index.ts b/src/main/capabilities/index.ts index 014cca8..e1b6611 100644 --- a/src/main/capabilities/index.ts +++ b/src/main/capabilities/index.ts @@ -10,6 +10,7 @@ import "./browser-control"; import "./office"; import "./desktop"; import "./desktop-uia"; +import "./web-batch2"; import "./pdf"; import "./word"; import "./word-extended"; diff --git a/src/main/capabilities/web-batch2.ts b/src/main/capabilities/web-batch2.ts new file mode 100644 index 0000000..db61113 --- /dev/null +++ b/src/main/capabilities/web-batch2.ts @@ -0,0 +1,74 @@ +import { browserSession } from "../browser/session"; +import { register } from "../engine/registry"; +import { RuntimeError } from "../engine/types"; + +function cssValue(selector: unknown): string { + const raw = typeof selector === "object" && selector !== null + ? String((selector as { value?: string }).value ?? "") + : String(selector ?? ""); + const text = raw.trim(); + return text.startsWith("css:") ? text.slice(4).trim() : text; +} + +function requireUploadPath(args: Record): string { + const filePath = String(args.file_path ?? args.filePath ?? args.path ?? "").trim(); + const folder = String(args.folder_path ?? args.folder ?? "").trim(); + const target = filePath || folder; + if (!target) { + throw new RuntimeError("validation_error", "上传文件路径为空", { recommendation: "填写要上传的本地文件完整路径" }); + } + return target; +} + +register(["Element_FileUploader", "WebBrowser_Upload", "WebBrowser_UploadDialog"], async (ctx, node, args) => { + const browser = await browserSession(); + const selector = cssValue(args.uiSelector ?? "css:input[type=file]") || "input[type=file]"; + const filePath = requireUploadPath(args); + const nodeId = await browser.uploadFileToInput(selector, filePath); + ctx.log("已设置上传文件:" + filePath, "info", node.id); + return { uploaded: true, selector, filePath, nodeId }; +}); + +register(["WebBrowser_GetTAB", "WebBrowser_GetTAB_V1", "WebBrowser_GetTAB_V2"], async (ctx, node) => { + const browser = await browserSession(); + const url = await browser.currentUrl(); + ctx.log("已获取标签页列表(单窗口模式)", "info", node.id); + return [{ index: 0, url, active: true }]; +}); + +register("WebBrowser_Switch_V1", async (ctx, node, args) => { + const browser = await browserSession(); + const url = await browser.currentUrl(); + ctx.log("运行浏览器为单窗口模式,当前即唯一标签页", "info", node.id); + return { switched: true, index: 0, url, active: String(args.is_active ?? "1") === "1" }; +}); + +register("WebBrowser_Maximize", async (ctx, node) => { + browser.maximize(); + return true; +}); + +register("WebBrowser_Minimize", async (ctx, node) => { + browser.minimize(); + return true; +}); + +register("WebBrowser_Stop", async (ctx, node) => { + browser.stopLoading(); + return true; +}); + +register(["Element_ElementAarray", "Element_ElementCaptureSimilar"], async (ctx, node, args) => { + const browser = await browserSession(); + const selector = args.uiSelector ?? args.selector ?? args.target; + if (!selector) { + throw new RuntimeError("validation_error", "元素选择器为空", { nodeId: node.id, componentId: node.componentId, recommendation: "请拾取或填写相似元素" }); + } + const references = await browser.getSimilarElements(selector, { + start: Number.isFinite(Number(args.start_index)) ? Math.trunc(Number(args.start_index)) : 1, + end: Number.isFinite(Number(args.end_index)) ? Math.trunc(Number(args.end_index)) : -1, + reverse: String(args.reverse ?? "") === "1" || String(args.reverse ?? "") === "true" + }); + ctx.log("已获取 " + references.length + " 个相似元素(数组)", "info", node.id); + return references; +});