- 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 全通过
150 lines
7.0 KiB
TypeScript
150 lines
7.0 KiB
TypeScript
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;
|
||
});
|