agent-bridge
by wensia
README.md
# Agent Bridge
让本地 agent(Codex / Claude Code)通过 MCP 遥控浏览器里**已登录的 ChatGPT Pro 会话**。
浏览器插件只负责操作页面,输入什么、什么时候发、发完怎么验收,全部由 agent 决定。人不需要往插件里敲任何内容。
## 架构
```
Agent (Codex / Claude Code)
│ MCP tool call(stdio)
▼
mcp-server/ Node 进程:MCP server + WebSocket server
│ ws://127.0.0.1:8767
▼
扩展 background WebSocket 客户端,负责重连、保活、动作路由
│ chrome.tabs / content script
▼
ChatGPT 页面 复用你在 Chrome 里已经登录好的 Pro 会话
```
反过来(server 连插件)行不通:MV3 扩展没法监听端口,所以由扩展主动连 MCP server。
## 安装
### 1. 构建并加载扩展
```bash
cd <仓库路径>/extension
npm install
npm run build
```
打开 `chrome://extensions` → 右上角开启「开发者模式」→「加载已解压的扩展程序」→ 选 `extension/dist/` 目录。
### 2. 启动 MCP server
```bash
cd mcp-server && npm install
```
server 不需要构建,Node 26 直接跑 TypeScript:
```bash
node mcp-server/src/index.ts # 或在根目录 npm run mcp
```
端口默认 `8767`,用环境变量 `AGENT_BRIDGE_PORT` 可改(插件那边要在 popup 里同步改)。
### 3. 注册给 agent
**Claude Code:**
```bash
claude mcp add agent-bridge -- node <仓库路径>/mcp-server/src/index.ts
```
**Codex**(`~/.codex/config.toml`):
```toml
[mcp_servers.agent-bridge]
command = "node"
args = ["<仓库路径>/mcp-server/src/index.ts"]
```
agent 会自己拉起 server 进程,不用另外开终端手动跑。
### 多会话并行
每个 Claude/Codex 会话会拉起自己的 MCP 进程,但浏览器扩展只有一个连接。agent-bridge 用选主机制解决:先启动的进程监听 8767 成为 hub,后启动的自动作为客户端经 hub 转发;hub 所在会话退出后,客户端会在几秒内自动接管,扩展重连回去。不需要守护进程,也不要去 kill 占用端口的进程——那通常是另一个会话正在用的 hub。
多 agent 共享同一个 ChatGPT 账号。**任务的持久身份是会话 URL**(`/c/<id>`):每个任务用 `chatgpt_new_conversation` 开新会话,首次发送后拿到会话 URL,后续调用用 `conversation` 参数携带——标签页被关了也会按 URL 自动重开。`tabId` 仍可用,但它只是会话当前的载体。
### 4. 配对(v0.3 起必须)
hub 不再接受匿名连接。首次启动 MCP server 时会自动生成配对码:
```bash
cat ~/.agent-bridge/pairing-secret # 0600 权限,等同于凭据,别外发
```
点扩展图标打开 popup → 状态显示「待配对」→ 粘贴配对码 → 配对。只需要做一次,存在扩展本地存储里。
### 5. 确认链路
popup 状态显示「已连接」即可。没连上就检查 server 是否在跑、地址是否一致、配对码是否正确。
## 一句话触发协作(内置协议)
不需要手动粘贴 prompt。协议正文内置在 MCP server 的 `prompts/*.md` 里,与工具同版本演进,通过两个通道暴露:
- **`chatgpt_collab_guide` 工具**:所有客户端都能调,skill 走这条路
- **MCP prompts**(`chatgpt_discuss` / `chatgpt_review` / `chatgpt_implement`):支持该能力的客户端会列成斜杠命令
配套 skill 在 `skill/SKILL.md`(已安装到 `~/.claude/skills/chatgpt-pro/`)。装好后直接说人话:
```
和 ChatGPT Pro 探讨一下这个迁移方案
让 ChatGPT Pro review 一下导入模块
让 ChatGPT Pro 按这个计划把代码写了
```
agent 会自动:判定模式 → 取协议 → 备上下文 → 新会话 → 发送 → 分段等待 → 独立验收 → 按固定格式汇报(含会话链接、SHA 基线、测试结果)。
## 工具清单
| 工具 | 作用 |
| --- | --- |
| `chatgpt_collab_guide` | 返回内置协作协议(discuss / review / implement),agent 一句话触发协作时先调它 |
| `chatgpt_status` | 桥接状态、标签页列表、页面处于 idle/generating/login_required |
| `chatgpt_open` | 打开或复用 ChatGPT 标签页,可跳到指定会话恢复上下文 |
| `chatgpt_new_conversation` | 整页导航开新会话,确保无上下文残留 |
| `chatgpt_attach` | 上传本地文件(传路径,server 侧读取),返回大小与 SHA-256 |
| `chatgpt_send` | 写入并发送 prompt,支持超长文本 |
| `chatgpt_wait_reply` | 轮询到本轮回复生成完毕,返回正文与代码块 |
| `chatgpt_last_reply` | 不等待,直接读最后一条回复(含 `complete` 标志) |
| `chatgpt_link` | 取当前会话 URL 和 id,用于写进交付报告 |
| `page_text` / `page_query` / `page_click` | 通用页面操作;`page_query` 返回可直接回喂给 `page_click` 的稳定选择器 |
## 配合双代理协作 prompt
这套工具是按你那份「Codex 当总负责人、ChatGPT Pro 当外部工程师」的 prompt 设计的,几条硬性要求都有对应支撑:
| prompt 里的要求 | 对应能力 |
| --- | --- |
| 打包 ZIP,记录大小和 SHA-256 | `chatgpt_attach` 直接返回 `bytes` 与 `sha256`,不用另外算 |
| 多个独立任务各开一个对话,避免上下文污染 | `chatgpt_new_conversation` 走整页导航,草稿和附件都不会残留 |
| ChatGPT Pro 可能很久,不要催促、不要重复发送 | `chatgpt_wait_reply` 默认等 30 分钟且只读不发;超时只是停止等待,页面上的生成不受影响,再调一次即可续等 |
| 保存每个对话的链接,中断后自主恢复 | `chatgpt_link` 取链接,`chatgpt_open` 带 url 跳回去 |
| 登录失效 / 验证码 / 2FA 要暂停并通知人 | 页面一旦判定 `login_required`,工具直接返回错误并明确要求停下来找人,不会尝试绕过 |
| 交付后独立验收 | `chatgpt_wait_reply` 单独返回 `codeBlocks`,补丁可以直接取用,不必二次解析正文 |
典型调用顺序:
```
chatgpt_status → chatgpt_new_conversation → chatgpt_attach(源码.zip)
→ chatgpt_send(任务说明) → chatgpt_wait_reply → chatgpt_link
```
## 安全模型(v0.3)
这条通道能驱动一个已登录的浏览器,所以:
- **配对码鉴权**:所有客户端(扩展、agent 进程)握手时必须携带 `~/.agent-bridge/pairing-secret`(自动生成,0600)。错误配对码、版本不匹配、角色不明都会被拒绝并断开。
- **角色锁定 + Origin 检查**:扩展角色必须来自 `chrome-extension://` Origin(浏览器强制不可伪造,挡网页);网页 Origin 不能当 agent。
- **只绑 127.0.0.1**:`AGENT_BRIDGE_HOST` 配非本机地址会直接拒绝启动。
- **hello 5 秒超时**:握手不完成的连接会被断开,防止沉默连接阻塞 shutdown。
**信任边界声明**:配对码文件只能防住"其他 OS 用户"和"无文件权限的进程"。**与你同 UID 运行的本机进程(依赖、脚本、其他 agent)属于可信计算基**——它们本来就能读你的文件。如果你的威胁模型包含同用户恶意进程,这套机制不够,需要 OS keychain 级别的隔离。
## 故障排查
| 现象 | 原因与处理 |
| --- | --- |
| popup 显示未连接 | MCP server 没跑,或端口和 popup 里填的不一致 |
| `NO_TAB` | 还没打开 ChatGPT 页面,先调 `chatgpt_open` |
| `TAB_NOT_READY` | 页面早于扩展安装就打开了,刷新一下 ChatGPT 标签页 |
| `LOGIN_REQUIRED` | 登录态失效或撞到验证码,必须人工处理,agent 不应尝试绕过 |
| `ELEMENT_NOT_FOUND` | ChatGPT 改版了,更新 `extension/src/content/selectors.ts` 里的候选选择器 |
| 回复读到一半 | `complete: false` 表示还在流式输出,继续等或改用 `chatgpt_wait_reply` |
长任务建议让标签页保持前台(`chatgpt_open` 传 `focus: true`)—— 后台标签页会被 Chrome 节流,轮询会变慢。
## 开发
```
agent-bridge/
├── shared/protocol.ts 线协议唯一真值源,extension 与 mcp-server 共享
├── extension/ 浏览器插件(Vite + CRXJS + React,独立 package)
│ └── src/
│ ├── shared/protocol.ts 一行 re-export 到顶层 shared/,保持内部导入路径稳定
│ ├── background/ WebSocket 客户端、重连保活、动作路由
│ ├── content/
│ │ ├── selectors.ts ChatGPT 的 DOM 选择器,改版了只改这里
│ │ ├── chatgpt.ts 页面操作:写入、发送、上传、读状态
│ │ ├── state.ts 页面状态判定(idle/generating/login_required)
│ │ └── serialize.ts 把回复 DOM 还原成 Markdown
│ └── popup/ 连接状态、地址配置、运行日志(kiln 设计语言)
├── mcp-server/ MCP server + WebSocket hub(Node 26 直接跑 TS,无构建)
│ ├── prompts/ 协作协议唯一真值源(protocol + discuss/review/implement)
│ └── src/
│ ├── hub.ts 抢到端口的进程:持有扩展连接,转发 agent 客户端的调用
│ ├── link.ts 没抢到端口的进程:作为客户端接入 hub,hub 挂了触发接管
│ └── prompts.ts 协议装载,经 MCP prompts 和 guide 工具双通道暴露
└── skill/SKILL.md 自然语言触发器(同步到 ~/.claude/skills/chatgpt-pro/)
```
```bash
# 以下都在项目根目录执行
npm run build # 构建扩展到 extension/dist/
npm run typecheck # extension + mcp-server 双侧类型检查
npm run lint
npm test # 全部单测(serialize + state)+ MCP 冒烟(含多会话选主场景)
npm run check:live # 真实浏览器只读自检,需要 Chrome 和扩展在线
npm run preview:popup # 生成 extension/dist/preview.html 核对 popup 视觉
```
popup 用 kiln 设计语言:暖白衬底 + 无边框白卡片靠暖调阴影分离,陶土红只用于关键动作和状态,控件 36px / 圆角 4px,卡片圆角 6px,字号收在 11–15px。改完 UI 后这样核对三种状态:
```bash
npm run build && npm run preview:popup
python3 -m http.server 4173 -d extension/dist
# http://127.0.0.1:4173/preview.html?s=connected | disconnected | granted
```
字体是个有意的偏离:kiln 主字体为 Noto Sans SC webfont,但 MV3 的 CSP 不放行远程字体,自托管一套完整 CJK 字重要十几 MB,对一个弹窗不成比例,因此走 kiln 定义的系统回退栈(macOS 上解析为 PingFang SC)。
两处容易踩的实现细节,改代码前值得知道:
- **输入框不能直接赋值。** ChatGPT 用的是 ProseMirror,写 `innerHTML` 只改渲染结果,它的内部文档还是空的,一发送就变成空消息。必须走 `document.execCommand('insertText')` —— 那会产生浏览器原生的 `beforeinput`/`input` 事件,ProseMirror 才接得住。
- **长等待不做成单个 RPC。** MV3 的 service worker 空闲 30 秒就被回收,扛不住几十分钟的单次请求。所以所有动作都是瞬时的,「等回复」由 MCP server 轮询 `chat.status` 编排,扩展侧再用 20 秒心跳续命。
判断「新回复出来了」也不只看消息条数:页面可能只渲染了用户那条,此时读到的最后一条仍是上一轮的旧回复。所以 `chatgpt_send` 会记下发送前的正文快照,`chatgpt_wait_reply` 拿它做对比,再要求内容连续两轮不变才收尾。
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues