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 { 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): 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): 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(browser: BrowserLike, key: K): NonNullable { const method = browser[key]; if (typeof method !== "function") throw new RuntimeError("browser_error", `浏览器不支持 DOM 方法:${String(key)}`); return method.bind(browser) as NonNullable; } 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 { 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 { 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): Promise { 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; });