From d39c77324a2b534004a9d3c2520dc2fe3abec47a Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Mon, 2 Nov 2026 13:44:46 +0800 Subject: [PATCH] fix: reuse external browsers for element pickup --- src/main/browser/session.ts | 20 +++++++++++++++++ src/main/index.ts | 30 +++++++++++++++++++++----- src/preload/index.ts | 2 +- src/renderer/src/panels/Properties.tsx | 22 ++++++++++++++----- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/main/browser/session.ts b/src/main/browser/session.ts index 5c383f1..4ad60cd 100644 --- a/src/main/browser/session.ts +++ b/src/main/browser/session.ts @@ -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()); } diff --git a/src/main/index.ts b/src/main/index.ts index 8728b93..fd65f2d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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); diff --git a/src/preload/index.ts b/src/preload/index.ts index 6ba6b75..6878953 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -29,7 +29,7 @@ const api = { ipcRenderer.invoke("dialog:save", options || {}) }, browser: { - pickElement: (options?: { url?: string }): Promise => + pickElement: (options?: { url?: string; browser?: { mode: "launch"; browserType: "chrome" | "edge"; port?: number; extraArgs?: string[] } | { mode: "remote"; host: string; port: number } }): Promise => ipcRenderer.invoke("browser:pick-element", options || {}) }, visionLab: { diff --git a/src/renderer/src/panels/Properties.tsx b/src/renderer/src/panels/Properties.tsx index 08be64e..b6f14f9 100644 --- a/src/renderer/src/panels/Properties.tsx +++ b/src/renderer/src/panels/Properties.tsx @@ -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) {