PRA-only-demo/src/main/capabilities/browser-control.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

77 lines
8.5 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 path from "node:path";
import { browserSession } from "../browser/session";
import { register } from "../engine/registry";
import { RuntimeError, type BrowserLike, type RunContext } from "../engine/types";
async function session(ctx: RunContext): Promise<BrowserLike> { if (!ctx.browser) ctx.browser = await browserSession(); return ctx.browser; }
function selector(args: Record<string, unknown>): unknown { const value = args.uiSelector ?? args.download_selector ?? args.selector; if (value === undefined || value === null || value === "") throw new RuntimeError("validation_error", "元素选择器为空"); return value; }
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string { for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); } return fallback; }
register("WebBrowser_Reload", async (ctx) => { const browser = await session(ctx); return browser.navigate(await browser.currentUrl()); });
register("WebBrowser_GoBack", async (ctx) => { const browser = await session(ctx); await browser.runJs("history.back(); true"); return true; });
register("WebBrowser_GoForward", async (ctx) => { const browser = await session(ctx); await browser.runJs("history.forward(); true"); return true; });
register("WebBrowser_GetTitle", async (ctx) => session(ctx).then((browser) => browser.runJs("document.title")));
register("WebBrowser_GetUrl", async (ctx) => session(ctx).then((browser) => browser.currentUrl()));
register("WebBrowser_GetDocument", async (ctx) => session(ctx).then((browser) => browser.runJs("document.documentElement.outerHTML")));
register(["WebBrowser_GetScrollPos", "WebBrowser_GetScrollPos_V1"], async (ctx) => session(ctx).then((browser) => browser.runJs("({x:window.scrollX,y:window.scrollY})")));
register(["WebBrowser_SetScrollPos", "WebBrowser_SetScrollPos_V1"], async (ctx, _node, args) => {
const browser = await session(ctx);
const raw = String(args.scrollpos ?? "").trim();
let x = Number(args.x ?? args.left ?? 0);
let y = Number(args.y ?? args.top ?? args.position ?? 0);
if (raw) {
if (raw.includes(",")) { const parts = raw.split(","); x = Number(parts[0]) || 0; y = Number(parts[1]) || 0; }
else { y = Number(raw) || 0; }
}
await browser.runJs(`window.scrollTo(${Number.isFinite(x) ? x : 0},${Number.isFinite(y) ? y : 0}); true`); return { x, y };
});
/** 全量 Cookie:优先走 CDP Network.getCookies(含 HttpOnly),不支持时退回 document.cookie */
async function getCookies(ctx: RunContext): Promise<Record<string, string>> {
const browser = await session(ctx);
if (browser.getCookies) {
const cookies = await browser.getCookies();
return Object.fromEntries(cookies.map((cookie) => [cookie.name, cookie.value]));
}
const raw = String(await browser.runJs("document.cookie"));
return Object.fromEntries(raw.split(";").map((part) => part.trim()).filter(Boolean).map((part) => { const [name, ...rest] = part.split("="); return [name, decodeURIComponent(rest.join("="))]; }));
}
register(["WebBrowser_GetCookies", "WebBrowser_GetCookieNew"], async (ctx, node) => {
const cookies = await getCookies(ctx);
ctx.log(`已获取 ${Object.keys(cookies).length} 个 Cookie(含 HttpOnly)`, "info", node.id);
return cookies;
});
register("WebBrowser_GetCookie", async (ctx, _node, args) => {
const name = text(args, ["name"]); const cookies = await getCookies(ctx); return cookies[name] ?? "";
});
register("WebBrowser_SetCookies", async (ctx, node, args) => {
let cookies: unknown = args.cookies; if (typeof cookies === "string") { try { cookies = JSON.parse(cookies); } catch { throw new RuntimeError("validation_error", "Cookie JSON无效", { nodeId: node.id, componentId: node.componentId }); } }
if (!cookies || typeof cookies !== "object") throw new RuntimeError("validation_error", "Cookie对象为空", { nodeId: node.id, componentId: node.componentId }); const browser = await session(ctx);
for (const [name, value] of Object.entries(cookies as Record<string, unknown>)) await browser.runJs(`document.cookie=${JSON.stringify(`${name}=${encodeURIComponent(String(value))}; path=/`)}; true`); return getCookies(ctx);
});
register("WebBrowser_SetCookies_V1", async (ctx, node, args) => {
const name = text(args, ["cookie_name"]); if (!name) throw new RuntimeError("validation_error", "Cookie name为空", { nodeId: node.id, componentId: node.componentId });
const value = encodeURIComponent(text(args, ["cookie_value"])); const cookiePath = text(args, ["cookie_path"], "/"); const domain = text(args, ["cookie_domain"]); const expires = text(args, ["cookie_expires"]); const secure = String(args.secure ?? "0") === "1";
const cookie = `${name}=${value}; path=${cookiePath}${domain ? `; domain=${domain}` : ""}${expires ? `; expires=${expires}` : ""}${secure ? "; secure" : ""}`; await (await session(ctx)).runJs(`document.cookie=${JSON.stringify(cookie)}; true`); return cookie;
});
register("WebBrowser_Wait", async (ctx) => {
const browser = await session(ctx); const deadline = Date.now() + 15000; do { try { if (await browser.runJs("document.readyState === 'complete'")) return true; } catch {} await new Promise((resolve) => setTimeout(resolve, 100)); } while (Date.now() < deadline); return false;
});
register(["WebBrowser_CloseWeb", "WebBrowser_CloseWeb_V1", "WebBrowser_CloseWeb_V2", "WebBrowser_Close_V1"], async (ctx) => { const browser = await session(ctx); await browser.close(); ctx.browser = undefined; return true; });
register(["WebBrowser_GetBrowserPic", "WebBrowser_GetBrowserPicV1"], async (ctx, node, args) => { const browser = await session(ctx); const target = path.resolve(text(args, ["path", "file_path", "image_path"], path.join(ctx.evidenceDir, `${node.id}.png`))); return browser.screenshot(target); });
register("Element_ExistsV1", async (ctx, _node, args) => (await session(ctx)).exists(selector(args)));
register("Element_WaitElement_v5", async (ctx, _node, args) => { const browser = await session(ctx); const target = selector(args); const expected = String(args.choose ?? "1") !== "3"; const deadline = Date.now() + Math.max(0, Number(args.time_out ?? args.timeout ?? 10)) * 1000; do { if ((await browser.exists(target)) === expected) return true; await new Promise((resolve) => setTimeout(resolve, 200)); } while (Date.now() <= deadline); return false; });
register(["WebBrowser_DownFile", "WebBrowser_DownFile_V1", "WebBrowser_DownloadFile", "WebBrowser_DownloadFile_V1"], async (ctx, node, args) => {
const browser = await session(ctx); const headers = text(args, ["headers"], "{}"); const body = text(args, ["body", "param"], "{}"); if (!["", "{}"].includes(headers) || !["", "{}"].includes(body)) throw new RuntimeError("validation_error", "当前下载能力暂不支持自定义Headers或请求体", { nodeId: node.id, componentId: node.componentId });
const folder = text(args, ["savepath", "save_path"]); if (!folder) throw new RuntimeError("validation_error", "下载目录为空", { nodeId: node.id, componentId: node.componentId }); const filename = text(args, ["filename", "file_name"]); const seconds = Number(args.download_time ?? 0); const timeout = seconds > 0 ? seconds * 1000 : Number(args.timeout ?? 10000) * (node.componentId.endsWith("_V1") && node.componentId.includes("DownFile") ? 1000 : 1);
const directUrl = text(args, ["url"]); if (!directUrl) return browser.download(selector(args), folder, filename, timeout);
const temporaryId = `rpa-download-${Date.now().toString(36)}`; await browser.runJs(`(()=>{const a=document.createElement('a');a.id=${JSON.stringify(temporaryId)};a.href=${JSON.stringify(directUrl)};a.style.display='none';document.body.appendChild(a);return true})()`);
try { return await browser.download(`css:#${temporaryId}`, folder, filename, timeout); } finally { try { await browser.runJs(`document.getElementById(${JSON.stringify(temporaryId)})?.remove();true`); } catch {} }
});
export const BROWSER_CONTROL_IDS = ["WebBrowser_Reload", "WebBrowser_GoBack", "WebBrowser_GoForward", "WebBrowser_GetTitle", "WebBrowser_GetUrl", "WebBrowser_GetDocument", "WebBrowser_GetScrollPos", "WebBrowser_GetScrollPos_V1", "WebBrowser_SetScrollPos", "WebBrowser_SetScrollPos_V1", "WebBrowser_GetCookies", "WebBrowser_GetCookieNew", "WebBrowser_GetCookie", "WebBrowser_SetCookies", "WebBrowser_SetCookies_V1", "WebBrowser_Wait", "WebBrowser_CloseWeb", "WebBrowser_CloseWeb_V1", "WebBrowser_CloseWeb_V2", "WebBrowser_Close_V1", "WebBrowser_GetBrowserPic", "WebBrowser_GetBrowserPicV1", "Element_ExistsV1", "Element_WaitElement_v5", "WebBrowser_DownFile", "WebBrowser_DownFile_V1", "WebBrowser_DownloadFile", "WebBrowser_DownloadFile_V1"] as const;