--- name: atomk-three-layer-arch description: AtomK三层架构 (Server/Bridge/Desktop) — 鉴权、注册、心跳、断线切换 version: 1.0.0 --- # AtomK 三层架构 (Server / Bridge / Desktop) ## 架构概览 ``` Desktop ──登录──→ Server (拿JWT + bridge列表+key) Desktop ──WS连接──→ Bridge (用Server推下来的key认证) Bridge ──注册───→ Server (X-API-Key = 共享admin密钥,首次→pending→admin审批→online) Bridge ──心跳───→ Server (X-Bridge-Key = bridge自身key) Server ──心跳响应──→ Bridge (下发指令: update_key/drain/kick) Desktop ──断线──→ 自动切下一个Bridge(或重试) ``` ## 三层职责 | 层 | 项目 | 域名/路径 | 职责 | |---|---|---|---| | Server | Atomlisting_Server | atomlisting.com (1主+1~2 fallback) | 鉴权中心、Bridge注册表、用户管理 | | Bridge | atomk-page-bridge | VPS上运行,可多实例按region部署 | CDP代理转发、Desktop连接管理 | | Desktop | AtomK-Desktop | /home/ubuntu/hermes-desktop/ | Electron桌面应用,用户入口 | ## 安全模型 (v4.0) - **Server** = 唯一鉴权中心,签发JWT、分配key、管理bridge注册 - **Bridge → Server 注册** = `X-API-Key` header(共享密钥,匹配 API_KEY_ADMIN 或 BRIDGE_SECRET 或默认值 `bingo301Tt23456Admin!`) - **Bridge → Server 心跳** = `X-Bridge-Key` header(bridge自身的key,注册时获得或自带的 `--key`) - **Desktop → Bridge** = Server 分配的 `key`(WS auth)或 Server 签发的 `JWT`(HTTP Bearer) - **Bridge** = 不存用户数据,只做CDP代理转发 ## Bridge 生命周期 (v4.0 审批流) 1. **启动注册**: Bridge 带 `X-API-Key` 调用 `/register` → Server 创建记录,`status=pending` 2. **等待审批**: Admin 通过 `PUT /bridges/{id}` 设置 `status=online` 审批通过 3. **心跳运行**: Bridge 定时带 `X-Bridge-Key` 调用 `/heartbeat` 上报连接数 4. **下线**: Admin 设 `is_active=false` 或 Bridge 停机超时 → `status=offline` 5. **重新注册**: 同一 key 再次 `/register` → 自动 `status=online`(无需再审批) > ⚠️ Desktop `GET /bridges` 只返回 `status=online` 的 bridge,pending/offline 不可见 ## Server 端 API | 端点 | 方向 | 认证 | 说明 | |------|------|------|------| | `POST /api/v1/auth/login` | Desktop → Server | 账号密码 | 登录获取JWT | | `POST /api/v1/bridges/register` | Bridge → Server | X-API-Key | 启动时注册,返回 key + jwt_secret,首次 status=pending | | `POST /api/v1/bridges/heartbeat` | Bridge → Server | X-Bridge-Key | 心跳上报,响应含 commands + status | | `GET /api/v1/bridges` | Desktop → Server | JWT | 获取bridge列表+key | | `POST /api/v1/bridges/approve` | Admin → Server | X-API-Key (admin级) | 批准pending bridge,分配用户 | | `POST /api/v1/bridges/reject` | Admin → Server | X-API-Key (admin级) | 拒绝/删除pending bridge | | `PUT /api/v1/bridges/{bridge_id}` | Admin → Server | JWT(admin) | 更新bridge设置(旧端点,与/approve重复) | | `DELETE /api/v1/bridges/{bridge_id}` | Admin → Server | JWT(admin) | 停用bridge | ## 关键代码文件 ### Server (Atomlisting_Server) - `backend/app/models/bridge.py` — Bridge数据库模型 - `backend/app/schemas/bridge.py` — Pydantic schemas (含 BridgeCommand),路由文件必须导入此文件而非内联 - `backend/app/api/v1/bridges.py` — Bridge API路由 - `backend/app/config.py` — BRIDGE_SECRET、API_KEY_READ/WRITE/ADMIN 配置项(全部从环境变量读取,默认空串) - `backend/app/core/security.py` — X-API-Key 校验逻辑,空串 key 会导致 `""` 成为一个有效 entry - `backend/app/api/v1/auth.py` — 登录 endpoint(⚠️ last_used_bridge_id 从 remote_user 读取而非本地 DB,已知 bug) - `backend/app/models/user.py` — User 模型(⚠️ last_used_bridge_id 缺 Alembic 迁移) - `backend/alembic/versions/add_bridges_table.py` — Bridges 迁移 - Deploy: bt109atomk.sh3.ikuai7.com, frontend:8091, backend:8092, MCP:8093 - Start scripts: `/root/AtomK_Operation_Tools/start_backend.sh`, `start_frontend.sh` ### Bridge (atomk-page-bridge) - `cloud-bridge/server.py` — 主服务,含注册/心跳/JWT/commands处理 - 启动参数: `--server-url`, `--bridge-secret`, `--bridge-name`, `--bridge-region`, `--key`, `--jwt-secret`(可选,优先从Server获取) ### Desktop (hermes-desktop) - `src/main/operation-api.ts` — fetchBridges() 函数 - `src/main/chrome-bridge.ts` — setBridgeList(), tryNextBridge(), 断线自动切换逻辑 - `src/main/index.ts` — IPC handlers (operation:fetch-bridges, chrome-bridge:set-bridge-list, chrome-bridge:try-next-bridge) - `src/preload/index.ts` — chromeBridgeSetBridgeList(), chromeBridgeTryNext() - `src/renderer/src/screens/ChromeBridge/ChromeBridge.tsx` — Bridge选择器UI ## Bridge v4.0 部署流程 Bridge 是**云端服务器组件**,部署在独立的 VPS 上(按地域可多实例),**不在** Server (atomlisting.com) 上运行。Desktop 通过 WebSocket 远程连 Bridge,Bridge 向 Server 注册。 ### 新建部署步骤 ```bash # 1. 克隆 cd /root GIT_SSL_NO_VERIFY=1 git clone https://gitea9webs.sh3.ikuai7.com/admin9webs/atomk-page-bridge.git cd atomk-page-bridge/cloud-bridge # 2. 启动(首次) python3 server.py \\ --key "<随机生成的bridge自身key>" \\ --api-key "bingo301Tt23456Admin!" \\ --server-url "https://www.atomlisting.com" \\ --bridge-name "US-West-1" \\ --bridge-region "us" \\ --ping-timeout 60 # 3. 生产环境用 systemd sudo cp templates/atomk-bridge.service /etc/systemd/system/ sudo vim /etc/atomk-bridge.env # 填入 ATOMK_BRIDGE_KEY, ATOMK_BRIDGE_NAME 等 sudo systemctl daemon-reload sudo systemctl enable --now atomk-bridge.service # 4. 验证 curl -s http://127.0.0.1:9228/cloud-bridge/health -H "Authorization: Bearer " # → {"status":"ok","version":"4.0.0",...} ``` ### 注册 → 审批 → 心跳流程 ``` Bridge 启动 → POST /api/v1/bridges/register (X-API-Key) → status=pending 管理员登录 atomlisting.com → /bridges 页面 → 批准 + 分配用户 Bridge → POST /api/v1/bridges/heartbeat (X-Bridge-Key) 每30s Desktop → GET /api/v1/bridges/ (JWT) → 看到自己的 bridge 列表 ``` > ⚠️ 新 Bridge 必须等 admin 批准后 Desktop 才能看到;也可通过 API 批准: > `POST /api/v1/bridges/approve {"bridge_id": , "user_id": }` + `X-API-Key` (admin级别) ### 启动参数说明 | 参数 | 说明 | 默认值 | |------|------|--------| | `--key` | Bridge 自身的桌面认证密钥(随机生成) | 无(必需) | | `--api-key` | 向 Server 注册的共享密钥 | `bingo301Tt23456Admin!` | | `--server-url` | AtomListing Server URL | `https://www.atomlisting.com` | | `--bridge-name` | 显示名称 | `Bridge-` | | `--bridge-region` | 地域标签 | 空 | | `--proxy-port` | 统一端口 | `9228` | | `--ping-timeout` | WS ping timeout (秒), 跨网建议60 | `30` | > `--api-key` 设计意图 = Server 的 `API_KEY_WRITE`(注册只需 write 权限)。 > 旧参数 `--bridge-secret` 已废弃,等价于 `--api-key`。 ## Heartbeat Commands Server 通过心跳响应下发指令: | Command | 说明 | params | |---------|------|--------| | `update_key` | 更换bridge认证key | `{ key: "new-xxx" }` | | `drain` | 标记draining,不再接受新Desktop连接 | `{}` | | `kick` | 踢掉指定Desktop连接 | `{ desktop_id: "dsk_xxx" }` | | `set_status` | 强制修改bridge状态 | `{ status: "offline" }` | ## Desktop 断线逻辑 - 多bridge时: 自动切换列表中下一个bridge(wrap around) - 单bridge时: 指数退避重试(1s → 2s → 4s → 8s → 15s cap) - 全部失败后才提示用户 ## WS 协议版本差异(关键兼容性) 项目中存在三个 Bridge server 实现,WS 协议互不兼容: | 维度 | relay.js (v1) | atomk-bridge (v3) | cloud-bridge (v4) | |------|-------------|------------------|------------------| | WS 认证 | 无认证,`extension_connect`/`desktop_connect` 消息标识身份 | 无 WS 认证,直接发 `register` | **5秒内必须发 `auth` 消息**(含 `key` 字段),否则 code 4001/4003 踢出 | | 注册消息 | `type: "extension_connect"` | `type: "register"` + `info: {}` | `type: "auth"` → `type: "registered"` + `slot_id`,然后可选 `type: "register"` 更新元数据 | | CDP 请求格式 | `cdp_command`/`cdp_response` + `requestId` | `request`/`response` + `request_id` | 同 v3 | | 心跳 | 无 | app-level `ping`/`pong` JSON | 多层:app ping/pong + TCP keepalive + HTTP POST 心跳 | | Desktop WS 客户端 | 无(Desktop 只 HTTP poll relay) | 需要 WS 客户端 | 需要带 auth 的 WS 客户端 | **Desktop 兼容策略**:先发 `auth` 消息(含 `key` + `desktop_id`),再发 `register` 消息。v3 和 relay 忽略未知 `type`,v4 依赖 `auth` 完成认证。这样 Desktop 代码一套逻辑兼容所有版本。 **⚠️ 关键修复(2026-05)**:Desktop `connectCloudBridge()` 原来只发 `register` 不发 `auth`,连 cloud-bridge v4 会被 5 秒超时踢掉(code 4003)。必须在 WS open handler 中先发 `auth`。 ## BridgeInfo Schema 对齐(Desktop ↔ Server) | Server `BridgeResponse` 字段 | Desktop `BridgeInfo` 旧字段 | 状态 | |------------------------------|---------------------------|------| | `id: int` | `id: number` | ✅ | | `user_id: Optional[int]` | ❌ 缺失 | **需添加** | | `name: str` | `name: string` | ✅ | | `host: str` | `host: string` | ✅ | | `port: int` | `port: number` | ✅ | | `key: str` | `key: string` | ✅ | | `status: str` ("online"/"offline"/"pending"/"draining") | `status: string` ("online"/"offline"/"draining") | 需加 "pending" | | `region: Optional[str]` | `region?: string` | ✅ | | `last_heartbeat: Optional[str]` | `last_heartbeat?: string` | ✅ | | `created_at: Optional[str]` | ❌ 缺失 | **需添加** | | ❌ 未暴露 | `bridge_id: string` | **需删除**(Server 未暴露) | | ❌ 未暴露 | `active_connections: number` | **需删除**(Server 未暴露) | | ❌ 未暴露 | `max_connections: number` | **需删除**(Server 未暴露) | > ⚠️ Desktop 需同步修改的文件:`operation-api.ts`(类型定义)、`preload/index.d.ts`(类型声明)、`ChromeBridge.tsx`(UI 渲染) ## Server 下发 Commands 处理 Server 通过心跳响应下发管理命令,Desktop 必须处理: | Command | 说明 | Desktop 处理 | |---------|------|-------------| | `update_key` | 更换 bridge 认证 key | 更新本地 `cloudBridgeConfig.apiKey` | | `drain` | 停止接受新请求,优雅断连 | 设 `intentionalClose=true`,5s 后断开(不重连) | | `kick` | 立即踢出 | 设 `intentionalClose=true`,立即断开 | | `set_status` | 强制修改上报状态 | 记录日志,下次心跳上报此状态 | 命令格式(通过 WS `type: "command"` 消息传递): ```json {"type": "command", "action": "update_key", "params": {"key": "new-xxx"}} ``` 或嵌套格式: ```json {"type": "command", "command": {"action": "drain", "params": {}}} ``` **⚠️ Desktop 原来完全忽略这些命令**,需要在 `ws.on('message')` handler 中添加 `msg.type === 'command'` 分支。 ## Production Bridges (as of 2026-05-29) | ID | Name | Host | Port | Region | Version | Location | Status | |----|------|------|------|--------|---------|----------|--------| | 9 | SG3-Server | 43.134.190.229 | 9228 | — | v4.0? | Remote VPS | online ✅ | | 10 | US-West-1 | 43.160.244.93 | 9228 | us | v4.0? | Remote VPS | online ✅ | | — | Bridge-CN-1 | 49.51.249.171 (public) / 127.0.0.1 (local) | 9228 | cn | v4.4.1 | **This machine** (`/home/ubuntu/atomk-page-bridge/cloud-bridge/`) | online ✅ | ### Bridge-CN-1 Local Details Bridge-CN-1 runs on THIS machine, not a remote VPS: - **Process**: PID varies, `/usr/bin/python3 server.py --key *** --proxy-port 9228 --ws-port 9229 --bridge-name Bridge-CN-1 --cdp-timeout 30 --proxy-timeout 60 --ping-timeout 60 --hermes-api-key ***` - **Working dir**: `/home/ubuntu/atomk-page-bridge/cloud-bridge/` - **Git remote**: `origin` → `gitea9webs.sh3.ikuai7.com/admin9webs/atomk-page-bridge.git` - **Code**: `server.py` = v4.4.1 (current, multi-slot), `server_multi.py` = v3.1 (legacy, not in use) - **Health**: `GET http://127.0.0.1:9228/health` with `Authorization: Bearer ***` → `{"status":"ok","version":"4.4.1","bridge_protocol":"1.1","connected_slots":1,...}` - **Models**: `GET http://127.0.0.1:9228/v1/models` → `{"data": [{"id": "hermes-agent"}]}` - **Chat proxy**: `POST http://127.0.0.1:9228/v1/chat/completions` → proxies to local Hermes agent (confirmed working) - **No hardcoded single-connection lock**: v4.0 uses dynamic `DesktopSlot` architecture with no `MAX_DESKTOPS` limit - **Restart**: Kill the process and re-run the same command, or use systemd if configured > 🔑 Approve via API: `POST /api/v1/bridges/approve {"bridge_id": , "user_id": }` + `X-API-Key` (admin). bridge_id is the DB auto-increment ID, not the name. Invalid user_id → 500. ## Pitfalls - **Header 不匹配**: Server 和 Bridge 必须用相同的 header 名(注册用 `X-API-Key`,心跳用 `X-Bridge-Key`)。改一边必须改另一边。详见 `references/protocol-pitfalls.md`。 - **API Key 对齐**: Bridge 默认 `--api-key bingo301Tt23456Admin!` 硬编码在 `server.py`(设计意图 = Server 的 `API_KEY_WRITE/ADMIN`)。Server 从环境变量读取(默认空串),需在生产服务器 `.env` 中设置相同值。注册只需 `write` 权限,不是 `admin`。详见 `references/server-deployment-and-keys.md`。 - **`check_permission` 参数 bug** (fixed `1f14950`): `bridges.py` 的 `register_bridge` 原来 `check_permission("write", x_api_key)` 把原始 key 字符串当权限级别传入,`PERMISSION_LEVELS` 里找不到 → 永远 False → 401。正确:先 `verify_api_key(x_api_key)` 拿到权限级别字符串,再 `check_permission("write", permission)`。 - **Patch 工具 `***` 红action**: 给 secret 类变量赋值的 patch 可能被安全拦截器替换为 `***`,导致语法错误。patch 后务必读回验证。 - **多 subagent 并发 push**: 同一 repo 被多个 subagent 同时改会 git reject,需 `pull --rebase` 解决。 - **Bridge 审批**: 新 bridge 注册后 status=pending,Desktop 看不到。测试时需手动 `PUT /bridges/{id}` 设 status=online。 - **WS close code 语义冲突**: relay 用 4001 表示 auth timeout,atomk-bridge 用 4001 表示 duplicate connection,cloud-bridge 用 4001 表示 auth timeout、4003 表示 invalid key。Desktop 必须同时处理 4001 和 4003 为认证失败,设 `lastError` 给用户看。 - **🔴 Bridge v3.0 `server_multi.py` has hardcoded single-connection**: The legacy `server_multi.py` (v3.1) had a hardcoded `max_connections=1` not read from DB. **Current state**: Bridge-CN-1 and all production bridges run v4.0 `server.py` which uses dynamic `DesktopSlot` architecture with no hardcoded limit. The `server_multi.py` file still exists in the repo but is NOT the running version. If a bridge were still on v3.0, `git pull` would bring in `server.py` v4.0; a restart would switch to multi-slot. - **🔴 首次登录后 model 未设置 → chat 卡住 (v3.9.6 fix, verified)**: `applyAgent` only configured Bridge connection, not model → `getModelConfig().model` was empty → chat used fallback `hermes-agent` → could fail. **v3.9.6 fix**: `applyAgent` now fetches `/v1/models` from Bridge HTTP API after connecting and calls `setModelConfig("auto", firstModelId, httpUrl)`. **Verified**: Bridge-CN-1 returns model `hermes-agent` via `/v1/models`; chat completions (`POST /v1/chat/completions`) proxy through Bridge to Hermes backend successfully (tested: ping → pong 🏓). If Bridge doesn't serve `/v1/models` (e.g. v3.0), user must manually select model from ModelPicker in Chat UI. - **🔴 apiKey 空串 = 静默跳过认证 → 4001 (v3.9.4→v3.9.5 fix)**: `connectCloudBridge()` 的 WS open handler 只在 `if (config.apiKey)` 时发送 `auth` 消息。如果调用方传 `apiKey: ""`,条件为 falsy → auth 消息不发送 → 服务端 5s 超时后踢出 (4001/4003)。**关键是空串和 undefined 都是 falsy**,所以传 `apiKey: ""` 等同于没有 key。必须确保 `bridges[0].key` 从 API 响应一路传播到 `connectCloudBridge({ serverUrl, apiKey: b.key })`,中间的 `connectionConfig.apiKey` 存储、`getCloudBridgeConfig()` 返回、UI 手动连接读取都不能丢掉 key。如果 key 确实为空(如旧版 Bridge 不需要 key),则应不传 apiKey 让代码走 v3/v1 兼容路径。 - **Schema 改动需三处同步**: 修改 `BridgeInfo` 类型时必须同时更新 `operation-api.ts`、`preload/index.d.ts`、`ChromeBridge.tsx`,否则 TypeScript 编译可能在 preload/renderer 侧报类型不匹配。 ## Desktop V4.0 Connection 架构 v3.9.0+ 的 Desktop Connection 板块以 V4.0 Cloud Bridge 为核心流程: ``` 用户登录 atomlisting.com → Server 返回 bridges + recommended_bridge → Desktop 缓存到 desktop.json (userBridges + recommendedBridgeId) → Connection 板块显示 Bridge 列表卡片 + 一键连接按钮 → 点击连接: chromeBridgeCloudConnect(recommendedBridgeUrl) + operationApplyAgent() → CloudBridge 状态实时呈现: ● 已注册 / ◌ 注册中 / ↻ 重连 / ○ 断开 → V3 手动 Remote/SSH 降为高级折叠选项 ``` 关键 IPC 通道: | 通道 | 用途 | |---|---| | `operation:get-bridges-cached` | 从 desktop.json 读取缓存的 Bridge 列表(无 API 调用) | | `chrome-bridge:set-bridge-list` | 登录后推送 Bridge 列表到 chrome-bridge 模块(支持断线自动切换) | | `chrome-bridge:cloud-connect` | 连接指定 Bridge WS 地址 | | `chrome-bridge:cloud-disconnect` | 断开当前 CloudBridge 连接 | | `chrome-bridge:cloud-get-state` | 获取当前连接状态(connected, registered, reconnectAttempt, serverUrl, clientId, lastError) | ### CloudBridge 远程模式 API URL 解析 (v3.9.7+) CloudBridge 模式下 `ConnectionConfig` 存储的是 `cloudBridgeUrl`(ws:// 格式)而非 `remoteUrl`。`getApiUrl()` 和 `getRemoteApiBaseUrl()` 必须在回退到 `remoteUrl` 之前先解析 `cloudBridgeUrl`: ``` cloudBridgeUrl = "ws://49.51.249.171:9228/ws" → replace wss→https / ws→http → strip /ws suffix → httpUrl = "http://49.51.249.171:9228" ``` **v3.9.7 fix**: 之前 `getApiUrl()` 只检查 `conn.remoteUrl`(CloudBridge 模式下为空),导致所有 REST API 调用(sessions、chat completions、model auto-detect)均失败。 ### Bridge → Gateway Auth Chain (v3.9.7+ 核心认证模型) Desktop 通过 Bridge 调用 Hermes Gateway API 的认证链: ``` Desktop ──Authorization: Bearer ──→ Bridge (check_auth 验证 b.key) Bridge ──strip all Authorization headers───→ Bridge (hermes_api_proxy_handler) Bridge ──inject Authorization: Bearer ──→ Gateway (8642) ``` **关键决策:Desktop 用 `b.key`(Bridge key)认证,不用 `agent_api_key`**: - Server `/api/v1/bridges/` 返回 `key`(Bridge 认证密钥)和 `agent_api_key`(Gateway 密钥,可选) - Desktop 的 `conn.apiKey` = `b.key`,用于 Bridge 的 `check_auth()` - Bridge 的 `hermes_api_proxy_handler` 在转发到 Gateway 前**移除所有incoming Authorization headers**,再注入自己的 `HERMES_API_KEY` - 因此 Desktop 不需要 Gateway key,Bridge 替它注入 - `agent_api_key` 字段仅用于参考/诊断,不应被 Desktop 用于 API 请求 **`--hermes-api-key` 启动参数(v4.0+)**: - 默认值为空串,自动 fallback 到 `--api-key`(同 `API_KEY`) - 单机部署(Bridge + Gateway 同一台机器)通常共享同一个 key - 启动命令示例:`python3 server.py --key Bing2026Cao$$$ --proxy-port 9228 --bridge-name Bridge-CN-1 --hermes-api-key Bing2026Cao$$$` - 如果省略 `--hermes-api-key`,Bridge 自动用 `--key` 的值作为 Gateway key **BridgeInfo 新字段(v3.9.7+,Server commit `5b49c04` 已部署)**: - `agent_api_key?: string` — Gateway API key(仅供参考,Desktop 不使用。⚠️ Server `config.py` 的 `HERMES_GATEWAY_API_KEY` 必须通过环境变量设置,不能硬编码——之前 `5b49c04` 硬编码了错误的 `5HH551...` 已修正为空串默认值) - `default_model?: string` — Server 推荐的默认模型(避免 `/v1/models` 额外请求)。Desktop `applyAgent` 优先用此值,fallback 到 `/v1/models` fetch - `default_provider?: string` — Server 推荐的默认 provider ### Model 自动选择 (v3.9.7+) Desktop 在 CloudBridge 远程模式下首次使用时本地模型配置为空。两处自动选择: 1. **applyAgent** (首次登录): 连接 Bridge 后主动 fetch `/v1/models` → `setModelConfig()` 2. **get-model-config IPC** (按需): 如果本地 `model` 为空且 `cloudBridgeUrl` + `apiKey` 可用,自动 fetch `/v1/models` 并缓存结果 两端均需 `cloudBridgeUrl` → HTTP 转换(同上节)+ `Authorization: Bearer ${conn.apiKey}` header。 **⚠️ `getEdgeInstanceConfig()` guard condition**: 此函数应以 `system_code`(主键,Server 一定返回)做 null guard,**绝不能**以 `cloud_bridge_ws_url`(派生字段)做 guard。当 Server 未配置 `INSTANCE_DEFAULT_DOMAIN` 时,`cloud_bridge_ws_url` 为空串但其余字段(system_code, domain, agent_api_url)均有效。以 `cloud_bridge_ws_url` 做 guard 会导致整个 EdgeInstanceConfig 返回 null,前端完全不显示 Bridge 信息。 - **Bridge key 明文暴露**: Server `GET /bridges/` 返回完整 `key` 字段,Desktop 客户端可直接获取所有 bridge 的认证密钥。生产环境应考虑脱敏或权限控制。 - **jwt_secret 心跳泄露**: Server 心跳响应每次都下发 `jwt_secret`(代码注释说"仅审批后首次",但实际是只要 `status=online` 就下发),Bridge 端持续持有完整 JWT 签名密钥。 - **Server schema 内联**: 路由文件不得内联 Pydantic schema 类,必须从 `schemas/bridge.py` 导入。之前有 subagent 在 `bridges.py` 里内联了 `BridgeResponse`,导致与 `schemas/bridge.py` 分歧。 - **Server 已知 bugs**: `User.last_used_bridge_id` 缺 Alembic 迁移;`auth.py` 从 `remote_user` 读此字段永远返回 None;`bridges.py` 有新旧两套审批端点。详见 `references/server-deployment-and-keys.md`。 - **`bridges.py` `unshare_bridge` NPE risk**: L663 accesses `bridge.user_id` without null-checking the bridge object first. If `bridge_id` doesn't exist, `bridge = session.query(Bridge).filter_by(id=bridge_id).first()` returns None → `.user_id` raises `AttributeError`. In practice the admin-only permission guard + UI constraints make hitting this unlikely, but defensive code should check `if not bridge: raise HTTPException(404)`. - **🔴 CDP Snapshot Handler 缺 slot 解析 → NameError**: `cdp_snapshot_handler` 如果没有调用 `_resolve_slot_from_request(request)`,访问 `slot.ref_map` 时抛出 `NameError: name 'slot' is not defined`。其他 CDP handler(click-ref, fill-ref, wait, scroll-ref)都正确调用了,仅 snapshot 遗漏。代码审查时应检查所有 Route B handler 是否以 `slot = _resolve_slot_from_request(request)` 开头。详见 `references/cdp-pipeline-debugging.md`。 - **🔴 proxy_handler 也缺 slot 解析 → NameError**: `proxy_handler` 在 `get_primary_client()` 后直接访问 `slot.pending`,但从未通过 `slots.get(cid)` 解析 slot。修复:`slot = slots.get(cid); if not slot: return 503`。**所有** 访问 `slot.*` 的 handler 都必须先解析 slot。详见 `references/cdp-pipeline-debugging.md` (Bug 3)。 - **🔴 Bridge WS tunnel 缺 auth header → Desktop Relay 401 (v3.9.10+)**: Desktop v3.9.10 的 Express relay(localhost:3928)加了 `relayAuthMiddleware`。Bridge 通过 WS tunnel 转发 CDP 请求到 Desktop 时,`handleCloudTunnelRequest` 将 WS 消息中的 headers 原样传给 `localhost:3928`。Bridge 的 `cdp_send()` 必须在 WS 消息 headers 中包含 `Authorization: Bearer `,否则 Desktop relay 返回 401。详见 `references/cdp-pipeline-debugging.md` (Bug 2)。 - **🔴 proxy_handler 过滤掉 Authorization header → 401**: `proxy_handler` 原来用 dict comprehension 构建 tunnel headers 时明确过滤了 `authorization` 字段(`if k.lower() not in ('host', 'authorization')`)。只应过滤 `host`,必须保留 `authorization` 并兜底注入 `API_KEY`。详见 `references/cdp-pipeline-debugging.md` (Bug 4)。 - **CDP 调试流程**: 当 CDP 不工作时,按顺序检查 Bridge diagnostics → Bridge 日志 → 隔离 pipeline 断点 → 直接 curl 测试。完整流程见 `references/cdp-pipeline-debugging.md`。 - **API 代理超时对齐 (`120s→300s, 2026-06-02`)**: Hermes Agent 任务可跑 20-30 分钟,Desktop API 客户端 + Bridge 两侧代理超时都必须从 120s 提升到 300s。改一处漏另一处会导致 `socket hang up` 或 `API request timed out`。详见 `references/cdp-pipeline-debugging.md` "API Proxy Timeout Tuning"。 ## References - `references/protocol-pitfalls.md` — Server/Bridge header 名不一致等协议陷阱 - `references/ws-protocol-comparison.md` — 三个 Bridge server 实现的 WS 协议详细对比 - `references/server-deployment-and-keys.md` — 生产服务器部署信息、API Key 架构(不在 Gitea 中)、已知 Server bugs - `references/cdp-pipeline-debugging.md` — CDP 命令流水线架构、常见 Bug 及修复、调试流程 - `references/bridge-health-diagnostics.md` — 解读 Bridge `/health` 端点输出,诊断多 Desktop 连接状态