Files
zclaw_openfang/wiki/routing.md
iven ed77095a37
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
docs(wiki): 系统性更新 — L0速览+L1模块标准化+L2功能链路映射(33条)
三层架构增强:
- L0 index.md: 用户功能清单+跨模块数据流全景图+导航树增强 (92→143行)
- L1 8个模块页标准化: 功能清单/API接口/测试链路/已知问题
  routing(252→326) chat(101→157) saas(153→230) memory(182→333)
  butler(137→179) middleware(121→159) hands-skills(218→257) pipeline(111→156)
- L1 新增2页: security.md(157行) data-model.md(180行)
- L2 feature-map.md: 33条端到端功能链路映射(408行)

维护机制: CLAUDE.md §8.3 wiki触发规则 5→9条
设计文档: docs/superpowers/specs/2026-04-21-wiki-systematic-overhaul-design.md
2026-04-21 23:48:19 +08:00

327 lines
13 KiB
Markdown
Raw 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.

---
title: 客户端路由
updated: 2026-04-21
status: active
tags: [module, routing, connection]
---
# 客户端路由
> 从 [[index]] 导航。关联模块: [[chat]] [[saas]]
## 设计思想
**核心决策: Tauri 桌面端通过 SaaS Token Pool 中转访问 LLM不直连。**
为什么?
1. **集中密钥管理** — 用户不需要自己的 API KeySaaS 维护共享 Key 池
2. **用量追踪 + 计费** — 每次调用经过 SaaS`record_usage` worker 记录 token 消耗
3. **模型白名单** — Admin 配置哪些模型可用,`listModels()` 返回白名单
4. **降级保障** — SaaS 挂了自动切本地 Kernel桌面端不变砖
## 功能清单
| 功能 | 描述 | 入口文件 | 状态 |
|------|------|----------|------|
| 连接管理 | 5 分支路由决策 + 自动降级 | connectionStore.ts | ✅ |
| SaaS Relay 中转 | Tauri 通过 SaaS Token Pool 中转 LLM | connectionStore.ts | ✅ |
| 浏览器模式 | SSE 连接 SaaS relay | saas-relay-client.ts | ✅ |
| 本地 Kernel | Tauri 内置 Kernel 直连 LLM | kernel-client.ts | ✅ |
| 外部 Gateway | WebSocket 独立进程 | gateway-client.ts | ✅ |
| Gateway 进程管理 | 启动/停止/重启/状态/诊断 | gateway/commands.rs | ✅ |
| 健康检查 | 端口检测 + 完整诊断 | health_check.rs | ✅ |
| 设备配对 | 设备审批 + 公钥交换 | gateway/commands.rs | ✅ |
| 模型路由 | 白名单验证 + fallback + 别名解析 | connectionStore.ts | ✅ |
## 代码逻辑
### 5 分支 + 降级决策树
入口: `connectionStore.ts``connect(url?, token?)`
```
connect()
├── [1] Admin 强制本地: adminRouting === 'local' && isTauri()
│ → 直接走 Kernel 模式,跳过 SaaS
├── [2] SaaS Relay (Tauri 路径): savedMode === 'saas' && isTauri()
│ → KernelClient + baseUrl = saasUrl/api/v1/relay
│ → apiKey = SaaS JWT (不是 LLM Key!)
│ → SaaS 不可达 → 降级到本地 Kernel
├── [3] SaaS Relay (Browser 路径): savedMode === 'saas' && !isTauri()
│ → SaaSRelayGatewayClient (SSE)
│ → SaaS 不可达 → 降级到本地 Kernel
├── [4] 本地 Kernel: isTauriRuntime() && 非 SaaS 模式
│ → KernelClient + 用户自定义模型配置
│ → 用户需要自己的 API Key
└── [5] External Gateway (fallback): !isTauri()
→ 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 层 (16 根文件 + chat/4 + saas/5 = 25)
```
desktop/src/store/
├── index.ts Store 协调器 + client 注入
├── agentStore.ts Agent 分身管理
├── browserHandStore.ts 浏览器 Hand 状态
├── chatStore.ts 聊天通用状态
├── classroomStore.ts 课堂模式
├── configStore.ts 配置读写
├── connectionStore.ts 路由决策核心
├── handStore.ts Hand 状态管理
├── industryStore.ts 行业配置 (已接通 ButlerPanel)
├── memoryGraphStore.ts 记忆图谱
├── offlineStore.ts 离线队列
├── saasStore.ts SaaS 认证 (re-export barrel)
├── securityStore.ts 安全状态
├── sessionStore.ts 会话管理
├── uiModeStore.ts 双模式 UI
├── workflowStore.ts 工作流状态
├── chat/
│ ├── artifactStore.ts 聊天产物
│ ├── conversationStore.ts 会话管理
│ ├── messageStore.ts 消息持久化
│ └── streamStore.ts 流式编排
└── saas/ (拆分子模块, 04-17 refactor)
├── index.ts 子模块入口
├── auth.ts 认证逻辑
├── billing.ts 计费逻辑
├── shared.ts 共享状态/工具
└── types.ts 类型定义
```
### lib/ 工具层 (75 个 .ts 文件)
关键分类:
| 类别 | 文件 | 数量 |
|------|------|------|
| Kernel 通信 | kernel-client/kernel-chat/kernel-agent/kernel-skills/kernel-triggers/kernel-hands/... | 8 |
| SaaS 通信 | saas-client/saas-auth/saas-billing/saas-relay/saas-industry/saas-knowledge/... | 12 |
| Gateway | gateway-client/gateway-api/gateway-auth/gateway-config/... | 9 |
| Intelligence | intelligence-backend/intelligence-client/embedding-client/memory-extractor | 4 |
| Viking | viking-client | 1 |
| Pipeline | pipeline-client/pipeline-recommender | 2 |
| Security | crypto-utils/secure-storage/security-audit/security-index/api-key-storage | 5 |
| 工具 | config-parser/logger/utils/error-types/error-utils/json-utils/... | 10+ |
| Tauri 集成 | safe-tauri/tauri-gateway | 2 |
| 工作流 | workflow-builder/ (index + types + yaml-converter) | 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] 仅用于本地 KernelSaaS relay 不解析别名!
```
### Browser 模式模型路由
```
createSaaSRelayGatewayClient(saasUrl, getModel)
│ getModel() 回调 → conversationStore.currentModel || relayModels[0]?.id
chatStream() → saasClient.chatCompletion({ model: getModel() })
│ 未获取到模型时 → onError 报错,不发请求
POST /api/v1/relay/chat/completions → SSE 流
```
## API 接口
### Tauri 命令
**Gateway 管理** (`desktop/src-tauri/src/gateway/commands.rs`):
| 命令 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `zclaw_status` | — | `LocalGatewayStatus` | Kernel 运行状态 |
| `zclaw_start` | — | `LocalGatewayStatus` | 启动 Kernel |
| `zclaw_stop` | — | `LocalGatewayStatus` | 停止 Kernel |
| `zclaw_restart` | — | `LocalGatewayStatus` | 重启 Kernel |
| `zclaw_local_auth` | — | `LocalGatewayAuth` | 获取本地认证 token |
| `zclaw_prepare_for_tauri` | — | `LocalGatewayPrepareResult` | 更新 Tauri allowed origins |
| `zclaw_approve_device_pairing` | device_id, public_key_base64, url? | `PairingApprovalResult` | 设备配对审批 |
| `zclaw_doctor` | — | `String` | 诊断报告 |
| `zclaw_process_list` | — | `ProcessListResponse` | 进程列表 |
| `zclaw_process_logs` | pid?, lines? | `ProcessLogsResponse` | 进程日志 |
| `zclaw_version` | — | `VersionResponse` | 版本信息 |
**健康检查** (`desktop/src-tauri/src/health_check.rs`):
| 命令 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `zclaw_health_check` | port?, timeout_ms? | `HealthCheckResponse` | 完整健康检查 |
| `zclaw_ping` | — | `bool` | 快速端口检测 |
### SaaS Relay 路由 (`crates/zclaw-saas/src/relay/`)
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| POST | `/api/v1/relay/chat/completions` | 认证+配额 | 主聊天中转 |
| GET | `/api/v1/relay/models` | 认证 | 可用模型列表 |
| GET | `/api/v1/relay/tasks` | 认证 | 任务列表 |
| GET | `/api/v1/relay/tasks/:id` | 认证 | 任务详情 |
| POST | `/api/v1/relay/tasks/:id/retry` | admin | 重试失败任务 |
### Provider Key 管理 (`crates/zclaw-saas/src/relay/handlers.rs`)
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/api/v1/providers/:id/keys` | admin | 列出 Provider Key |
| POST | `/api/v1/providers/:id/keys` | admin | 添加加密 Key |
| PUT | `/api/v1/providers/:id/keys/:kid/toggle` | admin | 启停 Key |
| DELETE | `/api/v1/providers/:id/keys/:kid` | admin | 删除 Key |
## 测试链路
| 功能 | 测试文件 | 测试数 | 覆盖状态 |
|------|---------|--------|---------|
| Admin 路由解析 | `tests/desktop/connectionStore.adminRouting.test.ts` | — | ✅ parseAdminRouting() 纯函数 |
| 连接流程 | `tests/desktop/gatewayStore.test.ts` | — | ✅ connect() + 数据加载 |
| GatewayClient | `tests/gateway/ws-client.test.ts` | — | ✅ WS 连接/事件/断连 |
| Mock Server | `tests/fixtures/zclaw-mock-server.ts` | — | ✅ HTTP+WS 模拟 |
## 关联模块
- [[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 注入 |
## 已知问题
-**Tauri invoke 参数名 snake_case vs camelCase** — P1 已修复 (commit f6c5dd2)。Tauri 2.x 默认 `rename_all = "camelCase"`,所有 invoke 调用必须用 camelCase
-**Provider Key 解密失败导致 relay 500** — P1 已修复 (commit b69dc61)。`key_pool.rs` 现在 decrypt 失败时 warn+skip 到下一个 key启动时 `heal_provider_keys()` 自动重新加密有效 key