PRA-only-demo/src/main/capabilities/web-batch2.ts
yu a968075100 feat: 外接浏览器支持阶段三+四(多标签管理 + 拾取与高级组件适配)
- external.ts: listPageTargets 标签枚举(过滤 chrome://edge:// 内部页)、openNewPageTarget(PUT /json/new)、ExternalConnection 增加 httpBase/targetId
- session.ts: listTabs/switchTab/openNewTab/getCookies 实现;外部按 targetId 缓存多 ws 会话,切换时迁移弹窗/网络监听;close 关闭全部会话;pickElement 放开外部浏览器(拾取脚本经 Runtime.evaluate 注入,行为与内置一致)
- engine/types.ts: BrowserLike 增加 listTabs/switchTab/openNewTab/getCookies 及窗口/监听可选方法
- browser.ts: Create 支持 openNewTab;WebBrowser_Switch 按标题(正则)/网址真实切换
- web-batch2.ts: GetTAB/V1/V2 真实枚举+匹配+激活;全部组件优先使用流程已选浏览器会话
- web-crawl.ts: 采集/监听组件优先流程会话;BatchDataCrawlNew browserType=chrome/edge 拉起外部浏览器
- browser-control.ts: GetCookieNew 升级 Network.getCookies(含 HttpOnly)
- 冒烟: Edge 双 Tab(ws 会话独立)、/json/new、页面枚举、Network.getCookies 全通过
2026-10-30 11:56:08 +08:00

150 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<BrowserLike> {
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, unknown>): 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;
});