From 63cb1d96b023f979fa98dc713c97f6c4f03ea50e Mon Sep 17 00:00:00 2001 From: admin9webs Date: Fri, 10 Jul 2026 16:10:36 +0800 Subject: [PATCH] Add archived/atomk-three-layer-arch --- .../archived/atomk-three-layer-arch/SKILL.md | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 skills/archived/atomk-three-layer-arch/SKILL.md diff --git a/skills/archived/atomk-three-layer-arch/SKILL.md b/skills/archived/atomk-three-layer-arch/SKILL.md new file mode 100644 index 0000000..20c54ea --- /dev/null +++ b/skills/archived/atomk-three-layer-arch/SKILL.md @@ -0,0 +1,346 @@ +--- +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 连接状态