baa-conductor

git clone 

commit
88ae94b
parent
0cdbd8a
author
im_wower
date
2026-03-22 18:30:52 +0800 CST
feat(firefox): default local ws bridge
8 files changed,  +657, -714
A coordination/tasks/T-C002.md
+91, -0
 1@@ -0,0 +1,91 @@
 2+---
 3+task_id: T-C002
 4+title: Firefox 插件接本地 WS
 5+status: review
 6+branch: feat/firefox-local-ws-client
 7+repo: /Users/george/code/baa-conductor
 8+base_ref: main@0cdbd8a
 9+depends_on:
10+  - T-C001
11+write_scope:
12+  - plugins/baa-firefox/**
13+  - docs/firefox/**
14+updated_at: 2026-03-22
15+---
16+
17+# Firefox 插件接本地 WS
18+
19+## 目标
20+
21+让 Firefox 插件默认自动连接 `mini` 本地 WS,并把管理页收口为 WS 状态、HTTP API 状态和控制按钮。
22+
23+## 本任务包含
24+
25+- 插件默认连接本地 `/ws/firefox`
26+- 保留远程 HTTP 控制状态同步
27+- 管理页只保留 WS 状态、HTTP 状态、暂停/恢复/排空按钮
28+- 去掉手工编辑 WS 地址和 API 地址
29+- 支持自动重连和服务端重启后恢复
30+- 更新 Firefox 使用文档和验证步骤
31+
32+## 本任务不包含
33+
34+- 不修改 `conductor-daemon`
35+- 不修改 Firefox 插件以外的业务逻辑
36+
37+## 建议起始文件
38+
39+- `plugins/baa-firefox/controller.js`
40+- `plugins/baa-firefox/background.js`
41+- `plugins/baa-firefox/controller.html`
42+- `plugins/baa-firefox/docs/conductor-control.md`
43+- `docs/firefox/README.md`
44+
45+## 交付物
46+
47+- 默认接入本地 WS 的 Firefox 插件
48+- 收口后的管理页 UI
49+- 已更新的 Firefox 文档和任务卡
50+
51+## 验收
52+
53+- `node --check` 覆盖插件脚本
54+- 说明如何验证 WS 连接、断线重连、按钮控制
55+- `git diff --check` 通过
56+
57+## files_changed
58+
59+- `coordination/tasks/T-C002.md`
60+- `plugins/baa-firefox/controller.js`
61+- `plugins/baa-firefox/controller.html`
62+- `plugins/baa-firefox/controller.css`
63+- `plugins/baa-firefox/manifest.json`
64+- `plugins/baa-firefox/README.md`
65+- `plugins/baa-firefox/docs/conductor-control.md`
66+- `docs/firefox/README.md`
67+
68+## commands_run
69+
70+- `node --check /Users/george/code/worktrees/baa-conductor-firefox-local-ws-client/plugins/baa-firefox/controller.js`
71+- `node --check /Users/george/code/worktrees/baa-conductor-firefox-local-ws-client/plugins/baa-firefox/background.js`
72+- `node --check /Users/george/code/worktrees/baa-conductor-firefox-local-ws-client/plugins/baa-firefox/content-script.js`
73+- `node --check /Users/george/code/worktrees/baa-conductor-firefox-local-ws-client/plugins/baa-firefox/page-interceptor.js`
74+- `git -C /Users/george/code/worktrees/baa-conductor-firefox-local-ws-client diff --check`
75+
76+## result
77+
78+- Firefox 插件默认固定连接本地 `ws://127.0.0.1:4317/ws/firefox`,不再允许手工修改地址
79+- 远程 HTTP control API 固定为 `https://conductor.makefile.so`,并继续按轮询加退避重试同步状态
80+- 管理页已收口为 WS 状态、HTTP 状态和 `暂停` / `恢复` / `排空` 按钮
81+- 本地 WS 已支持握手、快照展示、断线自动重连和本地服务重启后恢复
82+- Firefox 文档已更新为新的固定地址、简化 UI 和验证步骤
83+
84+## risks
85+
86+- 本次只做了静态校验;真实的 WS 握手、断线重连和 HTTP 控制按钮仍需在已运行的 `conductor-daemon` 与远程 `conductor.makefile.so` 上做人工联调
87+- 管理页不再展示标签页/凭证/端点细节;这些底层桥接能力仍然保留,但后续排障要看 WS 详情或浏览器扩展存储
88+
89+## next_handoff
90+
91+- 在装好插件的 Firefox 上手工验证三件事:本地 WS 首次连接、停服后自动重连、`暂停` / `恢复` / `排空` 的远程状态回写
92+- 如果需要更强可观测性,再考虑把当前被隐藏的桥接调试信息迁到单独调试页,而不是重新塞回主管理页
M docs/firefox/README.md
+97, -270
  1@@ -4,327 +4,154 @@
  2 
  3 - [`../../plugins/baa-firefox/`](../../plugins/baa-firefox/)
  4 
  5-当前推荐启动脚本:
  6+这里保留 Firefox 插件与 `baa-conductor` 的最小接入约定。
  7 
  8-- [`../../scripts/firefox/open-firefox-with-plugin.sh`](../../scripts/firefox/open-firefox-with-plugin.sh)
  9+## 当前固定入口
 10 
 11-这里保留协议和控制面约定文档;具体实现以 `plugins/baa-firefox/` 为准。
 12+- Local WS bridge: `ws://127.0.0.1:4317/ws/firefox`
 13+- Public HTTP control API: `https://conductor.makefile.so`
 14 
 15-## 快速启动
 16+当前插件默认同时使用这两条链路:
 17 
 18-当前推荐模式已经改成:
 19+- 本地 WS:负责 Firefox bridge 握手、浏览器元数据同步、服务端快照展示
 20+- 远程 HTTP:负责控制面状态同步,以及 `pause` / `resume` / `drain` 写入
 21 
 22-- 使用系统 Firefox 的正常默认 profile
 23-- `baa-firefox` 插件由你手动安装到这个 profile
 24-- Firefox 常开,不再依赖 `web-ext run` 的临时加载模式
 25+不再允许在插件管理页中手工编辑地址。
 26 
 27-启动命令:
 28+## 目标
 29 
 30-```bash
 31-./scripts/firefox/open-firefox-with-plugin.sh
 32-```
 33-
 34-如果要顺手打开 Claude:
 35-
 36-```bash
 37-./scripts/firefox/open-firefox-with-plugin.sh --url https://claude.ai
 38-```
 39-
 40-说明:
 41-
 42-- 脚本现在只是打开系统 Firefox,不再创建单独 profile
 43-- 脚本也不再临时注入扩展
 44-- 插件需要你手动安装到 Firefox 的正常使用 profile
 45-- 下次新对话直接运行这个脚本即可,前提是 Firefox 里已经装好插件
 46-
 47-本文档定义 `baa-firefox` 与 `baa-conductor` control API 之间的最小协议。
 48-
 49-目标:
 50-
 51-- 让 Firefox 插件能显示全局自动化状态
 52-- 让 Firefox 插件能触发 `pause`、`resume`,可选 `drain`
 53-- 明确可见 `control` 与隐藏 `dispatch` 的职责边界
 54-
 55-非目标:
 56-
 57-- 不定义完整 UI 视觉稿
 58-- 不让浏览器成为调度真相来源
 59-
 60-## 1. 基本原则
 61-
 62-- 真相来源是 control API 背后的 D1 `system_state`,不是浏览器本地状态。
 63-- Firefox 插件只调用 HTTP control API,不直接操作 conductor 进程。
 64-- 插件负责显示状态和发起人工控制动作,不负责创建、分配、恢复 task。
 65-- `pause`、`resume`、`drain` 都是全局动作,不是单标签页动作。
 66-- Firefox 插件默认模式不依赖 websocket;如果保留兼容能力,也只能作为手动启用的可选通道。
 67-- Firefox 启动时由 `background.js` 确保 controller 页存在;controller 启动后立即同步 Control API。
 68-- Control API 临时失败时,插件必须持续自动重试;服务恢复后自动回到正常同步周期。
 69-- 插件里的“跟踪标签页”只统计当前真实打开且仍匹配平台 host 的页面,不把历史 storage 残留当成当前状态。
 70-- 插件里的“凭证”只统计绑定到当前 tracked tab、最近重新捕获且仍通过平台登录规则校验的快照;启动时不自动补开全部平台页。
 71+- 让 Firefox 插件自动接入 `mini` 本地 `/ws/firefox`
 72+- 保留远程 HTTP control-plane 状态同步
 73+- 把管理页收口成 WS 状态、HTTP 状态、控制按钮
 74+- 在本地服务重启或远程 HTTP 短暂失败后自动恢复
 75 
 76-## 2. 模式语义
 77+## 非目标
 78 
 79-全局模式只有三种:
 80+- 不让浏览器成为调度真相源
 81+- 不重新引入旧的手工地址配置
 82+- 不把 `/ws/firefox` 作为公网入口
 83 
 84-- `running`: 正常调度。
 85-- `draining`: 不再启动新的 step,已启动的 run 可以自然结束。
 86-- `paused`: 不再启动新的 step,conductor 调度暂停;已运行任务是否继续或终止由后端策略决定。
 87+## 启动行为
 88 
 89-插件文案必须和这个语义保持一致,尤其不要把 `paused` 展示成“所有运行中的工作都已强制停止”。
 90+- Firefox 启动时,`background.js` 会确保 `controller.html` 存在。
 91+- `controller.html` 启动后立刻连接 `ws://127.0.0.1:4317/ws/firefox`。
 92+- WS 断开后按固定间隔自动重连;本地服务恢复后连接会自动恢复。
 93+- `controller.html` 启动后也会立刻请求 `GET /v1/system/state`。
 94+- HTTP 成功后按 `15` 秒周期继续同步。
 95+- HTTP 失败后按 `1` 秒、`3` 秒、`5` 秒快速重试,再进入 `30` 秒慢速重试。
 96 
 97-## 3. 入口与当前简化模式
 98+## 管理页 UI
 99 
100-推荐入口:
101+当前管理页只保留:
102 
103-- Base URL: `https://control-api.makefile.so`
104-- 新对话或新接入 AI 客户端时,先读 `GET /describe`
105+- 本地 WS 状态卡片
106+- 远程 HTTP 状态卡片
107+- `暂停` / `恢复` / `排空` 按钮
108+- `WS 状态` 原始详情面板
109+- `HTTP 状态` 原始详情面板
110 
111-当前单节点临时模式:
112+不再保留:
113 
114-- `CONTROL_API_AUTH_REQUIRED=false`
115-- Firefox 插件默认直接请求 `https://control-api.makefile.so`
116-- 如果用户没有手动保存配置,插件会继续默认使用 `https://control-api.makefile.so`
117-- Firefox 插件默认不配置 websocket,也不会主动连接 `ws://127.0.0.1:9800`
118-- 当前不要求本地 `baa-server`
119-- 当前不要求在插件里填写 token
120+- WS 地址输入框
121+- HTTP 地址输入框
122+- 手工重连按钮
123+- 手工刷新按钮
124+- 标签页/凭证/端点/日志面板
125 
126-如果后续重新启用鉴权,再恢复 `Authorization: Bearer <token>` 即可。
127-如果后续需要兼容旧的 browser-proxy WS,再在插件里手动填写 `ws://` / `wss://` 地址并显式启用。
128+## WS 协议
129 
130-推荐 describe-first 顺序:
131-
132-1. `GET /describe`
133-2. `GET /v1/capabilities`
134-3. `GET /v1/system/state`
135-4. 如需更强只读发现能力,再查 `GET /v1/controllers`、`GET /v1/tasks`、`GET /v1/runs`
136-
137-## 4. 读取状态
138-
139-### `GET /v1/system/state`
140-
141-Firefox 插件至少要读取这些字段:
142-
143-| 字段 | 类型 | 说明 |
144-| --- | --- | --- |
145-| `automation.mode` | string | `running \| draining \| paused` |
146-| `automation.updated_at` | integer | 最近一次模式变更时间,毫秒时间戳 |
147-| `automation.requested_by` | string \| null | 最近一次变更来源,建议用于审计展示 |
148-| `automation.reason` | string \| null | 最近一次变更原因 |
149-| `leader.controller_id` | string \| null | 当前 leader controller id |
150-| `leader.host` | string \| null | 当前持有租约的 host;当前主线一般是 `mini` |
151-| `leader.role` | string \| null | 当前 leader 注册角色 |
152-| `leader.lease_expires_at` | integer \| null | 当前 lease 到期时间,毫秒时间戳 |
153-| `queue.active_runs` | integer | 当前运行中的 run 数 |
154-| `queue.queued_tasks` | integer | 当前排队中的 task 数 |
155-| `request_id` | string | 请求追踪 id |
156-
157-推荐响应:
158+插件启动后会先发送:
159 
160 ```json
161 {
162-  "ok": true,
163-  "request_id": "req_123",
164-  "data": {
165-    "automation": {
166-      "mode": "running",
167-      "updated_at": 1760000000000,
168-      "requested_by": "browser_admin",
169-      "reason": "human_clicked_resume"
170-    },
171-    "leader": {
172-      "controller_id": "mini-main",
173-      "host": "mini",
174-      "role": "primary",
175-      "lease_expires_at": 1760000030000
176-    },
177-    "queue": {
178-      "active_runs": 2,
179-      "queued_tasks": 7
180-    }
181-  }
182+  "type": "hello",
183+  "clientId": "firefox-ab12cd",
184+  "nodeType": "browser",
185+  "nodeCategory": "proxy",
186+  "nodePlatform": "firefox"
187 }
188 ```
189 
190-插件行为:
191+随后插件会继续上送:
192 
193-- Firefox 启动时,background 会自动确保 `controller.html` 标签页存在。
194-- `controller.html` 启动后立即请求一次 `GET /v1/system/state`。
195-- 当前实现成功后按 `15` 秒周期继续同步。
196-- 当前实现失败后按 `1` 秒、`3` 秒、`5` 秒快速重试,再切到每 `30` 秒一次的慢速重试。
197-- 服务恢复后自动回到 `15` 秒正常同步周期,不需要手动刷新页面或重新打开 controller。
198-- `刷新控制面` 按钮仍然保留,但只是额外的手动触发入口。
199-- 每次成功执行 `pause`、`resume`、`drain` 后,优先使用写接口返回的新状态更新 UI;如果后端暂未回传完整状态,则立即补一次 `GET /v1/system/state`。
200+- `credentials`
201+- `api_endpoints`
202+- `client_log`
203 
204-## 4.1 能力发现和辅助只读接口
205+插件会消费服务端返回的:
206 
207-这些接口不是插件主循环必需,但适合 Claude、手机端网页或人工排障先读:
208+- `hello_ack`
209+- `state_snapshot`
210+- `request_credentials`
211+- `action_result`
212+- `error`
213 
214-- `GET /describe`
215-- `GET /v1/capabilities`
216-- `GET /v1/controllers`
217-- `GET /v1/tasks?limit=5`
218-- `GET /v1/runs?limit=5`
219+其中 `state_snapshot` 用来驱动管理页里的 WS 状态展示。
220 
221-用途:
222+## HTTP 协议
223 
224-- `/describe`:读完整端点表、字段结构、示例和当前模式说明
225-- `/v1/capabilities`:快速区分 public/read/write surface
226-- `/v1/controllers`:确认当前注册的 controller 和 active controller
227-- `/v1/tasks`:确认系统最近有哪些任务
228-- `/v1/runs`:确认系统最近执行过哪些 run
229+读取:
230 
231-## 5. 写接口
232+- `GET /v1/system/state`
233 
234-### 5.1 共用请求体
235+写入:
236 
237-`POST /v1/system/pause`、`POST /v1/system/resume`、`POST /v1/system/drain` 使用同一套 body。
238+- `POST /v1/system/pause`
239+- `POST /v1/system/resume`
240+- `POST /v1/system/drain`
241 
242-请求体:
243-
244-| 字段 | 类型 | 必填 | 说明 |
245-| --- | --- | --- | --- |
246-| `requested_by` | string | 是 | 固定写 `browser_admin` |
247-| `source` | string | 是 | 固定写 `firefox_extension` |
248-| `reason` | string | 是 | 例如 `human_clicked_pause` |
249-| `request_id` | string | 否 | 前端生成的幂等追踪 id,推荐 UUID |
250-
251-示例:
252+写接口请求体固定包含:
253 
254 ```json
255 {
256   "requested_by": "browser_admin",
257   "source": "firefox_extension",
258   "reason": "human_clicked_pause",
259-  "request_id": "4e08d0d6-4e78-4e58-b71f-9cc0f9c3f245"
260-}
261-```
262-
263-### 5.2 状态迁移
264-
265-- `POST /v1/system/pause`: 把全局模式设为 `paused`
266-- `POST /v1/system/resume`: 把全局模式设为 `running`
267-- `POST /v1/system/drain`: 把全局模式设为 `draining`
268-
269-推荐迁移规则:
270-
271-- `running -> paused`
272-- `draining -> paused`
273-- `paused -> running`
274-- `draining -> running`
275-- `running -> draining`
276-
277-推荐幂等规则:
278-
279-- 当前已经是目标模式时,返回 `200` 和当前状态,不报错。
280-- `paused -> drain` 返回 `409 invalid_mode_transition`,要求用户先 `resume` 再 `drain`。
281-
282-### 5.3 成功响应
283-
284-写接口成功时,建议直接返回最新状态,避免插件立刻多打一轮查询:
285-
286-```json
287-{
288-  "ok": true,
289-  "request_id": "req_124",
290-  "data": {
291-    "automation": {
292-      "mode": "paused",
293-      "updated_at": 1760000005000,
294-      "requested_by": "browser_admin",
295-      "reason": "human_clicked_pause"
296-    },
297-    "leader": {
298-      "controller_id": "mini-main",
299-      "host": "mini",
300-      "role": "primary",
301-      "lease_expires_at": 1760000030000
302-    },
303-    "queue": {
304-      "active_runs": 2,
305-      "queued_tasks": 7
306-    }
307-  }
308+  "request_id": "uuid"
309 }
310 ```
311 
312-### 5.4 失败响应
313-
314-失败体遵循 control API 的统一结构:
315-
316-```json
317-{
318-  "ok": false,
319-  "error": "invalid_mode_transition",
320-  "message": "Drain is not allowed while automation is paused.",
321-  "request_id": "req_125"
322-}
323-```
324-
325-插件至少要处理这些错误:
326-
327-- `401` / `403`: 如果后续重新启用鉴权,则表示 token 无效或角色不足
328-- `409`: 非法状态迁移或并发冲突
329-- `5xx`: control API 暂时不可用
330-
331-## 6. 按钮行为
332-
333-最少按钮:
334-
335-- `Pause`
336-- `Resume`
337-- 可选 `Drain`
338-
339-按钮启用规则:
340-
341-- `mode = running`: `Pause` 可点,`Drain` 可点,`Resume` 禁用
342-- `mode = draining`: `Pause` 可点,`Resume` 可点,`Drain` 禁用
343-- `mode = paused`: `Resume` 可点,`Pause` 禁用,`Drain` 禁用
344-
345-交互要求:
346-
347-- 任一写请求发出后,先把三个按钮全部置为 loading / disabled,直到请求结束。
348-- 成功后立即用返回状态刷新 badge、按钮和文案。
349-- 失败后保留旧状态,并展示后端错误消息。
350-- 插件 badge 至少区分 `running`、`draining`、`paused` 三种状态。
351-
352-推荐展示字段:
353+插件会尽量从 HTTP 返回中归一化这些字段:
354 
355-- Control API 连接状态
356-- 当前 `mode`
357-- 当前 leader host
358+- `mode`
359+- `leader`
360+- `lease_holder`
361+- `queue_depth` / `queued_tasks`
362 - `active_runs`
363-- `queued_tasks`
364-- 最近一次成功同步时间
365-- 自动重试中的失败信息和下一次重试时间
366-- 可选 WS 状态;未配置时展示 `未启用`,不要展示成主连接故障
367+- `controlConnection`
368+- `retryCount`
369+- `lastSuccessAt`
370+- `lastFailureAt`
371+- `nextRetryAt`
372 
373-## 7. `control` 与 `dispatch` 边界
374+## 验收与验证
375 
376-Firefox 插件要明确区分两个通道:
377+### 1. 验证 WS 连接
378 
379-- 可见 `control`: 给人类对话、查看状态、做高层决策
380-- 隐藏 `dispatch`: 给自动化 task dispatch 与 review 流程
381+1. 安装插件并启动 Firefox。
382+2. 确认 `controller.html` 自动打开。
383+3. 确认 `本地 WS` 最终显示 `已连接`。
384+4. 在 `WS 状态` 面板中确认:
385+   - `wsUrl` 是 `ws://127.0.0.1:4317/ws/firefox`
386+   - 有最近一次 `state_snapshot`
387 
388-必须遵守:
389+### 2. 验证断线重连
390 
391-- 插件的 `pause`、`resume`、`drain` 只走 control API,不走 Claude 对话注入。
392-- 不把自动化任务下发到可见 `control` 对话。
393-- 不把人类交互消息写进隐藏 `dispatch` 通道。
394-- 浏览器内任何状态都不能替代 D1 中的 `system_state`。
395+1. 在插件已连接的情况下停掉本地 `conductor-daemon`。
396+2. 确认 `本地 WS` 状态变成 `重连中` 或显示最近错误。
397+3. 重启本地服务。
398+4. 确认管理页无需刷新即可回到 `已连接`。
399 
400-推荐实现:
401+### 3. 验证按钮控制
402 
403-- 一个可见的 `control` 会话入口
404-- 一个隐藏的 `dispatch` 通道入口
405-- 一个独立的控制面板调用 control API
406+1. 点击 `暂停`,确认 HTTP 状态里的 `mode` 变成 `paused`。
407+2. 点击 `恢复`,确认 `mode` 变成 `running`。
408+3. 点击 `排空`,确认 `mode` 变成 `draining`。
409+4. 如果 HTTP 临时失败,确认管理页会展示明确错误并自动继续重试。
410 
411-## 8. 给 `baa-firefox` 的落地清单
412+## 相关文件
413 
414-- 读取 `GET /v1/system/state`
415-- 根据 `automation.mode` 渲染 badge 与按钮禁用状态
416-- 实现 `POST /v1/system/pause`
417-- 实现 `POST /v1/system/resume`
418-- 可选实现 `POST /v1/system/drain`
419-- 当前默认不配置 websocket;只有用户手动填写后才启用可选 WS
420-- 当前临时模式下,插件不携带 token;如果后续重新启用鉴权,再恢复 bearer token
421-- 所有状态展示都以 control API 返回值为准
422+- [`../../plugins/baa-firefox/controller.js`](../../plugins/baa-firefox/controller.js)
423+- [`../../plugins/baa-firefox/controller.html`](../../plugins/baa-firefox/controller.html)
424+- [`../../plugins/baa-firefox/background.js`](../../plugins/baa-firefox/background.js)
425+- [`../../plugins/baa-firefox/docs/conductor-control.md`](../../plugins/baa-firefox/docs/conductor-control.md)
M plugins/baa-firefox/README.md
+32, -82
  1@@ -2,117 +2,67 @@
  2 
  3 这个目录现在作为 `baa-conductor` 内部插件子目录维护。
  4 
  5-来源说明:
  6+## 当前默认连接
  7 
  8-- 初始代码来自原独立仓库 `baa-firefox`
  9-- 已迁入当前仓库,后续以这里为主写入面
 10+- 本地 WS bridge:`ws://127.0.0.1:4317/ws/firefox`
 11+- 远程 HTTP control API:`https://conductor.makefile.so`
 12 
 13-Firefox MVP for the BAA browser-proxy path.
 14+管理页已经收口为两块状态和三颗控制按钮:
 15 
 16-## What This Repo Does
 17+- 本地 WS 状态
 18+- 远程 HTTP 状态
 19+- `Pause` / `Resume` / `Drain`
 20 
 21-This extension keeps an always-open controller page and tracks one live browser tab per platform.
 22+不再允许用户手工编辑地址。
 23 
 24-It does four things:
 25+## What This Extension Does
 26 
 27-- reads and updates conductor state via `https://control-api.makefile.so`
 28-- keeps an optional legacy WebSocket path for browser-proxy workflows when manually enabled
 29-- tracks current real tabs for `claude.ai`, `chatgpt.com`, and `gemini.google.com`, and only opens them on demand
 30-- discovers live API endpoints from real browser traffic
 31-- captures auth-related request headers and forwards them to the optional WS channel when enabled
 32+- keeps an always-open `controller.html`
 33+- auto-connects the local Firefox bridge WS on startup
 34+- keeps polling remote HTTP control state with retry/backoff
 35+- sends `hello` / `credentials` / `api_endpoints` metadata to the local WS bridge
 36+- lets the operator trigger `pause` / `resume` / `drain`
 37 
 38-This MVP is intentionally narrow:
 39-
 40-- manual login only
 41-- one tracked tab per platform
 42-- no iframe
 43-- no DOM automation
 44-- no full remote execution path yet
 45+底层的 tab 跟踪、endpoint 发现和凭证捕获逻辑仍然保留,用于本地 WS bridge 同步,但不再在管理页里展开显示。
 46 
 47 ## Files
 48 
 49 - `manifest.json` - Firefox MV3 manifest
 50-- `background.js` - opens and keeps the controller tab around
 51-- `controller.html` - always-open management page
 52-- `controller.js` - control API client, optional WS owner, multi-platform tab manager, webRequest capture, message relay
 53+- `background.js` - keeps the controller tab alive and updates the toolbar badge
 54+- `controller.html` - compact management page
 55+- `controller.js` - local WS client, remote HTTP client, control actions, browser bridge logic
 56 - `content-script.js` - bridge from page events into the extension runtime
 57 - `page-interceptor.js` - MAIN world `fetch` interceptor
 58-- `scripts/run-persistent.sh` - starts Firefox with the persistent `baa-firefox-persistent` profile
 59-- `PLAN.md` - MVP design notes
 60-
 61-## Persistent Startup
 62-
 63-If you already created and logged into the persistent Firefox profile once, start the extension with:
 64-
 65-```bash
 66-./scripts/run-persistent.sh
 67-```
 68-
 69-This launches Firefox with the `baa-firefox-persistent` profile, keeps profile changes, reuses the saved browser login state on this machine, and reloads already-open platform pages so the interceptor is injected immediately.
 70-
 71-Useful overrides:
 72-
 73-- `BAA_FIREFOX_PROFILE` - change the Firefox profile name
 74-- `BAA_FIREFOX_BIN` - change the Firefox binary path
 75-- `BAA_WEB_EXT_BIN` - change the `web-ext` executable
 76-
 77-Example:
 78-
 79-```bash
 80-BAA_FIREFOX_PROFILE=baa-firefox-persistent ./scripts/run-persistent.sh
 81-```
 82 
 83 ## How To Load
 84 
 85 1. Open Firefox.
 86 2. Visit `about:debugging#/runtime/this-firefox`.
 87 3. Click `Load Temporary Add-on...`.
 88-4. Choose [manifest.json](/Users/george/code/baa-conductor-main-merge/plugins/baa-firefox/manifest.json).
 89+4. Choose [manifest.json](/Users/george/code/worktrees/baa-conductor-firefox-local-ws-client/plugins/baa-firefox/manifest.json).
 90 5. Firefox should open `controller.html` automatically.
 91 
 92 ## How To Run
 93 
 94 1. Load this extension temporarily.
 95-2. Open `controller.html`; the default control API should already be `https://control-api.makefile.so`.
 96-3. Leave the optional WS input empty unless you explicitly need the legacy browser-proxy channel.
 97-4. Use the controller buttons to open only the platform pages you actually need.
 98-5. Log in manually in the tabs you want to intercept.
 99-6. Use Claude, ChatGPT, or Gemini normally.
100-7. Watch the controller page for:
101-   - control-plane mode and leader sync
102-   - endpoint discovery
103-   - credential snapshots
104-
105-If you still need the old WS path, manually fill a `ws://` or `wss://` address and click the reconnect button to enable it.
106-
107-## Expected MVP Result
108-
109-Once the platform tabs start making real requests, the extension should forward:
110+2. Make sure local `conductor-daemon` is listening on `http://127.0.0.1:4317`.
111+3. Open `controller.html` and confirm:
112+   - `本地 WS` becomes `已连接`
113+   - `远程 HTTP API` reaches `已连接` or enters visible auto-retry
114+4. Click `暂停` / `恢复` / `排空` as needed.
115+5. Open Claude, ChatGPT, or Gemini normally if you want the browser bridge to report credentials and endpoints.
116 
117-- `hello`
118-- `api_endpoints`
119-- `credentials`
120-- `network_log`
121-- `sse_event`
122-- `client_log`
123+## Verification
124 
125-These message shapes still match the current `baa-server` browser-side WS handling when the optional WS channel is enabled.
126+- WS connect: controller page shows `本地 WS = 已连接`
127+- WS reconnect: stop local daemon and restart it; controller page should move to retrying and then recover automatically
128+- HTTP sync: `远程 HTTP API` should keep refreshing every `15` seconds
129+- Control buttons: after clicking `暂停` / `恢复` / `排空`, the HTTP state should reflect the new mode
130 
131 ## Limitations
132 
133 - only one tab per platform is tracked
134-- tracked tab count only reflects tabs that are currently open and still match the platform host
135-- credential count only reflects recent valid snapshots bound to the current tracked tab
136 - only `fetch` is patched in the page
137-- there is no remote request execution path yet
138+- there is no full remote request execution path yet
139 - if Firefox unloads the extension, the session must be re-established
140-- Gemini interception is heuristic because its web app mixes private RPC paths under `/_/`
141-
142-## Next Step
143-
144-After this works end to end, add:
145-
146-- request execution into the live platform tabs
147-- stronger tab recovery
148-- broader network interception
149-- more robust Gemini and SSE coverage
150+- Gemini interception is still heuristic because its web app mixes private RPC paths under `/_/`
M plugins/baa-firefox/controller.css
+2, -9
 1@@ -32,7 +32,6 @@ body {
 2 
 3 .topbar {
 4   display: flex;
 5-  justify-content: space-between;
 6   gap: 16px;
 7   align-items: end;
 8   margin-bottom: 18px;
 9@@ -67,7 +66,7 @@ h2 {
10   flex-wrap: wrap;
11 }
12 
13-.settings {
14+.actions {
15   margin-bottom: 18px;
16   padding: 14px;
17   border: 1px solid var(--line);
18@@ -110,7 +109,7 @@ button:hover {
19 
20 .grid {
21   display: grid;
22-  grid-template-columns: repeat(4, minmax(0, 1fr));
23+  grid-template-columns: repeat(2, minmax(0, 1fr));
24   gap: 14px;
25   margin-bottom: 18px;
26 }
27@@ -184,12 +183,6 @@ button:hover {
28   word-break: break-word;
29 }
30 
31-@media (max-width: 900px) {
32-  .grid {
33-    grid-template-columns: repeat(2, minmax(0, 1fr));
34-  }
35-}
36-
37 @media (max-width: 640px) {
38   .shell {
39     padding: 16px;
M plugins/baa-firefox/controller.html
+13, -83
  1@@ -11,25 +11,12 @@
  2     <section class="topbar">
  3       <div>
  4         <p class="eyebrow">BAA Firefox 控制台</p>
  5-        <h1>管理页面</h1>
  6-      </div>
  7-      <div class="actions">
  8-        <button id="focus-controller-btn" type="button">聚焦本页</button>
  9-        <button id="open-claude-btn" type="button">打开 Claude</button>
 10-        <button id="open-chatgpt-btn" type="button">打开 ChatGPT</button>
 11-        <button id="open-gemini-btn" type="button">打开 Gemini</button>
 12-        <button id="reconnect-btn" type="button">启用可选 WS</button>
 13+        <h1>本地 WS / 远程 HTTP</h1>
 14+        <p class="meta">本地 bridge 自动连接 mini,远程控制面固定走 <code>https://conductor.makefile.so</code>。</p>
 15       </div>
 16     </section>
 17 
 18-    <section class="settings">
 19-      <label for="ws-url">可选 WS</label>
 20-      <input id="ws-url" type="text" spellcheck="false" placeholder="留空表示未启用">
 21-      <button id="save-ws-btn" type="button">保存 WS 配置</button>
 22-      <label for="control-base-url">控制 API</label>
 23-      <input id="control-base-url" type="text" spellcheck="false" placeholder="https://control-api.makefile.so">
 24-      <button id="save-control-btn" type="button">保存控制面配置</button>
 25-      <button id="refresh-control-btn" type="button">刷新控制面</button>
 26+    <section class="actions">
 27       <button id="pause-btn" type="button">暂停</button>
 28       <button id="resume-btn" type="button">恢复</button>
 29       <button id="drain-btn" type="button">排空</button>
 30@@ -37,87 +24,30 @@
 31 
 32     <section class="grid">
 33       <article class="card">
 34-        <p class="label">控制面连接</p>
 35-        <p id="control-mode" class="value off">未知</p>
 36-        <p id="control-meta" class="meta">未同步</p>
 37-      </article>
 38-
 39-      <article class="card">
 40-        <p class="label">主控</p>
 41-        <p id="leader-value" class="value off">-</p>
 42-        <p id="leader-meta" class="meta">租约: -</p>
 43-      </article>
 44-
 45-      <article class="card">
 46-        <p class="label">队列</p>
 47-        <p id="queue-value" class="value off">-</p>
 48-        <p id="queue-meta" class="meta">排队任务</p>
 49-      </article>
 50-
 51-      <article class="card">
 52-        <p class="label">活动运行</p>
 53-        <p id="runs-value" class="value off">-</p>
 54-        <p id="runs-meta" class="meta">控制面</p>
 55-      </article>
 56-
 57-      <article class="card">
 58-        <p class="label">可选 WS</p>
 59-        <p id="ws-status" class="value off">未启用</p>
 60-        <p id="client-id" class="meta">当前默认仅使用 Control API</p>
 61-      </article>
 62-
 63-      <article class="card">
 64-        <p class="label">跟踪标签页</p>
 65-        <p id="tab-status" class="value off">0 / 3</p>
 66-        <p id="tab-meta" class="meta">标签页: -</p>
 67-      </article>
 68-
 69-      <article class="card">
 70-        <p class="label">凭证</p>
 71-        <p id="cred-status" class="value off">0 / 3</p>
 72-        <p id="cred-meta" class="meta">请求头: 0</p>
 73+        <p class="label">本地 WS</p>
 74+        <p id="ws-status" class="value off">连接中</p>
 75+        <p id="ws-meta" class="meta">等待握手</p>
 76       </article>
 77 
 78       <article class="card">
 79-        <p class="label">端点</p>
 80-        <p id="endpoint-count" class="value">0</p>
 81-        <p class="meta">自动探测</p>
 82+        <p class="label">远程 HTTP API</p>
 83+        <p id="control-mode" class="value off">连接中</p>
 84+        <p id="control-meta" class="meta">等待同步</p>
 85       </article>
 86     </section>
 87 
 88     <section class="panel">
 89       <div class="panel-head">
 90-        <h2>控制面</h2>
 91-      </div>
 92-      <pre id="control-view" class="code"></pre>
 93-    </section>
 94-
 95-    <section class="panel">
 96-      <div class="panel-head">
 97-        <h2>平台状态</h2>
 98-      </div>
 99-      <pre id="platforms-view" class="code"></pre>
100-    </section>
101-
102-    <section class="panel">
103-      <div class="panel-head">
104-        <h2>最新请求头</h2>
105+        <h2>WS 状态</h2>
106       </div>
107-      <pre id="headers-view" class="code"></pre>
108+      <pre id="ws-view" class="code"></pre>
109     </section>
110 
111     <section class="panel">
112       <div class="panel-head">
113-        <h2>已发现端点</h2>
114+        <h2>HTTP 状态</h2>
115       </div>
116-      <pre id="endpoints-view" class="code"></pre>
117-    </section>
118-
119-    <section class="panel">
120-      <div class="panel-head">
121-        <h2>日志</h2>
122-      </div>
123-      <pre id="log-view" class="code"></pre>
124+      <pre id="control-view" class="code"></pre>
125     </section>
126   </main>
127 
M plugins/baa-firefox/controller.js
+333, -204
  1@@ -20,13 +20,10 @@ const CONTROLLER_STORAGE_KEYS = {
  2   geminiSendTemplate: "baaFirefox.geminiSendTemplate"
  3 };
  4 
  5-const DEFAULT_WS_URL = "";
  6-const DEFAULT_CONTROL_BASE_URL = "https://control-api.makefile.so";
  7-const LEGACY_DEFAULT_WS_URLS = new Set([
  8-  "ws://127.0.0.1:9800",
  9-  "ws://localhost:9800"
 10-]);
 11-const STATUS_SCHEMA_VERSION = 2;
 12+const DEFAULT_LOCAL_API_BASE = "http://127.0.0.1:4317";
 13+const DEFAULT_WS_URL = "ws://127.0.0.1:4317/ws/firefox";
 14+const DEFAULT_CONTROL_BASE_URL = "https://conductor.makefile.so";
 15+const STATUS_SCHEMA_VERSION = 3;
 16 const CREDENTIAL_SEND_INTERVAL = 30_000;
 17 const CREDENTIAL_TTL = 15 * 60_000;
 18 const NETWORK_BODY_LIMIT = 5000;
 19@@ -39,6 +36,7 @@ const CONTROL_RETRY_SLOW_INTERVAL = 30_000;
 20 const CONTROL_RETRY_LOG_INTERVAL = 60_000;
 21 const TRACKED_TAB_REFRESH_DELAY = 150;
 22 const CONTROL_STATUS_BODY_LIMIT = 12_000;
 23+const WS_RECONNECT_DELAY = 3_000;
 24 const CHATGPT_SESSION_COOKIE_PATTERNS = [
 25   /__secure-next-auth\.session-token=/i,
 26   /__secure-authjs\.session-token=/i,
 27@@ -156,6 +154,7 @@ const state = {
 28   wsUrl: DEFAULT_WS_URL,
 29   controlBaseUrl: DEFAULT_CONTROL_BASE_URL,
 30   controlState: null,
 31+  wsState: null,
 32   ws: null,
 33   wsConnected: false,
 34   reconnectTimer: null,
 35@@ -193,52 +192,18 @@ function trimTrailingSlash(value) {
 36 }
 37 
 38 function normalizeSavedControlBaseUrl(value) {
 39-  const normalized = trimTrailingSlash(value);
 40-
 41-  if (
 42-    !normalized ||
 43-    normalized === "http://127.0.0.1:9800" ||
 44-    normalized === "http://localhost:9800"
 45-  ) {
 46-    return DEFAULT_CONTROL_BASE_URL;
 47-  }
 48-
 49-  return normalized;
 50+  void value;
 51+  return DEFAULT_CONTROL_BASE_URL;
 52 }
 53 
 54 function normalizeSavedWsUrl(value) {
 55-  const normalized = trimTrailingSlash(value);
 56-
 57-  if (!normalized || LEGACY_DEFAULT_WS_URLS.has(normalized)) {
 58-    return DEFAULT_WS_URL;
 59-  }
 60-
 61-  try {
 62-    const parsed = new URL(normalized);
 63-    return parsed.protocol === "ws:" || parsed.protocol === "wss:"
 64-      ? normalized
 65-      : DEFAULT_WS_URL;
 66-  } catch (_) {
 67-    return DEFAULT_WS_URL;
 68-  }
 69+  void value;
 70+  return DEFAULT_WS_URL;
 71 }
 72 
 73 function deriveControlBaseUrl(wsUrl) {
 74-  const normalizedWsUrl = normalizeSavedWsUrl(wsUrl);
 75-  if (!normalizedWsUrl) {
 76-    return DEFAULT_CONTROL_BASE_URL;
 77-  }
 78-
 79-  try {
 80-    const parsed = new URL(normalizedWsUrl);
 81-    parsed.protocol = parsed.protocol === "wss:" ? "https:" : "http:";
 82-    const derived = trimTrailingSlash(parsed.toString());
 83-    return derived === "http://127.0.0.1:9800" || derived === "http://localhost:9800"
 84-      ? DEFAULT_CONTROL_BASE_URL
 85-      : derived;
 86-  } catch (_) {
 87-    return DEFAULT_CONTROL_BASE_URL;
 88-  }
 89+  void wsUrl;
 90+  return DEFAULT_CONTROL_BASE_URL;
 91 }
 92 
 93 function createDefaultControlState(overrides = {}) {
 94@@ -272,6 +237,41 @@ function cloneControlState(value) {
 95   };
 96 }
 97 
 98+function createDefaultWsState(overrides = {}) {
 99+  return {
100+    connection: "disconnected",
101+    wsUrl: DEFAULT_WS_URL,
102+    localApiBase: DEFAULT_LOCAL_API_BASE,
103+    clientId: null,
104+    protocol: null,
105+    version: null,
106+    serverIdentity: null,
107+    serverHost: null,
108+    serverRole: null,
109+    leaseState: null,
110+    clientCount: 0,
111+    retryCount: 0,
112+    nextRetryAt: 0,
113+    lastOpenAt: 0,
114+    lastMessageAt: 0,
115+    lastSnapshotAt: 0,
116+    lastCloseCode: null,
117+    lastCloseReason: null,
118+    lastError: null,
119+    lastSnapshotReason: null,
120+    raw: null,
121+    ...overrides
122+  };
123+}
124+
125+function cloneWsState(value) {
126+  if (!isRecord(value)) return createDefaultWsState();
127+  return {
128+    ...createDefaultWsState(),
129+    ...value
130+  };
131+}
132+
133 function createPlatformMap(factory) {
134   const out = {};
135   for (const platform of PLATFORM_ORDER) {
136@@ -484,7 +484,96 @@ function formatRetryDelay(targetTime) {
137 }
138 
139 function isWsEnabled() {
140-  return !!state.wsUrl;
141+  return true;
142+}
143+
144+function normalizeWsConnection(value) {
145+  switch (String(value || "").trim().toLowerCase()) {
146+    case "connecting":
147+    case "connected":
148+    case "retrying":
149+    case "disconnected":
150+      return String(value || "").trim().toLowerCase();
151+    default:
152+      return "disconnected";
153+  }
154+}
155+
156+function formatWsConnectionLabel(snapshot) {
157+  switch (normalizeWsConnection(snapshot?.connection)) {
158+    case "connecting":
159+      return "连接中";
160+    case "connected":
161+      return "已连接";
162+    case "retrying":
163+      return "重连中";
164+    default:
165+      return "已断开";
166+  }
167+}
168+
169+function wsConnectionClass(snapshot) {
170+  switch (normalizeWsConnection(snapshot?.connection)) {
171+    case "connected":
172+      return "on";
173+    case "connecting":
174+    case "retrying":
175+      return "warn";
176+    default:
177+      return "off";
178+  }
179+}
180+
181+function formatWsMeta(snapshot) {
182+  const parts = [];
183+
184+  if (snapshot.serverIdentity) {
185+    parts.push(`服务端: ${snapshot.serverIdentity}`);
186+  } else if (snapshot.connection === "connecting") {
187+    parts.push("等待本地 bridge 握手");
188+  }
189+
190+  if (snapshot.lastSnapshotAt > 0) {
191+    parts.push(`最近快照: ${formatSyncTime(snapshot.lastSnapshotAt)}`);
192+  }
193+
194+  if (snapshot.connection === "retrying" && snapshot.nextRetryAt > 0) {
195+    parts.push(`下次重连: ${formatRetryDelay(snapshot.nextRetryAt)}`);
196+  }
197+
198+  if (snapshot.lastError) {
199+    parts.push(`错误: ${snapshot.lastError}`);
200+  }
201+
202+  return parts.length > 0 ? parts.join(" · ") : "等待本地 bridge 握手";
203+}
204+
205+function renderWsSnapshot() {
206+  const snapshot = cloneWsState(state.wsState);
207+
208+  return JSON.stringify({
209+    connection: snapshot.connection,
210+    wsUrl: snapshot.wsUrl,
211+    localApiBase: snapshot.localApiBase,
212+    clientId: snapshot.clientId,
213+    protocol: snapshot.protocol,
214+    version: snapshot.version,
215+    serverIdentity: snapshot.serverIdentity,
216+    serverHost: snapshot.serverHost,
217+    serverRole: snapshot.serverRole,
218+    leaseState: snapshot.leaseState,
219+    clientCount: snapshot.clientCount,
220+    retryCount: snapshot.retryCount,
221+    nextRetryAt: snapshot.nextRetryAt ? new Date(snapshot.nextRetryAt).toISOString() : null,
222+    lastOpenAt: snapshot.lastOpenAt ? new Date(snapshot.lastOpenAt).toISOString() : null,
223+    lastMessageAt: snapshot.lastMessageAt ? new Date(snapshot.lastMessageAt).toISOString() : null,
224+    lastSnapshotAt: snapshot.lastSnapshotAt ? new Date(snapshot.lastSnapshotAt).toISOString() : null,
225+    lastSnapshotReason: snapshot.lastSnapshotReason,
226+    lastCloseCode: snapshot.lastCloseCode,
227+    lastCloseReason: snapshot.lastCloseReason,
228+    lastError: snapshot.lastError,
229+    raw: snapshot.raw
230+  }, null, 2);
231 }
232 
233 function normalizeControlConnection(value) {
234@@ -1198,6 +1287,7 @@ function renderControlSnapshot() {
235   const snapshot = cloneControlState(state.controlState);
236 
237   return JSON.stringify({
238+    baseUrl: state.controlBaseUrl,
239     ok: snapshot.ok,
240     controlConnection: snapshot.controlConnection,
241     retryCount: snapshot.retryCount,
242@@ -1219,57 +1309,28 @@ function renderControlSnapshot() {
243 }
244 
245 function render() {
246-  const trackedCount = getTrackedCount();
247-  const credentialCount = getCredentialCount();
248-  const totalEndpointCount = getTotalEndpointCount();
249+  const wsSnapshot = cloneWsState(state.wsState);
250   const controlSnapshot = cloneControlState(state.controlState);
251-  const wsEnabled = isWsEnabled();
252-
253-  ui.wsStatus.textContent = !wsEnabled ? "未启用" : state.wsConnected ? "已连接" : "未连接";
254-  ui.wsStatus.className = `value ${!wsEnabled ? "off" : state.wsConnected ? "on" : "warn"}`;
255-
256-  ui.tabStatus.textContent = `${trackedCount} / ${PLATFORM_ORDER.length}`;
257-  ui.tabStatus.className = `value ${trackedCount === 0 ? "off" : trackedCount === PLATFORM_ORDER.length ? "on" : "warn"}`;
258-  ui.tabMeta.textContent = trackedCount > 0
259-    ? PLATFORM_ORDER
260-        .filter((platform) => Number.isInteger(state.trackedTabs[platform]))
261-        .map((platform) => `${platformLabel(platform)}:${state.trackedTabs[platform]}`)
262-        .join(" | ")
263-    : "标签页: -";
264-
265-  ui.credStatus.textContent = `${credentialCount} / ${PLATFORM_ORDER.length}`;
266-  ui.credStatus.className = `value ${credentialCount === 0 ? "off" : credentialCount === PLATFORM_ORDER.length ? "on" : "warn"}`;
267-  ui.credMeta.textContent = `有效请求头: ${PLATFORM_ORDER.reduce((sum, platform) => {
268-    const credential = getCredentialState(platform);
269-    return sum + (credential.valid ? credential.headerCount : 0);
270-  }, 0)}`;
271-
272-  ui.endpointCount.textContent = String(totalEndpointCount);
273-  ui.endpointCount.className = `value ${totalEndpointCount > 0 ? "on" : "off"}`;
274-  ui.clientId.textContent = !wsEnabled
275-    ? "可选能力;当前默认仅使用 Control API"
276-    : `客户端: ${state.clientId || "-"}${state.wsConnected ? "" : " · 等待连接"}`;
277-  ui.controlMode.textContent = formatControlConnectionLabel(controlSnapshot);
278-  ui.controlMode.className = `value ${controlConnectionClass(controlSnapshot)}`;
279-  ui.controlMeta.textContent = formatControlMeta(controlSnapshot);
280-  ui.leaderValue.textContent = controlSnapshot.leader || "-";
281-  ui.leaderValue.className = `value ${controlSnapshot.leader ? "on" : "off"}`;
282-  ui.leaderMeta.textContent = `租约: ${controlSnapshot.leaseHolder || "-"}`;
283-  ui.queueValue.textContent = controlSnapshot.queueDepth == null ? "-" : String(controlSnapshot.queueDepth);
284-  ui.queueValue.className = `value ${controlSnapshot.queueDepth > 0 ? "warn" : controlSnapshot.queueDepth === 0 ? "on" : "off"}`;
285-  ui.queueMeta.textContent = "排队任务";
286-  ui.runsValue.textContent = controlSnapshot.activeRuns == null ? "-" : String(controlSnapshot.activeRuns);
287-  ui.runsValue.className = `value ${controlSnapshot.activeRuns > 0 ? "warn" : controlSnapshot.activeRuns === 0 ? "on" : "off"}`;
288-  ui.runsMeta.textContent = `自动化模式: ${formatModeLabel(controlSnapshot.mode)}`;
289-  ui.platformsView.textContent = renderPlatformStatus();
290-  ui.controlView.textContent = renderControlSnapshot();
291-  ui.headersView.textContent = renderHeaderSnapshot();
292-  ui.endpointsView.textContent = renderEndpointSnapshot();
293-  ui.logView.textContent = state.logs.length > 0
294-    ? state.logs.join("\n")
295-    : "还没有日志。";
296-  if (ui.reconnectBtn) {
297-    ui.reconnectBtn.textContent = wsEnabled ? "重连可选 WS" : "启用可选 WS";
298+
299+  if (ui.wsStatus) {
300+    ui.wsStatus.textContent = formatWsConnectionLabel(wsSnapshot);
301+    ui.wsStatus.className = `value ${wsConnectionClass(wsSnapshot)}`;
302+  }
303+  if (ui.wsMeta) {
304+    ui.wsMeta.textContent = formatWsMeta(wsSnapshot);
305+  }
306+  if (ui.controlMode) {
307+    ui.controlMode.textContent = formatControlConnectionLabel(controlSnapshot);
308+    ui.controlMode.className = `value ${controlConnectionClass(controlSnapshot)}`;
309+  }
310+  if (ui.controlMeta) {
311+    ui.controlMeta.textContent = formatControlMeta(controlSnapshot);
312+  }
313+  if (ui.wsView) {
314+    ui.wsView.textContent = renderWsSnapshot();
315+  }
316+  if (ui.controlView) {
317+    ui.controlView.textContent = renderControlSnapshot();
318   }
319 }
320 
321@@ -1477,15 +1538,34 @@ async function runControlPlaneAction(action, options = {}) {
322     throw new Error(`未知控制动作:${action || "-"}`);
323   }
324 
325+  const requestId = typeof crypto?.randomUUID === "function"
326+    ? crypto.randomUUID()
327+    : `req-${Date.now()}`;
328   const response = await requestControlPlane(`/v1/system/${methodName}`, {
329-    method: "POST"
330+    method: "POST",
331+    body: JSON.stringify({
332+      requested_by: "browser_admin",
333+      source: "firefox_extension",
334+      reason: `human_clicked_${methodName}`,
335+      request_id: requestId
336+    })
337   });
338 
339-  addLog("info", `控制动作 ${methodName} 已接受(${response.statusCode})`);
340+  const nextSnapshot = createControlSuccessState(response.payload, {
341+    ok: true,
342+    statusCode: response.statusCode,
343+    source: options.source || "http_action"
344+  }, state.controlState);
345+  await setControlState(nextSnapshot);
346+  resetControlFailureLog();
347+  restartControlPlaneRefreshTimer(CONTROL_REFRESH_INTERVAL, {
348+    reason: "poll"
349+  });
350+  addLog("info", `控制动作 ${methodName} 已执行(${response.statusCode})`, false);
351 
352   try {
353     await refreshControlPlaneState({
354-      source: options.source || "http",
355+      source: options.source || "http_action",
356       silent: true
357     });
358   } catch (_) {}
359@@ -1694,6 +1774,64 @@ function sendCredentialSnapshot(platform = null, force = false) {
360   }
361 }
362 
363+function setWsState(next) {
364+  state.wsState = cloneWsState(next);
365+  render();
366+}
367+
368+function handleWsHelloAck(message) {
369+  const previous = cloneWsState(state.wsState);
370+  const version = Number.isFinite(Number(message.version)) ? Number(message.version) : null;
371+
372+  setWsState({
373+    ...previous,
374+    connection: "connected",
375+    wsUrl: typeof message.wsUrl === "string" && message.wsUrl.trim() ? message.wsUrl.trim() : state.wsUrl,
376+    localApiBase: typeof message.localApiBase === "string" && message.localApiBase.trim()
377+      ? message.localApiBase.trim()
378+      : previous.localApiBase,
379+    clientId: typeof message.clientId === "string" && message.clientId.trim()
380+      ? message.clientId.trim()
381+      : (state.clientId || previous.clientId),
382+    protocol: typeof message.protocol === "string" && message.protocol.trim() ? message.protocol.trim() : null,
383+    version,
384+    retryCount: 0,
385+    nextRetryAt: 0,
386+    lastMessageAt: Date.now(),
387+    lastError: null,
388+    raw: truncateControlRaw(message)
389+  });
390+}
391+
392+function handleWsStateSnapshot(message) {
393+  const previous = cloneWsState(state.wsState);
394+  const snapshot = isRecord(message.snapshot) ? message.snapshot : {};
395+  const server = isRecord(snapshot.server) ? snapshot.server : {};
396+  const browserSnapshot = isRecord(snapshot.browser) ? snapshot.browser : {};
397+  const clientCount = normalizeCount(getFirstDefinedValue(browserSnapshot, ["client_count", "clients"]));
398+
399+  setWsState({
400+    ...previous,
401+    connection: "connected",
402+    wsUrl: typeof server.ws_url === "string" && server.ws_url.trim() ? server.ws_url.trim() : state.wsUrl,
403+    localApiBase: typeof server.local_api_base === "string" && server.local_api_base.trim()
404+      ? server.local_api_base.trim()
405+      : previous.localApiBase,
406+    serverIdentity: typeof server.identity === "string" && server.identity.trim() ? server.identity.trim() : null,
407+    serverHost: typeof server.host === "string" && server.host.trim() ? server.host.trim() : null,
408+    serverRole: typeof server.role === "string" && server.role.trim() ? server.role.trim() : null,
409+    leaseState: typeof server.lease_state === "string" && server.lease_state.trim() ? server.lease_state.trim() : null,
410+    clientCount: clientCount ?? previous.clientCount,
411+    retryCount: 0,
412+    nextRetryAt: 0,
413+    lastMessageAt: Date.now(),
414+    lastSnapshotAt: Date.now(),
415+    lastSnapshotReason: typeof message.reason === "string" && message.reason.trim() ? message.reason.trim() : null,
416+    lastError: null,
417+    raw: truncateControlRaw(snapshot)
418+  });
419+}
420+
421 function closeWsConnection() {
422   clearTimeout(state.reconnectTimer);
423 
424@@ -1713,32 +1851,56 @@ function closeWsConnection() {
425 
426 function connectWs(options = {}) {
427   const { silentWhenDisabled = false } = options;
428-
429   closeWsConnection();
430-  render();
431+  setWsState({
432+    ...cloneWsState(state.wsState),
433+    connection: "connecting",
434+    wsUrl: state.wsUrl,
435+    localApiBase: DEFAULT_LOCAL_API_BASE,
436+    clientId: state.clientId,
437+    nextRetryAt: 0,
438+    lastError: null
439+  });
440 
441   if (!isWsEnabled()) {
442     if (!silentWhenDisabled) {
443-      addLog("info", "可选 WS 未启用;当前默认仅使用 Control API", false);
444+      addLog("info", "本地 WS 已被禁用", false);
445     }
446     return;
447   }
448 
449-  addLog("info", `正在连接 WS:${state.wsUrl}`, false);
450+  addLog("info", `正在连接本地 WS:${state.wsUrl}`, false);
451 
452   try {
453     state.ws = new WebSocket(state.wsUrl);
454   } catch (error) {
455-    addLog("error", `WS 创建失败:${error.message}`, false);
456-    scheduleReconnect();
457+    const message = error instanceof Error ? error.message : String(error);
458+    setWsState({
459+      ...cloneWsState(state.wsState),
460+      connection: "disconnected",
461+      lastError: message
462+    });
463+    addLog("error", `本地 WS 创建失败:${message}`, false);
464+    scheduleReconnect(message);
465     return;
466   }
467 
468   state.ws.onopen = () => {
469     state.wsConnected = true;
470-    render();
471+    setWsState({
472+      ...cloneWsState(state.wsState),
473+      connection: "connected",
474+      wsUrl: state.wsUrl,
475+      localApiBase: DEFAULT_LOCAL_API_BASE,
476+      clientId: state.clientId,
477+      retryCount: 0,
478+      nextRetryAt: 0,
479+      lastOpenAt: Date.now(),
480+      lastMessageAt: Date.now(),
481+      lastError: null
482+    });
483     sendHello();
484-    addLog("info", "WS 已连接");
485+    addLog("info", "本地 WS 已连接", false);
486     sendCredentialSnapshot(null, true);
487     sendEndpointSnapshot();
488   };
489@@ -1753,7 +1915,26 @@ function connectWs(options = {}) {
490 
491     if (!message || typeof message !== "object") return;
492 
493+    setWsState({
494+      ...cloneWsState(state.wsState),
495+      lastMessageAt: Date.now(),
496+      lastError: null
497+    });
498+
499     switch (message.type) {
500+      case "hello_ack":
501+        handleWsHelloAck(message);
502+        break;
503+      case "state_snapshot":
504+        handleWsStateSnapshot(message);
505+        break;
506+      case "action_result":
507+        if (message.ok) {
508+          addLog("info", `本地 WS 动作确认:${message.action || "-"}`, false);
509+        } else {
510+          addLog("warn", `本地 WS 动作失败:${message.message || message.error || message.action || "-"}`, false);
511+        }
512+        break;
513       case "open_tab": {
514         const targets = getTargetPlatforms(message.platform);
515         for (const target of targets) {
516@@ -1770,6 +1951,15 @@ function connectWs(options = {}) {
517       case "request_credentials":
518         sendCredentialSnapshot(message.platform || null, true);
519         break;
520+      case "error":
521+        setWsState({
522+          ...cloneWsState(state.wsState),
523+          lastMessageAt: Date.now(),
524+          lastError: String(message.message || message.code || "ws_error"),
525+          raw: truncateControlRaw(message)
526+        });
527+        addLog("error", `本地 WS 返回错误:${message.message || message.code || "未知错误"}`, false);
528+        break;
529       case "reload":
530         addLog("warn", "收到重载命令");
531         window.location.reload();
532@@ -1781,24 +1971,42 @@ function connectWs(options = {}) {
533 
534   state.ws.onclose = (event) => {
535     state.wsConnected = false;
536-    render();
537-    addLog("warn", `WS 已关闭 code=${event.code || 0} reason=${event.reason || "-"}`, false);
538-    scheduleReconnect();
539+    setWsState({
540+      ...cloneWsState(state.wsState),
541+      connection: "disconnected",
542+      lastCloseCode: event.code || 0,
543+      lastCloseReason: event.reason || null,
544+      lastError: event.reason || `closed_${event.code || 0}`
545+    });
546+    addLog("warn", `本地 WS 已关闭 code=${event.code || 0} reason=${event.reason || "-"}`, false);
547+    scheduleReconnect(event.reason || `closed_${event.code || 0}`);
548   };
549 
550   state.ws.onerror = () => {
551     state.wsConnected = false;
552-    render();
553-    addLog("error", `WS 错误:${state.wsUrl}`, false);
554+    setWsState({
555+      ...cloneWsState(state.wsState),
556+      lastError: "连接错误"
557+    });
558+    addLog("error", `本地 WS 错误:${state.wsUrl}`, false);
559   };
560 }
561 
562-function scheduleReconnect() {
563+function scheduleReconnect(reason = null) {
564   clearTimeout(state.reconnectTimer);
565   if (!isWsEnabled()) return;
566+  const previous = cloneWsState(state.wsState);
567+  const nextRetryAt = Date.now() + WS_RECONNECT_DELAY;
568+  setWsState({
569+    ...previous,
570+    connection: "retrying",
571+    retryCount: (Number(previous.retryCount) || 0) + 1,
572+    nextRetryAt,
573+    lastError: reason || previous.lastError
574+  });
575   state.reconnectTimer = setTimeout(() => {
576     connectWs({ silentWhenDisabled: true });
577-  }, 3000);
578+  }, WS_RECONNECT_DELAY);
579 }
580 
581 async function resolveTrackedTab(platform) {
582@@ -2279,81 +2487,11 @@ function registerTabListeners() {
583 
584 function bindUi() {
585   ui.wsStatus = qs("ws-status");
586-  ui.tabStatus = qs("tab-status");
587-  ui.credStatus = qs("cred-status");
588-  ui.endpointCount = qs("endpoint-count");
589-  ui.clientId = qs("client-id");
590+  ui.wsMeta = qs("ws-meta");
591+  ui.wsView = qs("ws-view");
592   ui.controlMode = qs("control-mode");
593   ui.controlMeta = qs("control-meta");
594-  ui.leaderValue = qs("leader-value");
595-  ui.leaderMeta = qs("leader-meta");
596-  ui.queueValue = qs("queue-value");
597-  ui.queueMeta = qs("queue-meta");
598-  ui.runsValue = qs("runs-value");
599-  ui.runsMeta = qs("runs-meta");
600-  ui.tabMeta = qs("tab-meta");
601-  ui.credMeta = qs("cred-meta");
602   ui.controlView = qs("control-view");
603-  ui.platformsView = qs("platforms-view");
604-  ui.headersView = qs("headers-view");
605-  ui.endpointsView = qs("endpoints-view");
606-  ui.logView = qs("log-view");
607-  ui.wsUrl = qs("ws-url");
608-  ui.controlBaseUrl = qs("control-base-url");
609-  ui.reconnectBtn = qs("reconnect-btn");
610-
611-  for (const platform of PLATFORM_ORDER) {
612-    qs(`open-${platform}-btn`).addEventListener("click", () => {
613-      ensurePlatformTab(platform, { focus: true }).catch((error) => {
614-        addLog("error", `打开 ${platformLabel(platform)} 失败:${error.message}`);
615-      });
616-    });
617-  }
618-
619-  ui.reconnectBtn.addEventListener("click", () => {
620-    state.wsUrl = normalizeSavedWsUrl(ui.wsUrl.value);
621-    ui.wsUrl.value = state.wsUrl;
622-    persistState().catch(() => {});
623-    connectWs({ silentWhenDisabled: false });
624-  });
625-
626-  qs("save-ws-btn").addEventListener("click", () => {
627-    const previousWsUrl = state.wsUrl;
628-    const wasConnected = state.wsConnected;
629-
630-    state.wsUrl = normalizeSavedWsUrl(ui.wsUrl.value);
631-    ui.wsUrl.value = state.wsUrl;
632-    persistState().catch(() => {});
633-
634-    if (!isWsEnabled()) {
635-      closeWsConnection();
636-      addLog("info", "已禁用可选 WS;当前默认仅使用 Control API", false);
637-      render();
638-      return;
639-    }
640-
641-    if (previousWsUrl !== state.wsUrl) {
642-      closeWsConnection();
643-      addLog("info", `已保存可选 WS 地址:${state.wsUrl};点击“重连可选 WS”开始连接`, false);
644-      render();
645-      return;
646-    }
647-
648-    addLog("info", `已保存可选 WS 地址:${state.wsUrl}${wasConnected ? "" : ";点击“重连可选 WS”开始连接"}`, false);
649-    render();
650-  });
651-
652-  qs("save-control-btn").addEventListener("click", () => {
653-    state.controlBaseUrl = normalizeSavedControlBaseUrl(ui.controlBaseUrl.value);
654-    persistState().catch(() => {});
655-    addLog("info", `已保存控制 API:${state.controlBaseUrl}`, false);
656-    render();
657-    refreshControlPlaneState({ source: "manual", silent: false }).catch(() => {});
658-  });
659-
660-  qs("refresh-control-btn").addEventListener("click", () => {
661-    refreshControlPlaneState({ source: "manual", silent: false }).catch(() => {});
662-  });
663 
664   for (const action of ["pause", "resume", "drain"]) {
665     qs(`${action}-btn`).addEventListener("click", () => {
666@@ -2362,14 +2500,6 @@ function bindUi() {
667       });
668     });
669   }
670-
671-  qs("focus-controller-btn").addEventListener("click", async () => {
672-    const tab = await browser.tabs.getCurrent();
673-    await browser.tabs.update(tab.id, { active: true });
674-    if (tab.windowId != null) {
675-      await browser.windows.update(tab.windowId, { focused: true });
676-    }
677-  });
678 }
679 
680 async function init() {
681@@ -2417,9 +2547,12 @@ async function init() {
682     state.lastCredentialTabId = createPlatformMap(() => null);
683   }
684   state.lastCredentialHash = createPlatformMap((platform) => JSON.stringify(state.lastHeaders[platform]));
685-
686-  ui.wsUrl.value = state.wsUrl;
687-  ui.controlBaseUrl.value = state.controlBaseUrl;
688+  state.wsState = createDefaultWsState({
689+    connection: "connecting",
690+    wsUrl: state.wsUrl,
691+    localApiBase: DEFAULT_LOCAL_API_BASE,
692+    clientId: state.clientId
693+  });
694 
695   registerRuntimeListeners();
696   registerTabListeners();
697@@ -2435,11 +2568,7 @@ async function init() {
698     addLog("info", "已清理旧版平台状态缓存,等待新的真实请求重新建立凭证", false);
699   }
700 
701-  if (isWsEnabled()) {
702-    connectWs({ silentWhenDisabled: true });
703-  } else {
704-    addLog("info", "当前默认模式只使用 Control API;可选 WS 保持未启用", false);
705-  }
706+  connectWs({ silentWhenDisabled: true });
707   await prepareStartupControlState();
708   refreshControlPlaneState({ source: "startup", silent: true }).catch(() => {});
709 }
M plugins/baa-firefox/docs/conductor-control.md
+88, -62
  1@@ -1,84 +1,110 @@
  2 # Firefox Conductor Control
  3 
  4-`baa-firefox` 现在默认通过 Control API 自动连接 conductor control-plane,并在 Firefox 启动后自动恢复同步。
  5+`baa-firefox` 现在默认同时接两条固定链路:
  6+
  7+- 本地 WS bridge:`ws://127.0.0.1:4317/ws/firefox`
  8+- 远程 HTTP control API:`https://conductor.makefile.so`
  9+
 10+管理页已经收口,只保留:
 11+
 12+- 本地 WS 状态
 13+- 远程 HTTP 状态
 14+- `Pause` / `Resume` / `Drain` 按钮
 15+
 16+不再允许用户手工编辑 WS 地址或 HTTP 地址。
 17 
 18 ## 范围
 19 
 20 - `controller.html` / `controller.js`
 21-  - 配置 Control API base URL
 22-  - 启动后立即读取 `GET /v1/system/state`
 23-  - 失败时自动退避重试,成功后恢复常规轮询
 24+  - Firefox 启动后自动连接本地 `/ws/firefox`
 25+  - 启动后立即请求 `GET /v1/system/state`
 26+  - HTTP 失败时自动退避重试,成功后恢复常规轮询
 27   - 调用 `POST /v1/system/pause`
 28   - 调用 `POST /v1/system/resume`
 29   - 调用 `POST /v1/system/drain`
 30 - `background.js`
 31-  - Firefox 启动时确保 `controller.html` 存在
 32-  - 根据最新 control snapshot 更新扩展 badge
 33-- `content-script.js`
 34-  - 在 Claude 页面右下角提供最小浮层入口
 35+  - Firefox 启动时确保 `controller.html` 标签页存在
 36+  - 根据最新 HTTP control snapshot 更新扩展 badge
 37 
 38-## 配置
 39+## 固定地址
 40 
 41-控制页现在有两项控制面相关配置:
 42+- Local WS: `ws://127.0.0.1:4317/ws/firefox`
 43+- Remote HTTP: `https://conductor.makefile.so`
 44 
 45-- `Control API`
 46-  - 默认预填 `https://control-api.makefile.so`
 47-- `可选 WS`
 48-  - 默认留空
 49-  - 留空表示未启用
 50-  - 只有手动填写 `ws://` / `wss://` 地址后才会尝试连接
 51+当前实现不再从 UI 或 storage 读取用户自定义地址;旧配置会被固定地址覆盖。
 52 
 53-当前临时单节点模式默认不开启 Control API 鉴权,因此插件不会再要求填写 bearer token。
 54-当前默认模式也不再要求本地 `ws://127.0.0.1:9800` 或 `ws://localhost:9800`。
 55-如果 storage 中没有显式配置,插件仍然会默认回落到 `https://control-api.makefile.so`。
 56+## 默认连接行为
 57 
 58-状态快照会持久化到 `browser.storage.local` 的 `baaFirefox.controlState`,供:
 59+- Firefox 启动时,`background.js` 会确保 `controller.html` 标签页存在。
 60+- `controller.html` 启动后立即尝试连接本地 WS。
 61+- WS 断开后按固定间隔自动重连;本地服务重启后,连接会自动恢复。
 62+- `controller.html` 启动后也会立刻请求一次 `GET /v1/system/state`。
 63+- HTTP 成功后按 `15` 秒周期继续拉取状态。
 64+- HTTP 失败后按 `1` 秒、`3` 秒、`5` 秒快速重试,再切到每 `30` 秒一次的慢速重试。
 65+- 服务恢复后自动回到正常已连接状态,不需要手工刷新页面。
 66 
 67-- controller 面板渲染
 68-- toolbar badge 渲染
 69-- Claude 页面浮层渲染
 70+## WS 侧职责
 71 
 72-## 状态字段
 73+插件会向本地 WS 发送:
 74 
 75-插件会尽量从 `/v1/system/state` 响应中归一化这些字段:
 76+- `hello`
 77+- `credentials`
 78+- `api_endpoints`
 79+- `client_log`
 80 
 81-- `mode`
 82-- `leader`
 83-- `lease_holder`
 84-- `queue_depth` / `queued_tasks`
 85-- `active_runs`
 86-- `controlConnection`
 87-- `retryCount`
 88-- `lastSuccessAt`
 89-- `lastFailureAt`
 90-- `nextRetryAt`
 91+插件会消费服务端返回的:
 92 
 93-如果服务端实际字段名略有不同,`controller.js` 已做多路径兼容解析。
 94+- `hello_ack`
 95+- `state_snapshot`
 96+- `request_credentials`
 97+- `error`
 98 
 99-## 默认连接行为
100+管理页中的 WS 卡片和 WS 详情面板直接展示这条本地 bridge 的连接状态、最近快照和服务端元数据。
101 
102-- Firefox 启动时,`background.js` 会确保 `controller.html` 标签页存在。
103-- `controller.html` 启动后会立刻请求 `GET /v1/system/state`,不需要用户先点“刷新控制面”。
104-- 正常同步成功后,插件按 `15` 秒周期继续拉取 Control API。
105-- 如果 Control API 临时失败、服务端重启或网络抖动,插件会按 `1` 秒、`3` 秒、`5` 秒快速重试,再进入 `30` 秒慢速重试,不会停在一次性报错状态。
106-- 服务恢复后,插件会自动回到正常已连接状态,不需要手动刷新页面或重新打开 controller。
107-
108-## 可见入口
109-
110-- 扩展工具栏 badge
111-  - 已连接时显示自动化模式
112-  - 自动重试时显示重试状态
113-- `controller.html`
114-  - 第一张卡片主状态表示 Control API 连接状态
115-  - 会显示最近成功时间、最近失败和下一次重试
116-  - 保留“刷新控制面”按钮作为手动触发入口
117-  - 如果未配置 WS,则显示 `未启用`,不再把它当成主故障
118-- Claude 页面
119-  - 右下角 `BAA` 浮层,可直接 `Pause` / `Resume` / `Drain` 或打开 controller
120-
121-## 当前限制
122-
123-- 本次没有改 `manifest.json`
124-- 因此 Control API 需要落在当前扩展 CSP 允许的地址范围内
125-- 当前默认目标就是 `https://control-api.makefile.so`
126-- 可选 WS 能力仍然保留,但默认不启用,也不是主连接状态来源
127+## HTTP 侧职责
128+
129+HTTP 仍然是管理页里控制状态的同步来源,也是控制按钮的写入通道。
130+
131+读取:
132+
133+- `GET /v1/system/state`
134+
135+写入:
136+
137+- `POST /v1/system/pause`
138+- `POST /v1/system/resume`
139+- `POST /v1/system/drain`
140+
141+写接口会带固定请求体字段:
142+
143+```json
144+{
145+  "requested_by": "browser_admin",
146+  "source": "firefox_extension",
147+  "reason": "human_clicked_pause",
148+  "request_id": "uuid"
149+}
150+```
151+
152+## 管理页可见信息
153+
154+- WS 卡片:连接状态、最近快照、最近错误
155+- HTTP 卡片:连接状态、当前 mode、最近成功/失败、下次重试
156+- WS 详情:本地 bridge 的原始状态摘要
157+- HTTP 详情:control API 的原始状态摘要
158+
159+不再展示:
160+
161+- 地址输入框
162+- 手工重连按钮
163+- 手工刷新按钮
164+- 标签页/凭证/端点/日志面板
165+
166+## 验证建议
167+
168+1. 安装插件并启动 Firefox,确认 `controller.html` 自动打开。
169+2. 打开管理页,确认:
170+   - `本地 WS` 最终变成 `已连接`
171+   - `远程 HTTP API` 能显示 `已连接` 或自动重试中的明确状态
172+3. 停掉本地 `conductor-daemon`,确认 WS 状态进入重连中;恢复服务后确认自动回到 `已连接`。
173+4. 点击 `暂停` / `恢复` / `排空`,确认 HTTP 状态会更新 mode,且服务端状态与按钮动作一致。
M plugins/baa-firefox/manifest.json
+1, -4
 1@@ -21,15 +21,12 @@
 2     "https://oaiusercontent.com/*",
 3     "https://*.oaiusercontent.com/*",
 4     "https://gemini.google.com/*",
 5-    "https://control-api.makefile.so/*",
 6     "https://conductor.makefile.so/*",
 7-    "http://localhost/*",
 8     "http://127.0.0.1/*",
 9-    "ws://localhost/*",
10     "ws://127.0.0.1/*"
11   ],
12   "content_security_policy": {
13-    "extension_pages": "default-src 'self'; connect-src https://control-api.makefile.so https://conductor.makefile.so ws://localhost:9800 ws://127.0.0.1:9800 http://localhost:9800 http://127.0.0.1:9800"
14+    "extension_pages": "default-src 'self'; connect-src https://conductor.makefile.so ws://127.0.0.1:4317 http://127.0.0.1:4317"
15   },
16   "background": {
17     "scripts": [