im_wower
·
2026-04-02
firefox-local-ws.md
1# Browser Local WS
2
3`conductor-daemon` 现在在本地 HTTP listener 上正式支持 browser bridge WebSocket。
4
5目标:
6
7- 给本地 Firefox / Safari 插件一个正式、稳定、可重连的双向入口
8- 复用 `mini` 本地 control plane 的 system state / action write 能力
9- 不再把这条链路当成公网或 Cloudflare Worker 通道
10
11## 监听方式
12
13WS 地址直接由 `BAA_CONDUCTOR_LOCAL_API` 派生,不单独引入新的环境变量或第二个端口。
14
15例子:
16
17- `BAA_CONDUCTOR_LOCAL_API=http://100.71.210.78:4317` -> `ws://100.71.210.78:4317/ws/browser`
18- `BAA_CONDUCTOR_LOCAL_API=http://127.0.0.1:4317` -> `ws://127.0.0.1:4317/ws/browser`
19
20约束:
21
22- canonical path 是 `/ws/browser`
23- `/ws/firefox` 仍保留为兼容路径
24- 只应监听 loopback 或显式允许的 Tailscale `100.x` 地址
25- 不是公网入口
26- 当前正式浏览器代发 HTTP 面只支持 `claude`;但 `GET /v1/browser` 的元数据读面可以返回所有已上报 `platform`
27
28## 自动重连语义
29
30- browser 客户端断开后可以直接重连到同一个 URL
31- server 会接受新的连接并继续推送最新 snapshot
32- 如果同一个 `clientId` 建立了新连接,旧连接会被替换
33- `state_snapshot` 会在 `hello`、browser metadata 变化、system state 变化时重新推送
34
35## 消息模型
36
37### client -> server
38
39| `type` | 说明 |
40| --- | --- |
41| `hello` | 注册当前 browser client,声明 `clientId`、`nodeType`、`nodeCategory`、`nodePlatform` |
42| `state_request` | 主动请求最新 snapshot |
43| `action_request` | 请求执行 `pause` / `resume` / `drain` |
44| `action_result` | 回传 browser/plugin 管理动作的结构化执行结果;带 `accepted` / `completed` / `failed` / `reason` / `result` / `shell_runtime` |
45| `credentials` | 上送账号、凭证指纹、新鲜度和脱敏 header 名称摘要;server 只持久化最小元数据 |
46| `api_endpoints` | 上送当前可代发的 endpoint 列表及其 `endpoint_metadata` |
47| `browser.final_message` | 上送 ChatGPT / Gemini 最终 assistant message 的 raw relay;只传完整最终文本,不在插件里做 parser |
48| `api_response` | 对服务端下发的 `api_request` 回包,按 `id` 做 request-response 关联 |
49| `stream_open` | 对服务端下发的 SSE `api_request` 回传流已打开,附带 `stream_id` 和上游状态码 |
50| `stream_event` | 回传单个流事件;每条都带递增 `seq` |
51| `stream_end` | 回传流已正常结束 |
52| `stream_error` | 回传流失败、超时、取消或本地执行错误 |
53| `client_log` | 可选日志消息;当前 server 只接收,不做业务处理 |
54
55### server -> client
56
57| `type` | 说明 |
58| --- | --- |
59| `hello_ack` | 握手确认,回传 protocol/version、WS URL、支持的消息类型 |
60| `state_snapshot` | 当前 server/system/browser 摘要 |
61| `action_result` | `action_request` 的执行结果;成功时直接回传最新 `system` |
62| `open_tab` | 指示浏览器打开或激活目标平台标签页 |
63| `plugin_status` / `ws_reconnect` / `controller_reload` / `tab_restore` | 正式插件管理动作;server 直接用动作名下发消息 |
64| `api_request` | 由 server 发起、浏览器代发的 API 请求;buffered 模式回 `api_response`,SSE 模式回 `stream_*` |
65| `request_cancel` | 请求浏览器取消当前 `api_request` 或流 |
66| `request_credentials` | 提示浏览器重新发送 `credentials` |
67| `reload` | legacy 重载消息;不带 `platform` 时等价 `controller_reload`,带 `platform` 时等价 `tab_reload` |
68| `error` | 非法 JSON、未知消息类型或未实现消息 |
69
70## 关键 payload
71
72### `hello`
73
74```json
75{
76 "type": "hello",
77 "clientId": "safari-ab12cd",
78 "nodeType": "browser",
79 "nodeCategory": "proxy",
80 "nodePlatform": "safari"
81}
82```
83
84### `hello_ack`
85
86```json
87{
88 "type": "hello_ack",
89 "clientId": "safari-ab12cd",
90 "protocol": "baa.browser.local",
91 "protocol_compat": ["baa.firefox.local"],
92 "version": 1,
93 "wsUrl": "ws://100.71.210.78:4317/ws/browser",
94 "wsCompatUrls": ["ws://100.71.210.78:4317/ws/firefox"],
95 "localApiBase": "http://100.71.210.78:4317",
96 "supports": {
97 "inbound": ["hello", "state_request", "action_request", "action_result", "credentials", "api_endpoints", "client_log", "browser.final_message", "api_response", "stream_open", "stream_event", "stream_end", "stream_error"],
98 "outbound": ["hello_ack", "state_snapshot", "action_result", "open_tab", "plugin_status", "ws_reconnect", "controller_reload", "tab_restore", "api_request", "request_cancel", "request_credentials", "reload", "error"]
99 }
100}
101```
102
103### `state_snapshot`
104
105```json
106{
107 "type": "state_snapshot",
108 "reason": "hello",
109 "snapshot": {
110 "version": 1,
111 "server": {
112 "identity": "mini-main@mini(primary)",
113 "local_api_base": "http://100.71.210.78:4317",
114 "ws_path": "/ws/browser",
115 "ws_url": "ws://100.71.210.78:4317/ws/browser"
116 },
117 "system": {
118 "mode": "running",
119 "automation": {
120 "mode": "running"
121 },
122 "leader": {
123 "controller_id": "mini-main"
124 },
125 "queue": {
126 "active_runs": 0,
127 "queued_tasks": 0
128 }
129 },
130 "browser": {
131 "automation_conversations": [
132 {
133 "platform": "chatgpt",
134 "remote_conversation_id": "conv_overlay_demo",
135 "local_conversation_id": "lc_overlay_demo",
136 "automation_status": "paused",
137 "last_non_paused_automation_status": "auto",
138 "pause_reason": "repeated_message",
139 "active_link": {
140 "page_url": "https://chatgpt.com/c/conv_overlay_demo",
141 "page_title": "ChatGPT Overlay Demo"
142 }
143 }
144 ],
145 "client_count": 1,
146 "clients": [
147 {
148 "client_id": "safari-ab12cd",
149 "node_platform": "safari",
150 "credentials": [],
151 "final_messages": [],
152 "request_hooks": []
153 }
154 ]
155 }
156 }
157}
158```
159
160说明:
161
162- `snapshot.system` 直接复用 `GET /v1/system/state` 的合同
163- `snapshot.browser.clients[].credentials` 只回传 `account`、`credential_fingerprint`、`freshness`、`header_count` 和时间戳
164- `snapshot.browser.clients[].final_messages` 只保留当前活跃 bridge client 最近观测到的最终消息,不写入当前持久化表
165- `snapshot.browser.automation_conversations` 会按 active link 暴露当前页面/对话的 `automation_status`、`pause_reason`、`local_conversation_id` 和 `remote_conversation_id`,供 Firefox / Safari 浮层同步统一自动化状态
166- `snapshot.browser.instruction_ingest` 暴露 live ingest / execute 的持久化读面:
167 - `last_ingest`
168 - `last_execute`
169 - `recent_ingests`
170 - `recent_executes`
171- `snapshot.browser.instruction_ingest.*.status` 常见值包括:
172 - `executed`
173 - `duplicate_message`
174 - `automation_busy`
175 - `automation_paused`
176 - `system_paused`
177- 当 `snapshot.system.mode === "paused"` 时,普通 BAA 指令会写成 `system_paused`;system 状态本身仍通过 `snapshot.system` 单独同步
178- 上述摘要历史来自 conductor 本地有界 journal;进程重启后仍可恢复
179- `snapshot.browser.clients[].request_hooks` 只回传 endpoint 列表、`endpoint_metadata` 和更新时间
180
181### `action_request`
182
183```json
184{
185 "type": "action_request",
186 "requestId": "req-pause-1",
187 "action": "pause",
188 "requestedBy": "browser_admin",
189 "reason": "human_clicked_pause",
190 "source": "firefox_extension_ws"
191}
192```
193
194`action` 当前只允许:
195
196- `pause`
197- `resume`
198- `drain`
199
200成功响应:
201
202```json
203{
204 "type": "action_result",
205 "requestId": "req-pause-1",
206 "action": "pause",
207 "ok": true,
208 "system": {
209 "mode": "paused"
210 }
211}
212```
213
214### browser / plugin `action_result`
215
216当 server 通过 `open_tab` / `plugin_status` / `request_credentials` / `reload` / `tab_restore` 等正式管理消息下发动作后,浏览器会回传:
217
218```json
219{
220 "type": "action_result",
221 "requestId": "action-browser-1",
222 "action": "tab_restore",
223 "command_type": "tab_restore",
224 "accepted": true,
225 "completed": true,
226 "failed": false,
227 "reason": null,
228 "target": {
229 "platform": "claude",
230 "requested_platform": "claude"
231 },
232 "result": {
233 "platform_count": 1,
234 "ok_count": 1,
235 "failed_count": 0,
236 "restored_count": 1,
237 "desired_count": 1,
238 "actual_count": 1,
239 "drift_count": 0,
240 "skipped_reasons": []
241 },
242 "results": [
243 {
244 "platform": "claude",
245 "ok": true,
246 "delivery_ack": {
247 "level": 1,
248 "status_code": 200,
249 "failed": false,
250 "reason": null,
251 "confirmed_at": 1760000012500
252 },
253 "restored": true,
254 "tab_id": 321,
255 "shell_runtime": {
256 "platform": "claude",
257 "desired": { "exists": true },
258 "actual": { "exists": true, "tab_id": 321 },
259 "drift": { "aligned": true, "needs_restore": false, "unexpected_actual": false, "reason": "aligned" }
260 }
261 }
262 ],
263 "shell_runtime": [
264 {
265 "platform": "claude",
266 "desired": { "exists": true },
267 "actual": { "exists": true, "tab_id": 321 },
268 "drift": { "aligned": true, "needs_restore": false, "unexpected_actual": false, "reason": "aligned" }
269 }
270 ]
271}
272```
273
274补充:
275
276- `browser.inject_message` / `browser.send_message` 这类 delivery 动作也会复用同一结构化 `action_result`
277- `browser.proxy_delivery` 现在会在 `results[*].delivery_ack` 里补充下游确认层级;首版固定回传 Level 1 HTTP 状态码,不在 proxy 内等待完整 SSE 结束
278- 当插件侧 delivery adapter fail-closed 时,`reason` 会带稳定前缀 `delivery.<code>:`,例如 `delivery.page_not_ready:`、`delivery.selector_missing:`、`delivery.send_not_confirmed:`
279- 这类失败表示浏览器没有确认 inject / send 已完成,server 不应把该轮交付误记为成功
280
281### `browser.final_message`
282
283```json
284{
285 "type": "browser.final_message",
286 "platform": "chatgpt",
287 "conversation_id": "conv_demo",
288 "assistant_message_id": "msg_demo",
289 "raw_text": "@conductor::describe",
290 "observed_at": 1760000012000
291}
292```
293
294约束:
295
296- `raw_text` 必须是完整最终文本,不是 stream chunk
297- 插件必须在 streaming 完成后再发送,不得在半截 stream 提前上报
298- `assistant_message_id` 允许退化为平台内等价稳定字段,但最终仍统一放进同名字段
299- `conversation_id` 允许为空
300- 当前 server 会把 live `browser.final_message` 直接送入 conductor 侧 instruction center
301- 若消息不含 ` ```baa `,会被安全忽略
302- 当前只允许 Phase 1 精确 target:
303 - `conductor`
304 - `system`
305- 当前 server 已把 live `browser.final_message` 执行结果接到 text-only `inject / send`
306- 但插件侧 `inject / send` 仍是 DOM heuristic,当前只对 `Claude` / `ChatGPT` 做了首版选择器与流程
307- 超长文本当前默认只保留前 `200` 行,并在末尾追加 `超长截断`
308- 当前交付仍按任务边界停留在单客户端、单轮 delivery 首版
309
310server 行为:
311
312- 把最新 final message 去重后写入当前 client 的最近快照
313- 以 `platform + assistant_message_id + raw_text` 做 live message replay 抑制
314- 把持久化的 ingest / execute 最近历史暴露到:
315 - `state_snapshot.snapshot.browser.instruction_ingest`
316 - `GET /v1/browser`
317
318### `credentials`
319
320```json
321{
322 "type": "credentials",
323 "platform": "claude",
324 "account": "user@example.com",
325 "credential_fingerprint": "fp-claude-demo",
326 "freshness": "fresh",
327 "captured_at": 1760000000000,
328 "last_seen_at": 1760000005000,
329 "headers": {
330 "cookie": "<redacted>",
331 "x-csrf-token": "<redacted>",
332 "anthropic-client-version": "<redacted>"
333 },
334 "timestamp": 1760000000000
335}
336```
337
338server 行为:
339
340- 把 `account`、`credential_fingerprint`、`freshness`、`captured_at`、`last_seen_at` 写入持久化登录态记录
341- `headers` 只用于保留名称和数量;这些值应当已经是脱敏占位符
342- `shell_runtime` 会并入当前活跃 client 的内存 runtime 视图,并透传到 `GET /v1/browser`
343- 不在 `state_snapshot` 或 `GET /v1/browser` 中回显原始 `cookie` / `token` / header 值
344
345### `api_endpoints`
346
347```json
348{
349 "type": "api_endpoints",
350 "platform": "claude",
351 "account": "user@example.com",
352 "credential_fingerprint": "fp-claude-demo",
353 "updated_at": 1760000008000,
354 "endpoints": [
355 "GET /api/organizations",
356 "POST /api/organizations/{id}/chat_conversations/{id}/completion"
357 ],
358 "endpoint_metadata": [
359 {
360 "method": "GET",
361 "path": "/api/organizations",
362 "first_seen_at": 1760000001000,
363 "last_seen_at": 1760000008000
364 }
365 ]
366}
367```
368
369server 行为:
370
371- 把 endpoint 列表和 `endpoint_metadata` 写入持久化端点记录
372- 如果 payload 带 `shell_runtime`,会同步刷新该平台的当前 runtime 视图
373- `GET /v1/browser` 会把这些持久化记录和当前活跃 WS 连接视图合并
374- client 断开或 daemon 重启后,最近一次元数据仍可通过 `/v1/browser` 读取
375
376### `api_request`
377
378buffered 请求示例:
379
380```json
381{
382 "type": "api_request",
383 "id": "browser-request-1",
384 "platform": "claude",
385 "method": "GET",
386 "path": "/api/organizations",
387 "response_mode": "buffered"
388}
389```
390
391SSE 请求示例:
392
393```json
394{
395 "type": "api_request",
396 "id": "browser-stream-1",
397 "platform": "claude",
398 "method": "POST",
399 "path": "/api/organizations/org-1/chat_conversations/conv-1/completion",
400 "body": {
401 "prompt": "Summarize the current bridge state."
402 },
403 "response_mode": "sse",
404 "stream_id": "browser-stream-1"
405}
406```
407
408说明:
409
410- `response_mode=buffered` 时浏览器仍走 `api_response`
411- `response_mode=sse` 时浏览器必须改走 `stream_open` / `stream_event` / `stream_end` / `stream_error`
412- `stream_id` 首版默认与 `id` 保持一致
413
414### `request_cancel`
415
416```json
417{
418 "type": "request_cancel",
419 "id": "browser-stream-1",
420 "stream_id": "browser-stream-1",
421 "reason": "browser_request_cancelled"
422}
423```
424
425说明:
426
427- 浏览器收到后应尽快中止本地 fetch / SSE reader
428- 如果流已经开始,推荐回 `stream_error`
429- 如果是 buffered 请求,本地请求 promise 应尽快失败并释放占位
430
431### `stream_open`
432
433```json
434{
435 "type": "stream_open",
436 "id": "browser-stream-1",
437 "stream_id": "browser-stream-1",
438 "status": 200,
439 "meta": {
440 "method": "POST",
441 "platform": "claude",
442 "url": "https://claude.ai/api/organizations/org-1/chat_conversations/conv-1/completion"
443 }
444}
445```
446
447### `stream_event`
448
449```json
450{
451 "type": "stream_event",
452 "id": "browser-stream-1",
453 "stream_id": "browser-stream-1",
454 "seq": 1,
455 "event": "message",
456 "data": {
457 "type": "content_block_delta"
458 },
459 "raw": "event: message\ndata: {\"type\":\"content_block_delta\"}"
460}
461```
462
463### `stream_end`
464
465```json
466{
467 "type": "stream_end",
468 "id": "browser-stream-1",
469 "stream_id": "browser-stream-1",
470 "status": 200
471}
472```
473
474### `stream_error`
475
476```json
477{
478 "type": "stream_error",
479 "id": "browser-stream-1",
480 "stream_id": "browser-stream-1",
481 "status": 499,
482 "code": "request_cancelled",
483 "message": "browser_request_cancelled"
484}
485```
486
487### `open_tab`
488
489```json
490{
491 "type": "open_tab",
492 "requestId": "action-browser-1",
493 "action": "tab_open",
494 "platform": "claude"
495}
496```
497
498说明:
499
500- 可显式指定 `clientId` 目标;未指定时 server 会选最近活跃的 browser client
501- 当前插件按 `platform` 激活或拉起对应页面
502
503### `request_credentials`
504
505```json
506{
507 "type": "request_credentials",
508 "requestId": "action-browser-2",
509 "action": "request_credentials",
510 "platform": "claude",
511 "reason": "hello"
512}
513```
514
515说明:
516
517- 插件收到后会重新上送对应平台的 `credentials`
518- `platform` 可省略,表示尽量刷新全部已知平台的凭证快照
519
520### `reload`
521
522```json
523{
524 "type": "reload",
525 "requestId": "action-browser-3",
526 "action": "controller_reload",
527 "reason": "operator_requested_reload"
528}
529```
530
531### `api_request` / `api_response`
532
533服务端下发:
534
535```json
536{
537 "type": "api_request",
538 "id": "req-browser-1",
539 "platform": "claude",
540 "method": "POST",
541 "path": "/api/organizations/org-demo/chat_conversations/conv-demo/completion",
542 "headers": {
543 "x-csrf-token": "..."
544 },
545 "body": {
546 "prompt": "hello from conductor"
547 }
548}
549```
550
551浏览器回包:
552
553```json
554{
555 "type": "api_response",
556 "id": "req-browser-1",
557 "ok": true,
558 "status": 200,
559 "body": {
560 "conversation_id": "abc"
561 },
562 "error": null
563}
564```
565
566请求生命周期:
567
568- `conductor-daemon` 为每个 `api_request` 绑定唯一 `id`
569- 可按显式 `clientId` 投递,也可默认投递到最近活跃 client
570- 回包必须带同一个 `id`,由 bridge broker 完成 request-response 关联
571- 若 client 超时未回、连接断开,或同 `clientId` 被新连接替换,等待中的请求会立即失败
572
573当前非目标:
574
575- 不把 `/ws/browser` 或兼容路径 `/ws/firefox` 直接暴露成公网产品接口
576- 不把页面对话 UI、聊天 DOM 自动化或多标签会话编排写成正式 bridge 能力
577- 正式 HTTP 面已经收口到 `GET /v1/browser`、`POST /v1/browser/actions`、`POST /v1/browser/request`、`POST /v1/browser/request/cancel`;`/v1/browser/claude/*` 只保留 legacy 包装与 Claude 辅助读
578- 本文仍只讨论 WS transport、client registry 和 request-response 基础能力
579
580## 最小 smoke
581
582### WS 握手 smoke
583
584```bash
585LOCAL_API_BASE="${BAA_CONDUCTOR_LOCAL_API:-http://100.71.210.78:4317}"
586WS_URL="$(node --input-type=module -e 'const u = new URL(process.argv[1]); u.protocol = u.protocol === \"https:\" ? \"wss:\" : \"ws:\"; u.pathname = \"/ws/browser\"; u.search = \"\"; u.hash = \"\"; console.log(u.toString());' "$LOCAL_API_BASE")"
587
588WS_URL="$WS_URL" node --input-type=module <<'EOF'
589const socket = new WebSocket(process.env.WS_URL);
590
591socket.addEventListener("open", () => {
592 socket.send(JSON.stringify({
593 type: "hello",
594 clientId: "safari-smoke-client",
595 nodeType: "browser",
596 nodeCategory: "proxy",
597 nodePlatform: "safari"
598 }));
599
600 socket.send(JSON.stringify({
601 type: "state_request"
602 }));
603});
604
605socket.addEventListener("message", (event) => {
606 console.log(event.data);
607});
608EOF
609```
610
611### 端到端 Claude HTTP smoke
612
613如果要验证 `conductor HTTP -> /ws/browser -> Claude 页面内 HTTP 代理` 这条最小闭环,直接运行:
614
615```bash
616./scripts/runtime/browser-control-e2e-smoke.sh
617```
618
619这条 smoke 会覆盖:
620
621- `GET /v1/browser` 上的元数据上报与合并读面
622- `GET /v1/browser` 持久化记录在断连和重启后的可读性
623- `GET /v1/browser` 上 `fresh` / `stale` / `lost` 的状态变化
624- `GET /v1/browser` 不回显原始 `cookie` / `token` / header 值
625- `POST /v1/browser/actions`
626- `POST /v1/browser/request`
627- `POST /v1/browser/request/cancel`
628- 正式 SSE 与 Claude legacy wrapper
629- 以及 `/ws/browser` 上的 `open_tab`、`request_cancel`、`api_request` / `api_response` / `stream_*`