fix: reuse external browsers for element pickup

This commit is contained in:
yu 2026-11-02 13:44:46 +08:00
parent 927b18551f
commit d39c77324a
4 changed files with 63 additions and 11 deletions

View File

@ -166,6 +166,16 @@ export class BrowserSession implements BrowserLike {
}
/** 当前会话是否为外部浏览器 */
/** 会话底层 CDP 是否已断开(缓存清理用) */
get closed(): boolean {
return this.transport.closed;
}
/** 缓存键,外部会话排序用 */
get sessionKey(): string {
return this.externalKey;
}
get isExternal(): boolean {
return this.external !== null;
}
@ -1370,6 +1380,16 @@ export async function externalBrowserSession(options: ExternalBrowserOptions): P
return BrowserSession.createExternal(options);
}
export function listExternalSessions(): BrowserSession[] {
return [...externalSessionCache().values()]
.filter((session) => session.isExternal && !session.closed)
.sort((left, right) => right.sessionKey.localeCompare(left.sessionKey));
}
export function latestExternalSession(): BrowserSession | null {
return listExternalSessions()[0] || null;
}
export function hasBrowserSession(): boolean {
return Boolean(globalThis.__rpaBrowserWindow && !globalThis.__rpaBrowserWindow.isDestroyed());
}

View File

@ -1,7 +1,13 @@
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
import path from "node:path";
import type { AppInfo, AppSettings, FlowDefinition, FlowSaveOptions, FlowSaveResult } from "../shared/types";
import { BrowserSession, hasBrowserSession, setBrowserElementWaitMs } from "./browser/session";
import {
BrowserSession,
externalBrowserSession,
hasBrowserSession,
latestExternalSession,
setBrowserElementWaitMs
} from "./browser/session";
import "./capabilities";
import { catalogPath, loadCatalog } from "./catalog";
import { createDemoFlow, createExampleFlows, createFlow, deleteFlow, getFlow, listFlows, saveFlow } from "./flows";
@ -103,12 +109,26 @@ function registerIpc(): void {
setBrowserElementWaitMs(settings.browserElementWaitMs);
return settings;
});
ipcMain.handle("browser:pick-element", async (_event, options: { url?: string } = {}) => {
type BrowserPickOptions = {
url?: string;
browser?: import("./browser/external").ExternalBrowserOptions;
};
ipcMain.handle("browser:pick-element", async (_event, options: BrowserPickOptions = {}) => {
const url = options.url?.trim();
if (!hasBrowserSession() && !url) {
throw new Error("请先配置或运行「打开网页」,再拾取元素");
const browserOptions = options.browser;
let session;
if (browserOptions) {
session = await externalBrowserSession(browserOptions);
} else {
session = latestExternalSession();
}
if (!session) {
if (!hasBrowserSession() && !url) {
throw new Error("请先配置或运行「打开网页」,再拾取元素");
}
session = await BrowserSession.create();
}
const session = await BrowserSession.create();
const currentUrl = await session.currentUrl();
if (url && (currentUrl === "about:blank" || currentUrl !== url)) {
await session.navigate(url);

View File

@ -29,7 +29,7 @@ const api = {
ipcRenderer.invoke("dialog:save", options || {})
},
browser: {
pickElement: (options?: { url?: string }): Promise<import("../shared/types").ElementPickResult> =>
pickElement: (options?: { url?: string; browser?: { mode: "launch"; browserType: "chrome" | "edge"; port?: number; extraArgs?: string[] } | { mode: "remote"; host: string; port: number } }): Promise<import("../shared/types").ElementPickResult> =>
ipcRenderer.invoke("browser:pick-element", options || {})
},
visionLab: {

View File

@ -506,12 +506,24 @@ function FieldControl({
const currentFlow = useEditorStore.getState().flow;
const nodeIndex = currentFlow?.nodes.findIndex((item) => item.id === node.id) ?? -1;
const openNode = currentFlow && nodeIndex > 0
? [...currentFlow.nodes.slice(0, nodeIndex)].reverse().find((item) => {
const url = textOf(item.values.url).trim();
return Boolean(url && !url.startsWith("${"));
})
? [...currentFlow.nodes.slice(0, nodeIndex)].reverse().find((item) => /^WebBrowser_Create/.test(item.componentId))
: undefined;
const picked = await window.rpa.browser.pickElement({ url: textOf(openNode?.values.url) || undefined });
let browser;
if (openNode) {
const type = textOf(openNode.values.type).trim().toLowerCase();
const isRemote = ["1", "true", "yes"].includes(textOf(openNode.values.isRemote).trim().toLowerCase());
const port = Number(textOf(openNode.values.port) || 9222) || 9222;
if (isRemote) {
browser = { mode: "remote" as const, host: textOf(openNode.values.ipAddress) || "127.0.0.1", port };
} else if (type === "chrome" || type === "edge") {
const extraArgs = textOf(openNode.values.arguments).split(/\s+/).map((item) => item.trim()).filter(Boolean);
browser = { mode: "launch" as const, browserType: type === "edge" ? "edge" as const : "chrome" as const, port, extraArgs };
}
}
const picked = await window.rpa.browser.pickElement({
url: textOf(openNode?.values.url).trim() || undefined,
browser
});
onChange(field.key, picked.selector);
onToast(`已拾取 ${picked.tag}${picked.label ? ` · ${picked.label}` : ""}`);
} catch (error) {