--- title: 客户端路由 updated: 2026-04-22 status: active tags: [module, routing, connection] --- # 客户端路由 > 从 [[index]] 导航。关联模块: [[chat]] [[saas]] ## 1. 设计决策 **核心: Tauri 桌面端通过 SaaS Token Pool 中转访问 LLM,不直连。** | 决策 | 原因 | |------|------| | 5 分支路由 | 覆盖全部部署形态: Admin本地 / Tauri+SaaS / Browser+SaaS / Tauri本地 / 外部Gateway | | SaaS Relay 中转 | 集中密钥管理 — 用户无需自备 API Key;用量追踪计费 — 每次调用经 SaaS;模型白名单 — Admin 控制可用模型 | | 自动降级到本地 Kernel | SaaS 不可达时桌面端不变砖,无感切换,不需要用户干预 | | Kernel 不直连 LLM | 直连是降级后备。主路径经 SaaS Token Pool 做 RPM/TPM 轮换 + 故障转移 | | getClient() 全局单例 | 所有 Store 通过 `initializeStores()` 获取共享 client,避免重复连接 | ## 2. 关键文件 + 数据流 ### 核心文件 | 文件 | 职责 | |------|------| | `desktop/src/store/connectionStore.ts` | 路由决策核心: 5 分支 + 降级 + 模型路由 | | `desktop/src/lib/kernel-chat.ts` | KernelClient ChatStream (Tauri Event) | | `desktop/src/lib/kernel-client.ts` | Kernel 客户端配置 (setConfig/boot) | | `desktop/src/lib/saas-relay-client.ts` | SaaS Relay ChatStream (SSE) | | `desktop/src/lib/gateway-client.ts` | External Gateway ChatStream (WebSocket) | | `desktop/src/store/index.ts` | Store 协调器 + client 注入 | ### 5 分支决策树 ``` connect() ├─ [1] Admin强制本地: adminRouting=local && isTauri → Kernel 直连 ├─ [2] SaaS+Tauri: savedMode=saas && isTauri → KernelClient + baseUrl=SaaS relay │ └─ SaaS不可达 → 降级 [4] ├─ [3] SaaS+Browser: savedMode=saas && !isTauri → SaaSRelayClient (SSE) │ └─ SaaS不可达 → 降级 [4] ├─ [4] 本地Kernel: isTauriRuntime && 非SaaS → KernelClient + 用户自配 Key └─ [5] 外部Gateway: !isTauri → GatewayClient (WebSocket) ``` ### 集成契约 | 方向 | 模块 | 接口 | 说明 | |------|------|------|------| | Calls -> | saas | relay URL + JWT | Chat relay, model list, 用量上报 | | Calls -> | kernel | Tauri invoke | Kernel boot, chat, config | | Called by <- | all stores | `getClient()` | 每个 API 调用都经过路由决策 | | Provides -> | UI | Connection status, model list | 所有聊天依赖组件消费 | ## 3. 代码逻辑 ### 模型路由链 (SaaS Relay 主路径) ``` 前端选择模型 → preferredModel || fallbackId → kernelClient.setConfig({ model, apiKey: JWT, baseUrl: saasUrl/api/v1/relay }) → Tauri invoke kernel_init → Kernel::boot(config) → loop_runner → POST {base_url}/chat/completions → SaaS Relay → cache 精确匹配 model_id → Key Pool 轮换 → 真实 LLM → SSE 流式返回 ``` ### SaaS 降级流程 ``` listModels() 失败 → 401 → session 过期 → logout → 其他 → saasDegraded=true → 降级本地 Kernel ``` ### 客户端类型 | 客户端 | 传输 | 用途 | |--------|------|------| | GatewayClient | WebSocket + REST | 外部 Gateway 进程 | | KernelClient | Tauri invoke() | 内置 Kernel (桌面端) | | SaaSRelayGatewayClient | HTTP SSE | 浏览器端 SaaS 中继 | ### 不变量 - `getClient()` 是全局单例,所有 Store 通过 `initializeStores()` 获取共享 client - SaaS 不可达时自动降级到本地 Kernel,不需要用户干预 - SaaS Relay 按 `model_id` 精确匹配,不解析别名 (`config.toml [llm.aliases]` 仅本地 Kernel) - Provider Key 解密失败时 warn+skip,不 500 (`key_pool.rs`) ### Tauri 命令 | 命令 | 说明 | |------|------| | `kernel_init` | 初始化 Kernel 配置 (model, apiKey, baseUrl) | | `zclaw_start` / `zclaw_stop` / `zclaw_restart` | Kernel 生命周期管理 | | `zclaw_health_check` / `zclaw_ping` | 健康检查 + 端口检测 | | `zclaw_doctor` | 完整诊断报告 | ### SaaS Relay 路由 | 路径 | 说明 | |------|------| | `POST /api/v1/relay/chat/completions` | 主聊天中转 (认证+配额) | | `GET /api/v1/relay/models` | 可用模型列表 | ## 4. 活跃问题 + 注意事项 | 问题 | 状态 | 说明 | |------|------|------| | Tauri invoke 参数名 snake_case | ✅ 已修复 (f6c5dd2) | Tauri 2.x 默认 `rename_all="camelCase"`,invoke 必须用 camelCase | | Provider Key 解密致 relay 500 | ✅ 已修复 (b69dc61) | decrypt 失败 warn+skip,启动时 `heal_provider_keys()` 自动重新加密 | | Tauri 命令孤儿 | ~0 (差异来自内部调用) | 190 定义 / 104 invoke / 97 @reserved | **注意事项:** - `summarizer_adapter.rs` 和 `extraction_adapter.rs` 在 `kernel_init` 时配置,使用与聊天相同的 model+base_url。未配置时明确报错,不静默 fallback - Browser 模式 `getModel()` 未获取到模型时 onError 报错,不发请求 ## 5. 变更日志 | 日期 | 变更 | |------|------| | 04-22 | Wiki 重写: 5 节模板,移除 Store/lib 全量列表 | | 04-21 | 上一轮更新 | | 04-19 | TRUTH.md 数字校准: 190 命令 / 104 invoke / 97 @reserved | | 04-16 | Provider Key 解密修复 (b69dc61) | | 04-16 | Tauri invoke 参数名修复 (f6c5dd2) |