Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
summarizer_adapter.rs 和 saas-relay-client.ts 中的 fallback 模型名 (glm-4-flash / glm-4-flash-250414) 在 SaaS relay 中不存在,导致请求被拒绝。 改为未配置时明确报错(fail fast),不再静默使用错误模型。
241 lines
8.0 KiB
Markdown
241 lines
8.0 KiB
Markdown
---
|
||
title: 客户端路由
|
||
updated: 2026-04-11
|
||
status: active
|
||
tags: [module, routing, connection]
|
||
---
|
||
|
||
# 客户端路由
|
||
|
||
> 从 [[index]] 导航。关联模块: [[chat]] [[saas]]
|
||
|
||
## 设计思想
|
||
|
||
**核心决策: Tauri 桌面端通过 SaaS Token Pool 中转访问 LLM,不直连。**
|
||
|
||
为什么?
|
||
1. **集中密钥管理** — 用户不需要自己的 API Key,SaaS 维护共享 Key 池
|
||
2. **用量追踪 + 计费** — 每次调用经过 SaaS,`record_usage` worker 记录 token 消耗
|
||
3. **模型白名单** — Admin 配置哪些模型可用,`listModels()` 返回白名单
|
||
4. **降级保障** — SaaS 挂了自动切本地 Kernel,桌面端不变砖
|
||
|
||
## 代码逻辑
|
||
|
||
### 4 分支决策树
|
||
|
||
入口: `connectionStore.ts:349` → `connect(url?, token?)`
|
||
|
||
```
|
||
connect()
|
||
│
|
||
├── [1] Admin 强制路由: localStorage llm_routing
|
||
│ ├── "relay" → 强制 SaaS Relay 模式
|
||
│ └── "local" → 强制本地 Kernel (adminForceLocal=true)
|
||
│
|
||
├── [2] SaaS Relay 模式: localStorage('zclaw-connection-mode') === 'saas'
|
||
│ ├── Tauri: KernelClient + baseUrl = saasUrl/api/v1/relay
|
||
│ │ apiKey = SaaS JWT (不是 LLM Key!)
|
||
│ ├── Browser: SaaSRelayGatewayClient (SSE)
|
||
│ └── SaaS 不可达 → 降级到本地 Kernel
|
||
│
|
||
├── [3] 本地 Kernel: isTauriRuntime() === true
|
||
│ KernelClient + 用户自定义模型配置
|
||
│ 用户需要自己的 API Key
|
||
│
|
||
└── [4] External Gateway (fallback)
|
||
GatewayClient via WebSocket/REST
|
||
```
|
||
|
||
### SaaS Relay 主路径 (Tauri 桌面端)
|
||
|
||
关键代码: `connectionStore.ts:482-535`
|
||
|
||
```ts
|
||
kernelClient.setConfig({
|
||
provider: 'custom',
|
||
model: modelToUse, // 从 SaaS listModels() 获取
|
||
apiKey: session.token, // SaaS JWT,不是 LLM Key
|
||
baseUrl: `${session.saasUrl}/api/v1/relay`, // 指向 SaaS relay
|
||
apiProtocol: 'openai',
|
||
});
|
||
```
|
||
|
||
**注意**: Kernel 仍然执行 LLM 调用逻辑,但请求发往 SaaS relay 而非直连 LLM。
|
||
SaaS relay 接到请求后,从 Token Pool 中取一个可用 Key,转发给真实 LLM。
|
||
|
||
### SaaS 降级流程
|
||
|
||
关键代码: `connectionStore.ts:446-468`
|
||
|
||
```
|
||
listModels() 失败
|
||
→ 401 → session 过期 → logout → 要求重新登录
|
||
→ 其他错误 → saasDegraded = true
|
||
→ saasStore.saasReachable = false
|
||
→ 降级到本地 Kernel 模式
|
||
```
|
||
|
||
### 客户端类型
|
||
|
||
| 客户端 | 传输 | 文件 | 用途 |
|
||
|--------|------|------|------|
|
||
| GatewayClient | WebSocket + REST | `lib/gateway-client.ts` | 外部 Gateway 进程 |
|
||
| KernelClient | Tauri invoke() | `lib/kernel-chat.ts` | 内置 Kernel (桌面端) |
|
||
| SaaSRelayGatewayClient | HTTP SSE | `lib/saas-relay-client.ts` | 浏览器端 SaaS 中继 |
|
||
|
||
`getClient()` 定义: `connectionStore.ts:844`
|
||
所有 Store 通过 `initializeStores()` (store/index.ts:94) 获取共享 client。
|
||
|
||
### Store 层 (17 文件 + chat/4)
|
||
|
||
```
|
||
desktop/src/store/
|
||
├── index.ts Store 协调器 + client 注入
|
||
├── agentStore.ts Agent 分身管理
|
||
├── browserHandStore.ts 浏览器 Hand 状态
|
||
├── chatStore.ts 聊天通用状态
|
||
├── classroomStore.ts 课堂模式
|
||
├── configStore.ts 配置读写
|
||
├── connectionStore.ts 路由决策核心
|
||
├── handStore.ts Hand 状态管理
|
||
├── memoryGraphStore.ts 记忆图谱
|
||
├── offlineStore.ts 离线队列
|
||
├── saasStore.ts SaaS 认证
|
||
├── securityStore.ts 安全状态
|
||
├── sessionStore.ts 会话管理
|
||
├── uiModeStore.ts 双模式 UI
|
||
├── workflowBuilderStore.ts 工作流构建器
|
||
├── workflowStore.ts 工作流状态
|
||
└── chat/
|
||
├── artifactStore.ts 聊天产物
|
||
├── conversationStore.ts 会话管理
|
||
├── messageStore.ts 消息持久化
|
||
└── streamStore.ts 流式编排
|
||
```
|
||
|
||
### lib/ 工具层 (85 个文件)
|
||
|
||
关键分类:
|
||
|
||
| 类别 | 文件 | 数量 |
|
||
|------|------|------|
|
||
| Kernel 通信 | kernel-chat/kernel-client/kernel-agent/... | 7 |
|
||
| SaaS 通信 | saas-client/saas-auth/saas-billing/... | 9 |
|
||
| Gateway | gateway-client/gateway-api/gateway-auth/... | 6 |
|
||
| Intelligence | intelligence-client/ + 5 fallback | 7 |
|
||
| 工具 | config-parser/crypto-utils/logger/utils | 10+ |
|
||
| Tauri 集成 | safe-tauri/tauri-gateway/secure-storage | 3 |
|
||
|
||
## 模型路由
|
||
|
||
### 完整链路 (Tauri SaaS Relay 主路径)
|
||
|
||
```
|
||
前端模型选择
|
||
│
|
||
├─ conversationStore.currentModel (用户上次选择的模型)
|
||
│ 持久化到 IndexedDB,跨会话保留
|
||
│
|
||
├─ connectionStore 连接时获取 SaaS 可用模型
|
||
│ saasClient.listModels() → [{id: "deepseek-chat"}, {id: "GLM-4.7"}, ...]
|
||
│ relayModels[0]?.id 作为 fallback
|
||
│
|
||
└─ 最终: preferredModel || fallbackId
|
||
│
|
||
▼
|
||
kernelClient.setConfig({ model: modelToUse })
|
||
│
|
||
▼
|
||
kernel_init (Tauri Command)
|
||
│ KernelConfigRequest { model, api_key, base_url }
|
||
│ base_url = "https://saas-host/api/v1/relay"
|
||
│ api_key = SaaS JWT (不是 LLM Key!)
|
||
│
|
||
▼
|
||
Kernel::boot(config)
|
||
│ config.llm.model = modelToUse
|
||
│ config.llm.base_url = SaaS relay URL
|
||
│
|
||
▼
|
||
loop_runner → LLM Driver (OpenAI compatible)
|
||
│ POST {base_url}/chat/completions
|
||
│ body: { model: modelToUse, messages: [...] }
|
||
│ header: Authorization: Bearer {SaaS JWT}
|
||
│
|
||
▼
|
||
SaaS Relay Handler (handlers.rs)
|
||
│ cache.get_model(model_name) → 精确匹配 model_id
|
||
│ ⚠️ 无别名解析! "glm-4-flash" ≠ "deepseek-chat"
|
||
│ 找不到 → 400 "模型 xxx 不存在或未启用"
|
||
│
|
||
▼
|
||
Key Pool 轮换
|
||
│ priority ASC → last_used_at ASC → cooldown 检查 → RPM/TPM 滑动窗口
|
||
│
|
||
▼
|
||
真实 LLM API
|
||
│ 429 → mark cooldown → 切换 key
|
||
│ 5xx → exponential backoff
|
||
│ model_group → 跨 Provider 故障转移
|
||
│
|
||
▼
|
||
响应 → SSE 流式返回 → 前端
|
||
```
|
||
|
||
### 辅助 LLM 调用 (非聊天主路径)
|
||
|
||
这些 Rust 端组件也通过同一个 relay 发起 LLM 请求:
|
||
|
||
| 组件 | 文件 | 模型来源 | 触发时机 |
|
||
|------|------|----------|----------|
|
||
| 记忆摘要 | `summarizer_adapter.rs` | kernel_init 传入的 model | 定期 L0/L1 摘要生成 |
|
||
| 记忆提取 | `extraction_adapter.rs` | kernel_init 传入的 model | 中间件触发提取 |
|
||
| 管家路由 | ButlerRouter via loop_runner | 同聊天模型 | 聊天中间件链 |
|
||
|
||
**关键**: `summarizer_adapter.rs` 和 `extraction_adapter.rs` 在 `kernel_init` 时配置,
|
||
使用与聊天相同的 `model` 和 `base_url`。未配置时会明确报错,不会静默 fallback 到错误模型。
|
||
|
||
### SaaS Relay 模型匹配规则
|
||
|
||
```
|
||
前端发送 model: "deepseek-chat"
|
||
→ SaaS cache 按 model_id 精确匹配
|
||
→ 匹配: cache.models["deepseek-chat"] → 命中
|
||
→ 不匹配: cache.models["glm-4-flash"] → null → 400 错误
|
||
|
||
⚠️ config.toml 中的 [llm.aliases] 仅用于本地 Kernel,SaaS relay 不解析别名!
|
||
```
|
||
|
||
### Browser 模式模型路由
|
||
|
||
```
|
||
createSaaSRelayGatewayClient(saasUrl, getModel)
|
||
│ getModel() 回调 → conversationStore.currentModel || relayModels[0]?.id
|
||
│
|
||
▼
|
||
chatStream() → saasClient.chatCompletion({ model: getModel() })
|
||
│ 未获取到模型时 → onError 报错,不发请求
|
||
│
|
||
▼
|
||
POST /api/v1/relay/chat/completions → SSE 流
|
||
```
|
||
|
||
## 关联模块
|
||
|
||
- [[chat]] — 路由决定使用哪种 ChatStream
|
||
- [[saas]] — Token Pool、认证、模型管理
|
||
- [[middleware]] — 请求经过中间件链处理
|
||
- [[butler]] — 管家模式通过 ButlerRouter 中间件介入
|
||
|
||
## 关键文件
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `desktop/src/store/connectionStore.ts` | 路由决策核心 |
|
||
| `desktop/src/lib/gateway-client.ts` | WebSocket 客户端 |
|
||
| `desktop/src/lib/kernel-chat.ts` | Tauri 内核聊天 |
|
||
| `desktop/src/lib/kernel-client.ts` | Kernel 客户端配置 |
|
||
| `desktop/src/lib/saas-relay-client.ts` | SaaS SSE 中继 |
|
||
| `desktop/src/lib/saas-client.ts` | SaaS API 客户端 |
|
||
| `desktop/src/store/index.ts` | Store 协调器 + client 注入 |
|