import { register } from "@rpa/core"; import { RuntimeError } from "@rpa/core"; import { browserSession, externalBrowserSession } from "../browser/session"; import type { BrowserLike, RunContext } from "@rpa/core"; async function session(ctx: RunContext): Promise { if (!ctx.browser) ctx.browser = await browserSession(); return ctx.browser; } function isTruthyFlag(value: unknown): boolean { const text = String(value ?? "").trim().toLowerCase(); return text === "1" || text === "true" || text === "yes"; } /** * 「打开网页」浏览器选择: * - type 填 chrome / edge → 拉起本机对应浏览器(带独立 profile + 调试端口)并接管 * - isRemote 填 1/true → 直连 ipAddress:port 上已监听调试端口的实例(不拉起进程) * - type 缺省或其他值 → 内置运行浏览器(现状行为,兜底) */ async function resolveSession(ctx: RunContext, args: Record): Promise<{ browser: BrowserLike; label: string }> { if (ctx.browser) return { browser: ctx.browser, label: "" }; const type = String(args.type ?? "").trim().toLowerCase(); const isRemote = isTruthyFlag(args.isRemote); if (isRemote) { const host = String(args.ipAddress ?? "127.0.0.1"); const port = Number(args.port ?? 9222); const browser = await externalBrowserSession({ mode: "remote", host, port }); ctx.browser = browser; return { browser, label: browser.label }; } if (type === "chrome" || type === "edge") { const extraArgs = String(args.arguments ?? "") .split(/\s+/) .map((item) => item.trim()) .filter(Boolean); const browser = await externalBrowserSession({ mode: "launch", browserType: type === "edge" ? "edge" : "chrome", port: Number(args.port ?? 9222) || 9222, extraArgs }); ctx.browser = browser; return { browser, label: browser.label }; } return { browser: await session(ctx), label: "内置运行浏览器" }; } register(["WebBrowser_CreateV3", "WebBrowser_CreateV2", "WebBrowser_Create", "WebBrowser_CreateBackground"], async (ctx, node, args) => { const { browser, label } = await resolveSession(ctx, args); const url = String(args.url ?? ""); if (url && String(args.openNewTab ?? "0") === "1" && browser.openNewTab) { const tab = await browser.openNewTab(url); ctx.log(`已使用${label}在新标签页打开 ${tab.url}`, "info", node.id); return { url: tab.url, title: tab.title }; } if (url) { const result = await browser.navigate(url); ctx.log(`已使用${label}打开 ${result.url}`, "info", node.id); return result; } return { url: await browser.currentUrl(), title: "" }; }); function selectorOf(args: Record): unknown { const selector = args.uiSelector ?? args.selector ?? args.target ?? args.uiWindowSelector; if (selector === undefined || selector === null || selector === "") { throw new RuntimeError("validation_error", "元素选择器为空", { recommendation: "填写 css:#id、text:提交、aria:button=提交 或使用拾取器" }); } return selector; } register(["WebBrowser_GoUrl", "WebBrowser_Navigate"], async (ctx, node, args) => { const browser = await session(ctx); const url = String(args.url ?? ""); if (!url) { throw new RuntimeError("validation_error", "跳转地址为空", { nodeId: node.id, componentId: node.componentId, recommendation: "填写要访问的 URL" }); } const result = await browser.navigate(url); ctx.log(`已跳转 ${result.url}`, "info", node.id); return result; }); register("WebBrowser_Switch", async (ctx, node, args) => { const browser = await session(ctx); const title = String(args.title ?? "").trim(); const url = String(args.url ?? "").trim(); if (browser.switchTab && (title || url)) { const tab = await browser.switchTab({ title: title || undefined, url: url || undefined }); ctx.log(`已切换到标签页:${tab.title || tab.url}`, "info", node.id); return { switched: true, targetId: tab.targetId, title: tab.title, url: tab.url }; } ctx.log("网页切换已由统一运行浏览器接管", "info", node.id); return true; }); register("WebBrowser_Close", async (ctx) => { if (ctx.browser) await ctx.browser.close(); ctx.browser = undefined; return true; }); register(["Element_Click_V6", "Element_Click_V3", "Element_Click"], async (ctx, node, args) => { const browser = await session(ctx); const result = await browser.click(selectorOf(args)); ctx.log(`已点击 ${(result as { selector?: string }).selector || ""}`, "info", node.id); return result; }); register(["Element_SetValue_V6", "Element_SetValue_V4", "Element_SetValue"], async (ctx, node, args) => { const browser = await session(ctx); const text = String(args.text ?? args.value ?? ""); const result = await browser.type(selectorOf(args), text, true); ctx.log(`已输入 ${text.length > 40 ? `${text.slice(0, 40)}…` : text}`, "info", node.id); if (String(args.isEnter ?? "0") === "1" || String(args.isEnter) === "true") { await browser.runJs("document.activeElement && document.activeElement.blur()"); } return result; }); register(["Element_GetText_V5", "Element_GetText"], async (ctx, node, args) => { const browser = await session(ctx); const attrType = String(args.attr_type ?? ""); const attrName = String(args.attr_name ?? ""); const attribute = attrType && attrType !== "default" ? attrName || attrType : undefined; const text = await browser.getText(selectorOf(args), attribute); ctx.log(`读取到 ${text.length > 60 ? `${text.slice(0, 60)}…` : text}`, "info", node.id); return text; }); register(["Element_Exists", "Element_ElementExists"], async (ctx, node, args) => { const browser = await session(ctx); const exists = await browser.exists(selectorOf(args)); ctx.log(exists ? "元素存在" : "元素不存在", "info", node.id); return exists; }); register(["Element_SelectOperation_v1", "Element_SelectOperation"], async (ctx, node, args) => { const browser = await session(ctx); const value = String(args.itemsToSelect ?? args.choose ?? args.value ?? ""); const result = await browser.select(selectorOf(args), value); ctx.log(`已选择 ${value}`, "info", node.id); return result; }); register([ "WebBrowser_RunJs", "WebBrowser_RunJsInIeByTitle", "WebBrowser_RunJsInIeBySelector", "WebBrowser_RunJsInIeBySelector_v1", "WebBrowser_RunJsInIeBySelector_v2", "WebBrowser_RunJsInIeBySelector_v3", "WebBrowser_RunJsInIeBySelectorGround" ], async (ctx, node, args) => { const browser = await session(ctx); const script = String(args.jsCode ?? args.script ?? args.code ?? ""); if (!script) throw new RuntimeError("validation_error", "JS 代码为空", { nodeId: node.id, componentId: node.componentId }); return browser.runJs(script); }); register(["WebBrowser_SaveAs", "WebBrowser_SaveAs_V1"], async (ctx, node, args) => { const browser = await session(ctx); const folder = String(args.path ?? ""); if (!folder) throw new RuntimeError("validation_error", "下载保存目录为空", { nodeId: node.id, componentId: node.componentId }); const file = await browser.download(selectorOf(args), folder, String(args.filename ?? ""), Number(args.timeout ?? 10000)); ctx.log(`文件已下载 ${file}`, "info", node.id); return file; }); register("WebBrowser_Screenshot", async (ctx, node, args) => { const browser = await session(ctx); const target = String(args.file_path ?? args.image_path ?? `${ctx.evidenceDir}/${node.id}.png`); const file = await browser.screenshot(target); ctx.log(`截图已保存 ${file}`, "info", node.id); return file; }); // Image_Screenshot 统一在 vision.ts 实现(屏幕/文件来源 + 区域裁剪),此处不再重复注册