Compare commits
4 Commits
02af3478a7
...
2ae1010f67
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ae1010f67 | ||
|
|
8e711d502e | ||
|
|
ea3ff0b891 | ||
|
|
61a7f60a0e |
@ -19,3 +19,14 @@
|
||||
## 其他
|
||||
- 用户确认删除了 compat-audit 测试产物 .compat-report.txt(vitest 跑 compat-audit 会重新生成,未跟踪)。
|
||||
- 用户询问改造进度:阶段 0~5 已全部完成,只剩阶段 6(迁入 Agent-Employee + Bun 冒烟 + 可选 PowerShell worker)。
|
||||
|
||||
## 任务分享/导入功能 + 只读文件保存 EPERM 修复(提交 `f95a244`)
|
||||
- 用户报错 flows:save EPERM(read_gateway_api_keys.json.tmp rename 失败):根因是外部复制导入的任务文件带 Windows 只读属性,rename 覆盖只读文件在 Windows 抛 EPERM。已当场清掉用户文件只读属性和残留 .tmp。
|
||||
- saveFlow 加固为 writeFlowFile:rename 失败(EPERM/EACCES/EBUSY)→ 清只读属性+删除后 rename → copyFileSync 覆盖兜底,仍失败给友好占用提示。
|
||||
- 新增分享/导入:exportFlowToPath/importFlowFromPath(.rpaflow.json = FlowDefinition JSON);IPC flows:export/flows:import(弹对话框、多选导入、失败逐个汇报);preload flows.export/import;TasksPage 头部「导入任务」+ 每行「分享」按钮;导入 id 冲突生成 imported_* 新 id 绝不覆盖。
|
||||
- 测试 7/7 绿(新增导出往返/id 冲突/非法文件/只读覆盖 4 例),typecheck、build 绿。
|
||||
|
||||
## 元素拾取 Chrome 启动失败(exit 0)修复(提交 `ea3ff0b`)
|
||||
- 根因:上次运行遗留的 Chrome 占用专用 profile(rpa-studio-external-*)时,再次启动的 Chrome 转交旧实例后自杀(exit 0),报「Chrome 启动失败(进程已退出,代码 0)」。
|
||||
- 修复:launchExternalBrowser 检测子进程立即退出 → 按命令行里的 user-data-dir 过滤杀残留进程 → 重试一次;启动参数加 --hide-crash-restore-bubble 防恢复气泡遮挡拾取。
|
||||
- 验证:故意拉起无调试端口的同 profile Chrome 后跑 e2e,自动清理重试生效,演示流程 succeeded。
|
||||
|
||||
@ -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<void> {
|
||||
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<ExternalConnection> {
|
||||
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)) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user