Files

347 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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` headerbridge自身的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` 的 bridgepending/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 远程连 BridgeBridge 向 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 <bridge-key>"
# → {"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": <int>, "user_id": <int>}` + `X-API-Key` (admin级别)
### 启动参数说明
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--key` | Bridge 自身的桌面认证密钥(随机生成) | 无(必需) |
| `--api-key` | 向 Server 注册的共享密钥 | `bingo301Tt23456Admin!` |
| `--server-url` | AtomListing Server URL | `https://www.atomlisting.com` |
| `--bridge-name` | 显示名称 | `Bridge-<hash前6位>` |
| `--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时: 自动切换列表中下一个bridgewrap 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": <int>, "user_id": <int>}` + `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=pendingDesktop 看不到。测试时需手动 `PUT /bridges/{id}` 设 status=online。
- **WS close code 语义冲突**: relay 用 4001 表示 auth timeoutatomk-bridge 用 4001 表示 duplicate connectioncloud-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 <b.key>──→ Bridge (check_auth 验证 b.key)
Bridge ──strip all Authorization headers───→ Bridge (hermes_api_proxy_handler)
Bridge ──inject Authorization: Bearer <HERMES_API_KEY>──→ 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 keyBridge 替它注入
- `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 handlerclick-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 relaylocalhost:3928)加了 `relayAuthMiddleware`。Bridge 通过 WS tunnel 转发 CDP 请求到 Desktop 时,`handleCloudTunnelRequest` 将 WS 消息中的 headers 原样传给 `localhost:3928`。Bridge 的 `cdp_send()` 必须在 WS 消息 headers 中包含 `Authorization: Bearer <API_KEY>`,否则 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 连接状态