import { browserSession } from "../browser/session"; import { register } from "../engine/registry"; import { RuntimeError, type BrowserLike, type BrowserTabInfo, type RunContext } from "../engine/types"; /** 优先使用流程中「打开网页」已建立的浏览器会话(含外部 Edge/Chrome),否则退回内置 */ async function currentSession(ctx: RunContext): Promise { return ctx.browser ?? (await browserSession()); } /** 标题匹配:优先按正则解释,非法正则退化为字面包含 */ function titleMatcher(title: string): (tab: BrowserTabInfo) => boolean { let pattern: RegExp; try { pattern = new RegExp(title); } catch { pattern = new RegExp(title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); } return (tab) => pattern.test(tab.title) || tab.title.includes(title); } 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 currentSession(ctx); 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, args) => { const browser = await currentSession(ctx); const tabs = (await browser.listTabs?.()) ?? [{ index: 0, targetId: "", title: "", url: await browser.currentUrl(), active: true }]; const title = String(args.title ?? "").trim(); const url = String(args.url ?? "").trim(); const matchType = String(args.match_type ?? "0"); // 0=按标题 1=按网址 let matched = tabs; if (title && (matchType !== "1" || !url)) matched = matched.filter(titleMatcher(title)); if (url && (matchType === "1" || !title)) matched = matched.filter((tab) => tab.url.includes(url)); if (matched.length === 0) { throw new RuntimeError("tab_not_found", "没有匹配的标签页", { detail: JSON.stringify(tabs.map((tab) => ({ title: tab.title, url: tab.url }))), recommendation: "用「获取标签页」不加条件查看当前打开的全部页面" }); } // 激活第一个匹配的标签页 const isActive = String(args.isActive ?? args.is_active ?? "0") === "1"; if (isActive && browser.switchTab) { const first = matched[0]; await browser.switchTab(first.targetId ? { targetId: first.targetId } : { index: first.index }); } ctx.log(`标签页匹配 ${matched.length}/${tabs.length} 个`, "info", node.id); return matched; }); register("WebBrowser_Switch_V1", async (ctx, node, args) => { const browser = await currentSession(ctx); const tabs = (await browser.listTabs?.()) ?? [{ index: 0, targetId: "", title: "", url: await browser.currentUrl(), active: true }]; const active = tabs.find((tab) => tab.active) ?? tabs[0]; ctx.log( tabs.length <= 1 ? "运行浏览器为单窗口模式,当前即唯一标签页" : `已定位当前标签页(共 ${tabs.length} 个,可用「获取标签页」+匹配条件切换)`, "info", node.id ); return { switched: true, index: active?.index ?? 0, url: active?.url ?? "", active: String(args.is_active ?? "1") === "1" }; }); register("WebBrowser_Maximize", async (ctx, node) => { const browser = await currentSession(ctx); browser.maximize?.(); return true; }); register("WebBrowser_Minimize", async (ctx, node) => { const browser = await currentSession(ctx); browser.minimize?.(); return true; }); register("WebBrowser_Stop", async (ctx, node) => { const browser = await currentSession(ctx); browser.stopLoading?.(); return true; }); register(["Element_ElementAarray", "Element_ElementCaptureSimilar"], async (ctx, node, args) => { const browser = await currentSession(ctx); 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; }); register("Element_StartAutoClosePopup", async (ctx, node) => { const browser = await currentSession(ctx); const started = browser.startAutoCloseDialogs?.(); ctx.log(started ? "弹窗自动关闭已开启" : "弹窗自动关闭已在运行中", "info", node.id); return { active: true, started }; }); register("Element_StopAutoClosePopup", async (ctx, node) => { const browser = await currentSession(ctx); const stopped = browser.stopAutoCloseDialogs?.(); ctx.log(stopped ? "弹窗自动关闭已停止" : "弹窗自动关闭未在运行", "info", node.id); return { active: false, stopped }; }); register(["Element_GetLineBarChartData", "Element_GetPieChartData"], async (ctx, node, args) => { const browser = await currentSession(ctx); const selector = String((typeof args.uiSelector === "object" && args.uiSelector !== null ? (args.uiSelector as { value?: string }).value : args.uiSelector) ?? "body").replace(/^css:/, "").trim() || "body"; const script = "(function(){" + "if (!window.echarts) return [];" + "var sel = " + JSON.stringify(selector) + ";" + "var dom = document.querySelector(sel);" + "var inst = dom ? window.echarts.getInstanceByDom(dom) : null;" + "if (!inst) { var all = document.querySelectorAll('[_echarts_instance_]'); for (var i = 0; i < all.length; i++) { inst = window.echarts.getInstanceByDom(all[i]); if (inst) break; } }" + "if (!inst) return [];" + "var opt = inst.getOption(); var out = [];" + "(opt.series || []).forEach(function(s) { out.push({ name: s.name || '', type: s.type || '', data: s.data || [] }); });" + "return out;" + "})()"; const data = await browser.runJs(script); ctx.log("已获取图表数据:" + JSON.stringify(data).slice(0, 120), "info", node.id); return data; });