多身份隔离浏览器 · 每个身份都是独立 partition,每个窗口都能被 AI agent 精确操控
内置 agent-browser 与 MCP server,用户无需安装 Node 或任何 CLI
English · 中文
一个 Electron 桌面浏览器,把「多账号隔离」和「让 AI agent 操作浏览器」这两件事拼在一起: - 每个**浏览器身份**是一个独立的 Chromium `persist:` partition —— cookie、localStorage、登录态完全隔离,同一个网站可以同时登录多个账号; - 每个身份是一个带前进/后退/地址栏的独立窗口,用起来就是个普通浏览器; - 内置 [agent-browser](https://github.com/vercel-labs/agent-browser) 二进制,并通过 **MCP** 把这些窗口暴露出去,让 Codex / Claude Code 精确操作**指定身份**的页面。 不需要用户预先安装 Node、Chrome for Testing 或任何 CLI。 ## 它解决什么问题 现成的浏览器自动化工具都假设「浏览器由 agent 启动」。但如果你的产品本身就是个浏览器客户端,情况是反过来的:**窗口已经存在,而且分属不同账号身份**,agent 需要的是接进来并且不串号。 这里的做法是: 1. app 启动时开启一个 CDP 端口(随机分配,仅监听回环); 2. 每开一个身份窗口,主进程用 `Target.getTargetInfo` 取到该窗口内容页的**权威 targetId**; 3. 通过一个本地 bridge 把「身份 → targetId」的映射暴露出去; 4. MCP server 拿 targetId 当 tab ref 直接定位,一步命中。 于是 `snapshot(identity: "账号A")` 和 `snapshot(identity: "账号B")` 永远打在正确的窗口上,即使两个窗口打开的是同一个网站、标题和 URL 完全一样。 ## 快速开始 需要 Node ≥ 20 和 pnpm(仅开发时需要;打包后的 app 不依赖它们)。 `agent-browser` 必须 **≥ 0.36**。低于这个版本,`tab list --json` 不会输出 `targetId`, 而它是把身份对应到窗口的唯一依据 —— 所有工具都会找不到目标。`package.json` 里已经锁定,不要降级。 ```bash pnpm install pnpm bundle:ab # 把 agent-browser 二进制与 core skill 复制到 resources/ pnpm dev ``` 在界面里添加一两个浏览器身份,点击身份即打开对应窗口。 界面默认英文。点右上角 **EN / 中** 可切换为中文,选择会被记住,身份窗口的顶栏和复制出去的 agent prompt 也会跟着切换。 ## 测试 那些微妙、且大多没有文档的判定逻辑,都抽在小巧的纯模块里,便于用测试锁住。 基于 `node:test`,无需额外依赖: ```bash pnpm test ``` 覆盖:`window.open` / 弹窗决策(`window-open.ts`)、代理输入解析(`proxy.ts`)、 地址栏 URL 规范化(`url-input.ts`)、Codex 配置的 TOML 段落拼接(`toml.ts`)、 身份色板(`colors.ts`)。 打包: ```bash pnpm build:mac # 或 build:win ``` 默认使用 **adhoc 签名**(`identity: '-'`),不需要任何 Apple 开发者证书,产物能在本机正常运行。但 adhoc 签名的 app **不能分发给别人**——对方打开会被 Gatekeeper 拦下。要正式分发,通过环境变量提供证书并把 `notarize` 改为 `true`: ```bash CSC_LINK=/path/to/cert.p12 CSC_KEY_PASSWORD=... \ APPLE_ID=... APPLE_APP_SPECIFIC_PASSWORD=... APPLE_TEAM_ID=... \ pnpm build:mac ``` ## 接入 Codex / Claude Code ### 最省事:让 agent 自己接 界面上有一块**「让 agent 自己接入」**,里面是一段现成的 prompt。复制它,粘给 Codex 或 Claude Code,agent 会自己写配置、重启、然后调 `list_identities` 验证链路通不通。 这段 prompt 不只是配置片段,它还告诉 agent 那 13 个工具分别能干什么、以及哪些坑别踩(ref 跨调用失效、凭记忆猜 agent-browser 语法、靠页面标题区分身份)。内容由运行时的真实值生成,所以里面的路径在当前机器上一定是对的。 ### 或者点一下按钮 同一个面板里有「一键配置 Codex」,会把 MCP server 写进 `~/.codex/config.toml`(幂等,写入前自动备份)。路径是运行时从 `process.execPath` 取的,不会写错。 ### 或者手动配 macOS 上是: ```toml [mcp_servers.personae] command = "/Applications/Personae.app/Contents/MacOS/Personae" args = ["/Applications/Personae.app/Contents/Resources/mcp-server.mjs"] [mcp_servers.personae.env] ELECTRON_RUN_AS_NODE = "1" ``` `Contents/MacOS/` 下的可执行文件名跟随 `productName`,所以是 `Personae`。注意 `electron-builder` 的 `executableName` 只对 Windows 生效。 `command` 指向 app 自己的二进制,配合 `ELECTRON_RUN_AS_NODE=1` 让它退化成纯 Node 运行时。**这样用户机器上不需要装 Node**(已实测:把 `PATH` 设为 `/usr/bin:/bin`,完整流程仍然跑通)。 Claude Code: ```bash claude mcp add personae \ --env ELECTRON_RUN_AS_NODE=1 \ -- /Applications/Personae.app/Contents/MacOS/Personae \ /Applications/Personae.app/Contents/Resources/mcp-server.mjs ``` ## MCP 工具 所有工具都接受 `identity` 参数(身份名或 id)。 | 工具 | 用途 | | -------------------------- | --------------------------------------------------------------------------------- | | `load_skill` | 取 agent-browser 当前版本的真实命令语法(默认返回段落目录,`section` 取具体段落) | | `list_identities` | 列出所有身份及其打开状态、当前 URL | | `open_identity` | 打开某个身份的窗口 | | `snapshot` | 无障碍树快照,返回 `[ref=eN]` 元素引用 | | `navigate` | 导航到 URL | | `click` / `fill` / `press` | 交互;`click` 支持按 ref 或按可读文本定位 | | `act` | 在同一个 agent-browser 进程里执行多条命令 | | `get_text` / `get_url` | 读取内容 | | `screenshot` | 截图 | | `eval_js` | 执行 JS | `click` / `fill` 传 `@eN` 时会自动前置一次 snapshot,因为 **ref 只在单个 agent-browser 进程内有效**,跨进程复用必然报 `Unknown ref`。需要多步操作时用 `act` 把它们放进同一个 batch。 **操作弹出的子窗口。** 当页面弹出窗口(OAuth 登录、分享对话框等),它会成为归属同一身份的子窗口(见[弹窗与 OAuth](#弹窗与-oauth))。子窗口是独立的 CDP target,在 `list_identities` 里以 `children` 列在该身份下。所有「操作 / 读取」类工具都接受可选的 `target` 参数 —— 传子窗口的 `targetId`(或序号)即可操作弹窗,不传则照旧操作主窗口。 ## 架构 ``` ┌─────────────────────────────────────────────────────────┐ │ Electron 主进程 │ │ │ │ ├─ CDP server(端口 0 → 系统分配,仅 127.0.0.1) │ │ ├─ IdentityManager 身份存档 / 窗口生命周期 / targetId │ │ └─ Agent Bridge 本地 HTTP,暴露身份↔targetId 映射 │ └───────────┬─────────────────────────────────┬────────────┘ │ │ ┌─────────▼──────────┐ ┌───────────▼───────────┐ │ 身份窗口(每身份一个)│ │ 发现文件 │ │ │ │ userData/ │ │ BrowserWindow 外壳 │ │ agent-bridge.json │ │ ├ WebContentsView │ │ (端口每次启动都变, │ │ │ 顶栏 chrome.html│ │ 靠固定路径发现) │ │ └ WebContentsView │ └───────────┬───────────┘ │ 内容(partition)│ │ │ ↑ agent 操作这里 │ │ └─────────────────────┘ │ │ ┌───────────────────────────────────▼─────────┐ │ scripts/mcp-server.mjs(stdio JSON-RPC) │ │ 读发现文件 → 取 targetId → 调 agent-browser │ └───────────────────┬─────────────────────────┘ │ ┌───────────▼────────────┐ │ Codex / Claude Code │ └────────────────────────┘ ``` ### 为什么身份窗口是「外壳 + 两个 WebContentsView」 需要在第三方网页外面套一层自己的导航栏 UI,同时不使用 `