- engine/(registry/types/runtime/expression)物理迁入包, 解耦shared/types(域类型归包, 宿主type-only re-export) - 31个纯Node能力+27个测试迁入包; 宿主留守Electron/浏览器/AI依赖能力, index.ts经@rpa/core/capabilities拉起 - 新增createEngine()/defaultEngine: excel缓存/Excel句柄/db连接/FTP客户端/OCR实例/拟真设置挂引擎实例 - remaining-core子流程依赖改为host-bridge注入, 包内不再import宿主flows - 宿主main/index.ts启动时注入子流程源; electron-vite/vitest/tsconfig配@rpa/core源码别名 - 验证: typecheck绿, 快照909个id diff为零, 332测试过(2个桌面冒烟因会话无交互SetForegroundWindow被拒, 环境阻塞)
182 lines
7.6 KiB
TypeScript
182 lines
7.6 KiB
TypeScript
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<BrowserLike> {
|
|
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<string, unknown>): 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<string, unknown>): 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 实现(屏幕/文件来源 + 区域裁剪),此处不再重复注册
|