PRA-only-demo/src/main/capabilities/browser-common.ts
yu caa8e19332 阶段1-rpa-core建包: engine+31个纯Node能力迁入packages/rpa-core, 领域类型单一源化, createEngine工厂收拢模块级状态
- 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被拒, 环境阻塞)
2026-11-04 12:36:03 +08:00

232 lines
11 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 "@rpa/core";
import { RuntimeError, type BrowserElementInfo, type BrowserLike, type RunContext } from "@rpa/core";
const GET_ELEMENT_IDS = ["Element_GetElement", "Element_GetElementV1", "Element_GetElementV2", "Element_GetElementNew"];
const GET_INFO_IDS = ["Element_GetElementInfo", "Element_GetElementInfo_V1"];
const PARENT_IDS = ["Element_Parent", "Element_Parent_V1", "Element_Parent_V2"];
const CHILDREN_IDS = ["Element_Children", "Element_ChildrenV1", "Element_Children_V1", "Element_Children_V2"];
const SIBLING_IDS = ["Element_Sibling", "Element_Sibling_V1", "Element_Sibling_V2"];
const SIMILAR_IDS = ["Element_SimilarElementList", "Element_SimilarElementList_V1", "Element_SimilarElementList_V2"];
export const BROWSER_COMMON_IDS = [
...GET_ELEMENT_IDS, ...GET_INFO_IDS, ...PARENT_IDS, ...CHILDREN_IDS, ...SIBLING_IDS,
"Element_GetSelect", "Element_SetSelect", "Element_GetTableData", ...SIMILAR_IDS
] as const;
async function session(ctx: RunContext): Promise<BrowserLike> {
if (!ctx.browser) ctx.browser = await browserSession();
return ctx.browser;
}
function enabled(value: unknown): boolean {
return value === true || value === 1 || String(value).toLowerCase() === "true" || String(value) === "1";
}
function assertUnifiedBrowser(args: Record<string, unknown>): void {
if (enabled(args.designated_info)) {
throw new RuntimeError("validation_error", "当前能力不支持指定其他网页或窗口对象", {
recommendation: "关闭指定网页/窗口对象,使用当前运行浏览器"
});
}
const browserType = String(args.browser_type ?? "").toLowerCase();
if (browserType.includes("ie") || browserType.includes("sap") || browserType.includes("ground")) {
throw new RuntimeError("validation_error", "当前能力仅支持标准浏览器 DOM");
}
}
function hasSelector(value: unknown): boolean {
if (typeof value === "string") return value.trim().length > 0;
return Boolean(value && typeof value === "object" && Object.keys(value as object).length > 0);
}
function selectorOf(args: Record<string, unknown>): unknown {
const position = String(args.element_position ?? "default").toLowerCase();
if (position.includes("xpath") && hasSelector(args.xpath)) return `xpath:${String(args.xpath)}`;
if (position.includes("css") && hasSelector(args.cssSelector)) return `css:${String(args.cssSelector)}`;
for (const key of ["uiSelector", "default", "selector", "target"]) {
if (hasSelector(args[key])) return args[key];
}
if (hasSelector(args.cssSelector)) return `css:${String(args.cssSelector)}`;
if (hasSelector(args.xpath)) return `xpath:${String(args.xpath)}`;
throw new RuntimeError("validation_error", "元素选择器为空", {
recommendation: "填写标准 CSS/XPath 选择器或使用已有元素引用"
});
}
function requireMethod<K extends keyof BrowserLike>(browser: BrowserLike, key: K): NonNullable<BrowserLike[K]> {
const method = browser[key];
if (typeof method !== "function") throw new RuntimeError("browser_error", `浏览器不支持 DOM 方法:${String(key)}`);
return method.bind(browser) as NonNullable<BrowserLike[K]>;
}
function positiveInteger(value: unknown, fallback: number): number {
const parsed = Math.trunc(Number(value));
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function optionalIndex(value: unknown): number | undefined {
if (value === undefined || value === null || String(value).trim() === "") return undefined;
return positiveInteger(value, 1);
}
function filterOf(args: Record<string, unknown>): string {
return enabled(args.use) ? String(args.filter ?? "").trim() : "";
}
function listValue(value: unknown): string[] {
if (Array.isArray(value)) return value.map(String).filter(Boolean);
const text = String(value ?? "").trim();
if (!text) return [];
if (text.startsWith("[")) {
try {
const parsed = JSON.parse(text) as unknown;
if (Array.isArray(parsed)) return parsed.map(String).filter(Boolean);
} catch {
// Fall back to comma-separated input.
}
}
return text.split(",").map((item) => item.trim()).filter(Boolean);
}
async function completeUrl(browser: BrowserLike, value: string, shouldComplete: boolean): Promise<string> {
if (!shouldComplete || !value) return value;
try {
return new URL(value, await browser.currentUrl()).toString();
} catch {
return value;
}
}
async function infoResult(browser: BrowserLike, info: BrowserElementInfo, args: Record<string, unknown>): Promise<unknown> {
const action = String(args.action ?? "gettext").toLowerCase();
if (["gettext", "text", "1"].includes(action)) return info.text;
if (["getvalue", "value", "2"].includes(action)) return info.elementValue;
if (["getposition", "position", "region", "3"].includes(action)) return info.region;
if (["gethref", "href", "link", "4"].includes(action)) return completeUrl(browser, info.href, enabled(args.attr_complate));
if (["gethtml", "html", "code", "5"].includes(action)) return info.html;
if (["getattribute", "attribute", "attr", "6"].includes(action)) {
const name = String(args.attr_name ?? "").trim();
if (!name) throw new RuntimeError("validation_error", "属性名为空");
return completeUrl(browser, info.attributes[name] ?? "", enabled(args.attr_complate));
}
if (["all", "info", "0"].includes(action)) return info;
throw new RuntimeError("validation_error", `不支持的元素信息类型:${action}`);
}
register(GET_ELEMENT_IDS, async (ctx, node, args) => {
assertUnifiedBrowser(args);
const result = await requireMethod(await session(ctx), "getElement")(selectorOf(args));
ctx.log(`已获取元素对象:${result.tag}`, "info", node.id);
return result;
});
register(GET_INFO_IDS, async (ctx, node, args) => {
assertUnifiedBrowser(args);
const browser = await session(ctx);
const info = await requireMethod(browser, "getElementInfo")(selectorOf(args));
const result = await infoResult(browser, info, args);
ctx.log("已获取元素信息", "info", node.id);
return result;
});
register(PARENT_IDS, async (ctx, node, args) => {
assertUnifiedBrowser(args);
const result = await requireMethod(await session(ctx), "getParent")(
selectorOf(args), positiveInteger(args.level, 1), filterOf(args)
);
ctx.log("已获取父元素", "info", node.id);
return result;
});
register(CHILDREN_IDS, async (ctx, node, args) => {
assertUnifiedBrowser(args);
const index = optionalIndex(args.num);
const result = await requireMethod(await session(ctx), "getChildren")(selectorOf(args), index, filterOf(args));
ctx.log(`已获取 ${result.length} 个子元素`, "info", node.id);
return index === undefined ? result : result[0];
});
register(SIBLING_IDS, async (ctx, node, args) => {
assertUnifiedBrowser(args);
const position = String(args.position ?? "2").toLowerCase();
const direction = ["1", "previous", "prev", "up"].includes(position) ? "previous" : "next";
const result = await requireMethod(await session(ctx), "getSibling")(selectorOf(args), direction, filterOf(args));
ctx.log(`已获取${direction === "previous" ? "上一个" : "下一个"}兄弟元素`, "info", node.id);
return result;
});
register("Element_GetSelect", async (ctx, node, args) => {
assertUnifiedBrowser(args);
const state = await requireMethod(await session(ctx), "getSelectOptions")(selectorOf(args));
const choose = String(args.choose ?? "1").toLowerCase();
let result: unknown = state.values.length === 1 ? state.values[0] : state.values;
if (["2", "text", "texts"].includes(choose)) result = state.texts.length === 1 ? state.texts[0] : state.texts;
if (["3", "all", "options"].includes(choose)) result = state.options;
ctx.log(`已读取下拉框,共 ${state.options.length} 个选项`, "info", node.id);
return result;
});
register("Element_SetSelect", async (ctx, node, args) => {
assertUnifiedBrowser(args);
const values = listValue(args.itemsToSelect ?? args.value);
if (values.length === 0) throw new RuntimeError("validation_error", "下拉框选项为空");
const choose = String(args.choose ?? "1");
const mode = choose === "2" ? "add" : choose === "3" ? "remove" : "replace";
const result = await requireMethod(await session(ctx), "setSelectOptions")(selectorOf(args), values, mode);
ctx.log(`已设置下拉框:${result.texts.join(", ")}`, "info", node.id);
return result;
});
register("Element_GetTableData", async (ctx, node, args) => {
assertUnifiedBrowser(args);
if (enabled(args.sap_mode) || String(args.mode ?? "").toLowerCase().includes("sap")) {
throw new RuntimeError("validation_error", "当前组件仅支持标准 HTML table,不支持 SAP 表格");
}
let rows = await requireMethod(await session(ctx), "getTableData")(selectorOf(args));
const includeHeader = !["0", "false"].includes(String(args.operation ?? "1").toLowerCase());
if (!includeHeader) rows = rows.slice(1);
const range = String(args.tabelRange ?? "0");
const row = positiveInteger(args.rowNo, 1) - 1;
const column = positiveInteger(args.columnNo, 1) - 1;
let result: unknown = rows;
if (range === "1") result = rows[row] ?? [];
else if (range === "2") result = rows.map((item) => item[column] ?? "");
else if (range === "3") result = rows[row]?.[column] ?? "";
ctx.log(`已读取 HTML 表格 ${rows.length} 行`, "info", node.id);
return result;
});
register(SIMILAR_IDS, async (ctx, node, args) => {
assertUnifiedBrowser(args);
const browser = await session(ctx);
const parent = enabled(args.relate_parent) ? args.parent_selector : undefined;
if (enabled(args.relate_parent) && !hasSelector(parent)) {
throw new RuntimeError("validation_error", "已启用关联父元素,但父元素选择器为空");
}
const references = await requireMethod(browser, "getSimilarElements")(selectorOf(args), {
parent,
start: positiveInteger(args.start_index, 1),
end: Number.isFinite(Number(args.end_index)) ? Math.trunc(Number(args.end_index)) : -1,
reverse: enabled(args.reverse)
});
const action = String(args.action ?? "selector").toLowerCase();
let result: unknown = references;
if (!["selector", "element", "elements"].includes(action)) {
const infos = await Promise.all(references.map((reference) => requireMethod(browser, "getElementInfo")(reference)));
if (["gettext", "text"].includes(action)) result = infos.map((info) => info.text);
else if (["getvalue", "value"].includes(action)) result = infos.map((info) => info.elementValue);
else if (["getattribute", "attribute", "attr"].includes(action)) {
const name = String(args.attr_name ?? "").trim();
if (!name) throw new RuntimeError("validation_error", "属性名为空");
result = await Promise.all(
infos.map((info) => completeUrl(browser, info.attributes[name] ?? "", enabled(args.attr_complate)))
);
} else {
throw new RuntimeError("validation_error", `不支持的相似元素操作:${action}`);
}
}
ctx.log(`已获取 ${references.length} 个相似元素`, "info", node.id);
return result;
});