From ea3ff0b8913a5f18965ec292a6d9ef360faca5b8 Mon Sep 17 00:00:00 2001 From: yu <1919727607@qq.com> Date: Wed, 4 Nov 2026 17:45:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=83=E7=B4=A0=E6=8B=BE?= =?UTF-8?q?=E5=8F=96=20Chrome=20=E5=90=AF=E5=8A=A8=E5=A4=B1=E8=B4=A5(exit?= =?UTF-8?q?=200):=20=E6=AE=8B=E7=95=99=E5=AE=9E=E4=BE=8B=E5=8D=A0=E7=94=A8?= =?UTF-8?q?=E4=B8=93=E7=94=A8=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: 上次运行遗留的 Chrome 仍占用 rpa-studio-external-* profile 时, 再次启动的 Chrome 会把请求转交旧实例并立即退出(代码 0), 报 「Chrome 启动失败(进程已退出,代码 0)」。 - external: 子进程立即退出时清理占用该 profile 的残留进程并重试一次 (win32 按命令行中的 user-data-dir 过滤杀进程, 其他平台 pkill -f) - 启动参数加 --hide-crash-restore-bubble, 避免固定 profile 被强杀后 「恢复页面?」气泡遮挡页面拾取 - 验证: 故意拉起无调试端口的同 profile Chrome 后跑浏览器 e2e, 自动 清理重试生效, 演示流程 succeeded --- packages/rpa-core/src/browser/external.ts | 44 +++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/rpa-core/src/browser/external.ts b/packages/rpa-core/src/browser/external.ts index 775ebf9..7007bb9 100644 --- a/packages/rpa-core/src/browser/external.ts +++ b/packages/rpa-core/src/browser/external.ts @@ -1,10 +1,13 @@ import { execFile, spawn, type ChildProcess } from "node:child_process"; +import { promisify } from "node:util"; import { promises as fs } from "node:fs"; import os from "node:os"; import path from "node:path"; import { RuntimeError } from "../engine/types"; import { ExternalTransport } from "./transport"; +const execAsync = promisify(execFile); + /** * 外部浏览器(Edge / Chrome)进程管理与 CDP 连接。 * @@ -209,6 +212,27 @@ export async function connectRemoteBrowser(options: ExternalRemoteOptions): Prom }; } +/** 清理占用专用 profile 的残留浏览器进程。 + * 同一 user-data-dir 已有实例时,再次启动的 Chrome 会把请求转交旧实例并立即退出(代码 0), + * 导致「启动失败(进程已退出,代码 0)」——这些实例都是本模块之前拉起的,清理是安全的。 */ +async function killStaleBrowserProcesses(browserType: ExternalBrowserType, userDataDir: string): Promise { + try { + if (process.platform === "win32") { + const names = browserType === "edge" ? "msedge.exe" : "chrome.exe"; + const marker = userDataDir.replace(/'/g, "''"); + const script = + `Get-CimInstance Win32_Process -Filter "Name='${names}'" | ` + + `Where-Object { $_.CommandLine -like '*${marker}*' } | ` + + "ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"; + await execAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 8000, windowsHide: true }); + } else { + await execAsync("pkill", ["-f", userDataDir], { timeout: 5000 }); + } + } catch { + // 清理失败不阻塞启动:可能本来就没有残留,或权限受限 + } +} + /** 拉起本机 Edge/Chrome(带独立 profile + 调试端口),并接管其页面 */ export async function launchExternalBrowser(options: ExternalLaunchOptions): Promise { const browserType = options.browserType === "edge" ? "edge" : "chrome"; @@ -233,11 +257,14 @@ export async function launchExternalBrowser(options: ExternalLaunchOptions): Pro `--user-data-dir=${userDataDir}`, "--no-first-run", "--no-default-browser-check", + // 固定 profile 曾被强杀过时,Chrome 会弹「恢复页面?」气泡挡住页面拾取 + "--hide-crash-restore-bubble", startUrl, ...extraArgs ]; - const child = spawn(browserPath, args, { detached: false, stdio: "ignore", windowsHide: true }); + const spawnBrowser = (): ChildProcess => spawn(browserPath, args, { detached: false, stdio: "ignore", windowsHide: true }); + let child = spawnBrowser(); child.on("error", () => { // spawn 失败由下方就绪轮询超时统一报错 }); @@ -245,11 +272,24 @@ export async function launchExternalBrowser(options: ExternalLaunchOptions): Pro // 就绪轮询:等 /json/version 可访问(最长 15s) const deadline = Date.now() + 15000; let ready = false; + let retriedStale = false; while (Date.now() < deadline) { if (child.exitCode !== null) { + if (!retriedStale) { + // 立即退出(常见代码 0):专用 profile 被残留实例占用,Chrome 转交后自杀; + // 清理残留后重试一次 + retriedStale = true; + await killStaleBrowserProcesses(browserType, userDataDir); + await new Promise((resolve) => setTimeout(resolve, 300)); + child = spawnBrowser(); + child.on("error", () => { + // spawn 失败由下方就绪轮询超时统一报错 + }); + continue; + } throw new RuntimeError("browser_error", `${label} 启动失败(进程已退出,代码 ${child.exitCode})`, { detail: browserPath, - recommendation: "检查浏览器安装是否完整,或参数是否合法" + recommendation: "已尝试清理残留实例仍失败:检查浏览器安装是否完整,或重启电脑后重试" }); } if (await isDebugPortReady(port)) {