Files
zclaw_openfang/docs/superpowers/specs/2026-03-27-saas-backend-design.md
iven 44256a511c feat: 增强SaaS后端功能与安全性
refactor: 重构数据库连接使用PostgreSQL替代SQLite
feat(auth): 增加JWT验证的audience和issuer检查
feat(crypto): 添加AES-256-GCM字段加密支持
feat(api): 集成utoipa实现OpenAPI文档
fix(admin): 修复配置项表单验证逻辑
style: 统一代码格式与类型定义
docs: 更新技术栈文档说明PostgreSQL
2026-03-31 00:12:53 +08:00

764 lines
30 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.

# ZCLAW SaaS 后台系统设计规格
> 日期: 2026-03-27
> 状态: 待审阅
> 范围: 独立 SaaS 后端服务 (Rust + Axum) + 独立 Web 管理后台 (React + TypeScript)
## 1. 概述
### 1.1 问题陈述
ZCLAW 当前是纯桌面单用户应用缺少用户账号系统、API 服务端、多租户支持和请求中转能力。所有 LLM 调用直接从桌面端发起API 密钥存储在本地环境变量或 OS Keyring模型配置散落在 TOML 文件和 localStorage 中。这限制了多用户协作、集中管控和资源优化。
### 1.2 设计目标
1. 为 ZCLAW 添加独立 SaaS 后端,提供账号权限、模型配置、请求中转、配置迁移四大能力
2. 桌面端通过简单配置切换即可接入 SaaS`base_url` + 添加 API Token
3. MVP 阶段支持 <100 用户后续可扩展至更大规模
### 1.3 用户决策
| 决策点 | 选择 |
|--------|------|
| 部署形态 | 独立 SaaS 后端服务 |
| 技术栈 | Rust + Axum |
| 实施顺序 | 按依赖顺序: 账号API中转迁移 |
| 管理界面 | 独立 React Web 管理后台 |
| 用户规模 | MVP <100 用户 |
| 支持模型 | 智谱 GLM, 通义千问, Kimi, DeepSeek |
| 代码位置 | 同仓库 workspace 成员 `crates/zclaw-saas/` |
---
## 2. 架构
### 2.1 系统架构图
```
┌─────────────────────┐ ┌─────────────────────────────────────────┐
│ ZCLAW 桌面客户端 │ │ SaaS 后端服务 │
│ (Tauri + React) │ │ (Rust + Axum) │
│ │ │ │
│ OpenAiDriver ──────┼────►│ /api/v1/relay/chat/completions │
│ (改 base_url) │ │ → 权限检查 → 队列 → 转发 → 流式响应 │
│ │ │ │
│ SaaSConfigClient ──┼────►│ /api/v1/config/sync │
│ (配置同步) │ │ → 配置覆盖/冲突检测 │
│ │ │ │
│ Model Picker ──────┼────►│ /api/v1/catalog/models │
│ (模型目录) │ │ → 动态模型列表 │
└─────────────────────┘ └─────────────────────────────────────────┘
┌───────────────┐
│ PostgreSQL │
│ zclaw 数据库 │
└───────────────┘
┌──────────────────────────────────────────────────────┐
│ 管理后台 (React + TypeScript) │
│ /admin/accounts /admin/providers /admin/relay │
│ /admin/roles /admin/usage /admin/migration │
└──────────────────────────────────────────────────────┘
```
### 2.2 Crate 依赖
```
zclaw-types (无依赖,复用 ID/错误/消息类型)
zclaw-saas (→ types + axum/sqlx/argon2/jsonwebtoken)
↑ (通过 API)
desktop/ (→ 通过 HTTP API 调用 SaaS)
saas-admin/ (→ 通过 HTTP API 调用 SaaS)
```
### 2.3 文件结构
```
crates/zclaw-saas/
Cargo.toml
src/
lib.rs # 模块声明
main.rs # 入口
error.rs # SaaS 错误类型
config.rs # SaaS 服务器配置 (TOML)
db.rs # 数据库初始化 + 迁移
state.rs # AppState
auth/
mod.rs
jwt.rs # JWT 创建/验证
password.rs # Argon2 哈希
totp.rs # TOTP 2FA
middleware.rs # Axum 认证中间件
account/
mod.rs
types.rs # Account/Role/Permission 类型
handlers.rs # HTTP 处理器
service.rs # 业务逻辑
model_config/
mod.rs
types.rs # Provider/Model/APIKey 类型
handlers.rs # HTTP 处理器
service.rs # 业务逻辑
relay/
mod.rs
types.rs # 请求/响应/队列类型
handlers.rs # HTTP 处理器 (代理端点)
service.rs # 队列管理、调度、批处理
provider_impl.rs # 提供商特定转发逻辑
migration/
mod.rs
types.rs # ConfigItem/SyncRecord 类型
handlers.rs # HTTP 处理器
service.rs # 配置分析和同步逻辑
saas-admin/ # 独立 React 管理后台
package.json
src/
App.tsx
pages/
Login.tsx
Dashboard.tsx
accounts/
providers/
api-keys/
usage/
relay/
migration/
roles/
logs/
```
---
## 3. 数据库设计
### 3.1 概述
- 引擎: PostgreSQL 16
- 连接: 通过 `DATABASE_URL` 环境变量配置 (推荐) `saas-config.toml` 中指定
- 迁移: 版本化 schema启动时自动迁移
### 3.2 完整 Schema
```sql
-- Schema 版本控制
CREATE TABLE IF NOT EXISTS saas_schema_version (
version INTEGER PRIMARY KEY
);
-- ============================================================
-- 模块一: 账号权限
-- ============================================================
CREATE TABLE IF NOT EXISTS accounts (
id TEXT PRIMARY KEY, -- UUID v4
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, -- Argon2id
display_name TEXT NOT NULL DEFAULT '',
avatar_url TEXT,
role TEXT NOT NULL DEFAULT 'user', -- 'super_admin' | 'admin' | 'user'
status TEXT NOT NULL DEFAULT 'active', -- 'active' | 'disabled' | 'suspended'
totp_secret TEXT, -- 加密存储
totp_enabled BOOLEAN NOT NULL DEFAULT false,
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email);
CREATE INDEX IF NOT EXISTS idx_accounts_role ON accounts(role);
CREATE TABLE IF NOT EXISTS api_tokens (
id TEXT PRIMARY KEY, -- UUID
account_id TEXT NOT NULL,
name TEXT NOT NULL, -- e.g. "桌面客户端"
token_hash TEXT NOT NULL, -- SHA256(token)
token_prefix TEXT NOT NULL, -- 前 8 字符用于展示
permissions TEXT NOT NULL DEFAULT '[]', -- JSON 权限数组
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ, -- NULL = 永不过期
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_api_tokens_account ON api_tokens(account_id);
CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash);
CREATE TABLE IF NOT EXISTS roles (
id TEXT PRIMARY KEY, -- 'super_admin' | 'admin' | 'user' | 自定义 UUID
name TEXT NOT NULL, -- 显示名称 (中文)
description TEXT,
permissions TEXT NOT NULL DEFAULT '[]', -- JSON 权限数组
is_system BOOLEAN NOT NULL DEFAULT false, -- 系统角色不可删除
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS permission_templates (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL, -- e.g. "标准用户", "只读用户"
description TEXT,
permissions TEXT NOT NULL DEFAULT '[]', -- JSON 权限数组
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS operation_logs (
id BIGSERIAL PRIMARY KEY,
account_id TEXT, -- NULL = 系统操作
action TEXT NOT NULL, -- e.g. "account.create", "model.update"
target_type TEXT, -- e.g. "account", "api_key", "model"
target_id TEXT,
details TEXT, -- JSON 详情
ip_address TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_op_logs_account ON operation_logs(account_id);
CREATE INDEX IF NOT EXISTS idx_op_logs_action ON operation_logs(action);
CREATE INDEX IF NOT EXISTS idx_op_logs_time ON operation_logs(created_at);
-- ============================================================
-- 模块二: 模型配置
-- ============================================================
CREATE TABLE IF NOT EXISTS providers (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL UNIQUE, -- e.g. 'zhipu', 'qwen', 'kimi', 'deepseek'
display_name TEXT NOT NULL, -- e.g. '智谱 AI'
api_key TEXT, -- 服务端提供商 API 密钥 (加密存储)
base_url TEXT NOT NULL,
api_protocol TEXT NOT NULL DEFAULT 'openai', -- 'openai' | 'anthropic'
enabled BOOLEAN NOT NULL DEFAULT true,
rate_limit_rpm INTEGER, -- 每分钟请求数
rate_limit_tpm INTEGER, -- 每分钟 token 数
config_json TEXT DEFAULT '{}', -- 提供商特定配置
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS models (
id TEXT PRIMARY KEY, -- UUID
provider_id TEXT NOT NULL,
model_id TEXT NOT NULL, -- 传给 API 的模型标识
alias TEXT NOT NULL, -- 显示名称
context_window INTEGER NOT NULL DEFAULT 8192,
max_output_tokens INTEGER NOT NULL DEFAULT 4096,
supports_streaming BOOLEAN NOT NULL DEFAULT true,
supports_vision BOOLEAN NOT NULL DEFAULT false,
enabled BOOLEAN NOT NULL DEFAULT true,
pricing_input DOUBLE PRECISION DEFAULT 0, -- 每 1K token 价格
pricing_output DOUBLE PRECISION DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(provider_id, model_id),
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_models_provider ON models(provider_id);
CREATE TABLE IF NOT EXISTS account_api_keys (
id TEXT PRIMARY KEY, -- UUID
account_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
key_value TEXT NOT NULL, -- API 密钥 (加密存储)
key_label TEXT, -- e.g. "主密钥", "备用密钥"
permissions TEXT NOT NULL DEFAULT '[]', -- JSON: 可访问的模型 ID 列表
enabled BOOLEAN NOT NULL DEFAULT true,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_account_api_keys_account ON account_api_keys(account_id);
CREATE TABLE IF NOT EXISTS usage_records (
id BIGSERIAL PRIMARY KEY,
account_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
model_id TEXT NOT NULL,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
latency_ms INTEGER,
status TEXT NOT NULL DEFAULT 'success', -- 'success' | 'error' | 'rate_limited'
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usage_account ON usage_records(account_id);
CREATE INDEX IF NOT EXISTS idx_usage_time ON usage_records(created_at);
-- ============================================================
-- 模块三: 中转服务
-- ============================================================
CREATE TABLE IF NOT EXISTS relay_tasks (
id TEXT PRIMARY KEY, -- UUID
account_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
model_id TEXT NOT NULL,
request_hash TEXT NOT NULL, -- 用于去重
status TEXT NOT NULL DEFAULT 'queued', -- 'queued' | 'processing' | 'completed' | 'failed'
priority INTEGER NOT NULL DEFAULT 0,
attempt_count INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
request_body TEXT NOT NULL, -- JSON: 转发的请求体
response_body TEXT, -- JSON: 响应体
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
error_message TEXT,
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_relay_status ON relay_tasks(status);
CREATE INDEX IF NOT EXISTS idx_relay_account ON relay_tasks(account_id);
CREATE INDEX IF NOT EXISTS idx_relay_provider ON relay_tasks(provider_id);
-- ============================================================
-- 模块四: 配置迁移
-- ============================================================
CREATE TABLE IF NOT EXISTS config_items (
id TEXT PRIMARY KEY, -- UUID
category TEXT NOT NULL, -- 'llm' | 'agent' | 'security' | 'hands' | 'skills' | 'tools' | 'logging' | 'desktop' | 'server'
key_path TEXT NOT NULL, -- e.g. 'llm.default_provider'
value_type TEXT NOT NULL, -- 'string' | 'integer' | 'float' | 'boolean' | 'array' | 'object'
current_value TEXT, -- JSON 编码的当前值
default_value TEXT, -- JSON 编码的默认值
source TEXT NOT NULL DEFAULT 'local', -- 'local' | 'saas' | 'override'
description TEXT, -- 中文描述
requires_restart BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(category, key_path)
);
CREATE INDEX IF NOT EXISTS idx_config_category ON config_items(category);
CREATE TABLE IF NOT EXISTS config_sync_log (
id BIGSERIAL PRIMARY KEY,
account_id TEXT NOT NULL,
client_fingerprint TEXT NOT NULL,
action TEXT NOT NULL, -- 'push' | 'pull' | 'conflict'
config_keys TEXT NOT NULL, -- JSON: 受影响的配置键
client_values TEXT, -- JSON: 客户端值
saas_values TEXT, -- JSON: SaaS 值
resolution TEXT, -- 'client_wins' | 'saas_wins' | 'manual'
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_sync_account ON config_sync_log(account_id);
```
---
## 4. 模块一: 账号权限管理
### 4.1 认证机制
| 机制 | 实现 | 用途 |
|------|------|------|
| 密码 | Argon2id (argon2 crate) | 账号登录 |
| JWT | HMAC-SHA256 (jsonwebtoken crate) | 管理后台会话 |
| TOTP 2FA | totp-rs crate | 可选双因素认证 |
| API Token | SHA-256 哈希存储 | 桌面客户端/程序化访问 |
**JWT Payload**: `{ sub: account_id, role: "admin", exp: timestamp }`
**API Token 格式**: `zclaw_<48 random bytes>`仅创建时展示一次
### 4.2 认证中间件
```rust
// auth/middleware.rs
pub struct AuthContext {
pub account_id: Uuid,
pub role: AccountRole,
pub permissions: Vec<String>,
pub auth_method: AuthMethod, // Jwt | ApiToken
}
// 中间件从 Authorization: Bearer <jwt> 或 X-API-Key: <token> 提取身份
// 将 AuthContext 注入 request extensions
```
### 4.3 角色与权限
权限使用字符串常量检查时匹配字符串列表:
```rust
pub mod permissions {
pub const ACCOUNT_READ: &str = "account:read";
pub const ACCOUNT_WRITE: &str = "account:write";
pub const ACCOUNT_ADMIN: &str = "account:admin";
pub const MODEL_READ: &str = "model:read";
pub const MODEL_WRITE: &str = "model:write";
pub const RELAY_USE: &str = "relay:use";
pub const RELAY_ADMIN: &str = "relay:admin";
pub const CONFIG_READ: &str = "config:read";
pub const CONFIG_WRITE: &str = "config:write";
pub const ADMIN_FULL: &str = "admin:full";
}
```
| 角色 | 权限 |
|------|------|
| `super_admin` | `admin:full` (授权检查时直接放行) |
| `admin` | account:read/write, model:read/write, relay:use/admin, config:read/write |
| `user` | model:read, relay:use, config:read |
### 4.4 API 端点
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| POST | `/api/v1/auth/register` | admin:full | 创建账号 |
| POST | `/api/v1/auth/login` | 公开 | 登录返回 JWT |
| POST | `/api/v1/auth/refresh` | JWT | 刷新 JWT |
| POST | `/api/v1/auth/totp/setup` | JWT | 获取 TOTP 设置 URI |
| POST | `/api/v1/auth/totp/verify` | JWT | 验证并启用 TOTP |
| POST | `/api/v1/auth/totp/disable` | JWT | 禁用 TOTP |
| GET | `/api/v1/accounts` | account:read | 账号列表 |
| GET | `/api/v1/accounts/:id` | account:read | 账号详情 |
| PUT | `/api/v1/accounts/:id` | account:write | 更新账号 |
| PATCH | `/api/v1/accounts/:id/status` | account:admin | 启用/禁用 |
| GET | `/api/v1/tokens` | - (本人) | 列出 API 令牌 |
| POST | `/api/v1/tokens` | - (本人) | 创建令牌 |
| DELETE | `/api/v1/tokens/:id` | - (本人) | 撤销令牌 |
| GET | `/api/v1/roles` | account:read | 角色列表 |
| POST | `/api/v1/roles` | account:admin | 创建自定义角色 |
| PUT | `/api/v1/roles/:id` | account:admin | 更新角色权限 |
| DELETE | `/api/v1/roles/:id` | account:admin | 删除自定义角色 |
| GET | `/api/v1/permission-templates` | account:read | 模板列表 |
| POST | `/api/v1/permission-templates` | account:admin | 创建模板 |
| POST | `/api/v1/permission-templates/:id/apply` | account:admin | 批量应用 |
| GET | `/api/v1/logs/operations` | account:read | 操作日志 (分页) |
### 4.5 关键数据结构
```rust
pub struct Account {
pub id: Uuid,
pub username: String,
pub email: String,
pub password_hash: String, // skip_serializing
pub display_name: String,
pub avatar_url: Option<String>,
pub role: AccountRole,
pub status: AccountStatus,
pub totp_enabled: bool,
pub last_login_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub enum AccountRole { SuperAdmin, Admin, User }
pub enum AccountStatus { Active, Disabled, Suspended }
```
---
## 5. 模块二: 模型与 API 配置中心
### 5.1 提供商管理
提供商存储服务端 API 密钥 (加密) 和基础配置管理员创建提供商后可为每个提供商添加多个模型
**API 密钥加密**: AES-256-GCM密钥通过 HKDF-SHA256 从服务器 JWT 密钥派生
### 5.2 账号级 API 密钥
每个账号可以为每个提供商配置独立的 API 密钥密钥权限 (JSON 数组) 控制该密钥可访问的模型 ID 列表支持:
- 生成: 创建新密钥返回明文 (仅一次)
- 轮换: 生成新密钥替换旧密钥
- 禁用: 临时禁用
- 撤销: 永久删除
### 5.3 使用量统计
每次中转请求完成后记录 `usage_records`统计查询使用 SQL 聚合MVP 阶段不需要预计算视图
### 5.4 API 端点
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/api/v1/providers` | model:read | 提供商列表 |
| POST | `/api/v1/providers` | model:write | 创建提供商 |
| GET | `/api/v1/providers/:id` | model:read | 提供商详情 |
| PUT | `/api/v1/providers/:id` | model:write | 更新提供商 |
| PATCH | `/api/v1/providers/:id/status` | model:write | 启用/禁用 |
| GET | `/api/v1/providers/:id/models` | model:read | 模型列表 |
| POST | `/api/v1/providers/:id/models` | model:write | 添加模型 |
| PUT | `/api/v1/models/:id` | model:write | 更新模型 |
| PATCH | `/api/v1/models/:id/status` | model:write | 启用/禁用模型 |
| GET | `/api/v1/account/api-keys` | - (本人) | 密钥列表 |
| POST | `/api/v1/account/api-keys` | - (本人) | 创建密钥 |
| PUT | `/api/v1/account/api-keys/:id` | - (本人) | 更新权限/标签 |
| POST | `/api/v1/account/api-keys/:id/rotate` | - (本人) | 轮换密钥 |
| PATCH | `/api/v1/account/api-keys/:id/status` | - (本人) | 启用/禁用 |
| DELETE | `/api/v1/account/api-keys/:id` | - (本人) | 撤销密钥 |
| GET | `/api/v1/usage/stats` | relay:admin | 总体统计 |
| GET | `/api/v1/usage/daily` | - (本人) | 每日统计 |
| GET | `/api/v1/usage/account/:id` | relay:admin | 账号统计 |
| GET | `/api/v1/catalog/models` | model:read | 公开模型目录 |
### 5.5 初始种子数据
`config/chinese-providers.toml` 导入:
| 提供商 | 模型 |
|--------|------|
| 智谱 AI | GLM-4-Plus, GLM-4-Flash, GLM-4V-Plus, GLM-Z1-AirX |
| 通义千问 | Qwen-Max, Qwen-Plus, Qwen-Turbo, Qwen-VL-Max |
| Kimi | moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k |
| DeepSeek | deepseek-chat, deepseek-reasoner |
---
## 6. 模块三: 模型请求中转服务
### 6.1 请求流
```
桌面客户端
POST /api/v1/relay/chat/completions
Headers: Authorization: Bearer <api_token> 或 X-API-Key: <token>
Body: { model: "glm-4-plus", messages: [...], stream: true }
认证中间件 → 提取 account_id + permissions
权限检查 → relay:use
提供商解析 → models 表查找 provider_id → providers 表获取 base_url + api_protocol
API 密钥解析 → account_api_keys 表获取该账号的提供商密钥
速率限制检查 → 令牌桶算法
队列调度 → tokio::sync::mpsc (如并发已满则排队)
转发请求 → reqwest::Client POST 到上游提供商
流式响应 → SSE 转发给客户端 (stream: true)
记录使用量 → usage_records 表
```
### 6.2 队列与调度
```rust
pub struct RelayService {
queues: Arc<DashMap<Uuid, mpsc::Sender<RelayTask>>>, // 每提供商独立队列
semaphores: Arc<DashMap<Uuid, Arc<Semaphore>>>, // 并发控制
clients: Arc<DashMap<Uuid, reqwest::Client>>, // 每提供商 HTTP 客户端
config: RelayConfig,
}
```
- 每提供商一个 mpsc channel (容量 100)
- 每提供商一个 Semaphore 控制并发 (默认 5)
- Worker task channel 拉取请求并执行
### 6.3 速率限制
令牌桶算法:
- RPM (每分钟请求数): `tokio::sync::Semaphore` + 定时释放
- TPM (每分钟 token ): 基于最近一分钟 `usage_records` SQL 查询
### 6.4 错误处理与重试
- 瞬态错误 (超时, 5xx): 指数退避重试 (1s, 2s, 4s)最多 3
- 客户端错误 (4xx, 认证失败): 立即返回不重试
- 提供商不可用: 标记 provider disabled通知管理员
### 6.5 API 端点
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| POST | `/api/v1/relay/chat/completions` | relay:use | 中转聊天补全 (支持流式) |
| GET | `/api/v1/relay/status` | relay:admin | 队列状态/提供商健康 |
| GET | `/api/v1/relay/tasks` | relay:admin | 任务列表 |
| GET | `/api/v1/relay/tasks/:id` | relay:admin | 任务详情 |
| DELETE | `/api/v1/relay/tasks/:id` | relay:admin | 取消排队任务 |
| POST | `/api/v1/relay/retry/:id` | relay:admin | 重试失败任务 |
### 6.6 桌面端集成
桌面端 `OpenAiDriver` `base_url` 指向 SaaS 中转:
```
原来: base_url = "https://open.bigmodel.cn/api/paas/v4"
改为: base_url = "https://saas.zclaw.example.com/api/v1/relay"
添加: X-API-Key: zclaw_xxxxx (账号的 API Token)
```
中转保持 OpenAI 兼容格式桌面端无需任何协议适配
---
## 7. 模块四: 系统配置迁移分析
### 7.1 迁移优先级分类
基于 `config/config.toml``config/chinese-providers.toml``config/security.toml` 分析:
**Critical (必须迁移)**:
- `llm.providers[].api_key` API 密钥应服务端管理
- `llm.providers[].base_url` 提供商端点集中管控
- `llm.providers[].models[]` 模型目录从 SaaS 下发
- `llm.default_provider` 默认提供商可按账号配置
- `llm.default_model` 默认模型可按账号配置
**High (重要)**:
- `llm.requests_per_minute` / `llm.tokens_per_minute` 按账号限流
- `llm.max_retries` / `llm.retry_delay` 重试策略
- `security.auth.token_expiration` 认证配置
- `security.rate_limit.*` 限流配置
**Medium (有益)**:
- `agent.defaults.max_sessions` 会话限制
- `hands.default_approval_mode` 默认审批模式
- `skills.execution_timeout` 技能超时
**Low (可选)**:
- `desktop.ui.*` UI 偏好
- `logging.level` 日志级别
**Local Only (不迁移)**:
- `server.host/port` 服务端特有
- `agent.defaults.workspace` 本地文件路径
- `security.shell_exec.*` 本地安全策略
- `tools.fs.allowed_paths` 本地文件路径
- `logging.file.path` 本地文件路径
### 7.2 同步协议
```
桌面客户端 SaaS 服务端
│ │
│ POST /api/v1/config/sync │
│ { action: "push", │
│ fingerprint: "machine_xxx", │
│ snapshot: { ... } } │
│ ──────────────────────────────► │
│ │ 比对 SaaS 存储
│ { updates: [...], │
│ conflicts: [...] } │
│ ◄────────────────────────────── │
│ │
│ 应用更新 / 提示冲突 │
│ │
```
**冲突解决策略**:
- `source: "saas"` / `"override"` 默认 saas_wins
- `source: "local"` 默认 client_wins
- 管理员可按配置项覆盖
### 7.3 API 端点
| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/api/v1/migration/report` | config:read | 迁移分析报告 |
| POST | `/api/v1/migration/analyze` | config:read | 分析配置快照 |
| GET | `/api/v1/migration/items` | config:read | 配置项列表 |
| PUT | `/api/v1/migration/items/:id` | config:write | 更新配置项 |
| PUT | `/api/v1/migration/items/batch` | config:write | 批量更新 |
| POST | `/api/v1/config/sync` | - (本人) | 客户端SaaS 同步 |
| GET | `/api/v1/config/snapshot` | config:read | SaaS 配置快照 |
| GET | `/api/v1/config/diff` | config:read | 客户端与 SaaS 差异 |
---
## 8. 管理后台前端
### 8.1 技术栈
React 18 + TypeScript + Tailwind CSS + Recharts + React Router
### 8.2 页面清单
| 路径 | 模块 | 说明 |
|------|------|------|
| `/admin/login` | 通用 | 登录页 (JWT 认证) |
| `/admin/dashboard` | 通用 | 概览仪表板 (账号数/使用量/中转状态) |
| `/admin/accounts` | M1 | 账号列表 (搜索/角色筛选/状态筛选) |
| `/admin/accounts/:id` | M1 | 账号详情 (编辑/查看令牌/查看日志) |
| `/admin/roles` | M1 | 角色权限矩阵编辑 |
| `/admin/templates` | M1 | 权限模板 CRUD + 批量应用 |
| `/admin/logs` | M1 | 操作日志 (分页/筛选/时间范围) |
| `/admin/profile` | M1 | 个人设置 (密码/2FA/显示名) |
| `/admin/providers` | M2 | 提供商卡片 (状态/模型数) |
| `/admin/providers/:id` | M2 | 提供商详情/编辑 |
| `/admin/providers/:id/models` | M2 | 模型表格 (添加/编辑/禁用) |
| `/admin/api-keys` | M2 | API 密钥管理 (创建/轮换/撤销) |
| `/admin/usage` | M2 | 使用量仪表板 (图表/按提供商/按模型/按日期) |
| `/admin/relay/dashboard` | M3 | 中转状态 (队列/提供商健康/实时指标) |
| `/admin/relay/tasks` | M3 | 任务列表 (状态筛选/取消/重试) |
| `/admin/migration/report` | M4 | 迁移分析报告 (分类统计/优先级图表) |
| `/admin/migration/items` | M4 | 配置项管理 (分类/优先级/来源筛选) |
| `/admin/config/sync-log` | M4 | 同步日志 (冲突详情/解决记录) |
---
## 9. 实施阶段
### Phase 1: 基础 + 账号模块
1. `crates/zclaw-saas/` 基础结构 (Cargo.toml, db.rs, config.rs, state.rs, error.rs)
2. `auth/` 模块 (JWT, Argon2, TOTP, 中间件)
3. `account/` 模块 (CRUD, 角色, 模板, 操作日志)
4. `saas-admin/` 脚手架 (登录 + 账号管理页面)
5. 验证: 注册登录创建 API Token操作日志
### Phase 2: 模型配置模块
1. `model_config/` 模块 (提供商/模型 CRUD)
2. 种子数据导入 (`chinese-providers.toml`)
3. 账号 API 密钥管理 + 使用量统计
4. 管理后台: 提供商/模型/API 密钥/使用量
5. 验证: 创建提供商添加模型分配密钥查看使用量
### Phase 3: 中转服务模块
1. `relay/` 模块 (队列, 调度, 流式转发)
2. 速率限制 + 重试逻辑
3. 管理后台: 中转仪表板/任务列表
4. 桌面端集成 ( `base_url`)
5. 验证: 桌面端中转提供商流式响应使用量记录
### Phase 4: 配置迁移模块
1. `migration/` 模块 (配置分析, 同步协议)
2. 种子配置项 ( TOML 文件分析)
3. 桌面端 `SaaSConfigClient`
4. 更新 `KernelConfig::load()` 支持 SaaS 覆盖
5. 验证: 配置同步覆盖冲突检测解决
---
## 10. 验证方案
### 每阶段验证
1. `cargo build -p zclaw-saas` 编译通过
2. `cargo test -p zclaw-saas` 单元测试通过
3. `curl` 测试每个 API 端点
4. 管理后台操作 验证数据库状态
### 最终端到端验证
1. **完整流程**: 注册账号 登录 创建 API Token 配置提供商 桌面端通过中转调用模型 查看使用量 配置同步
2. **安全验证**: JWT 过期拒绝权限不足拒绝API Token 撤销后拒绝TOTP 强制验证
3. **错误处理**: 提供商不可用降级速率限制触发排队队列溢出错误配置冲突检测
---
## 11. 关键参考文件
| 文件 | 用途 |
|------|------|
| `crates/zclaw-kernel/src/config.rs` | LlmConfig/ApiProtocol 模式参考 |
| `crates/zclaw-memory/src/schema.rs` | SQL schema 模式参考 |
| `crates/zclaw-runtime/src/driver/openai.rs` | 中转转发实现参考 |
| `crates/zclaw-runtime/src/driver/mod.rs` | LlmDriver trait 参考 |
| `config/chinese-providers.toml` | 提供商种子数据源 |
| `config/config.toml` | 配置迁移分析源 |
| `config/security.toml` | 安全配置迁移分析源 |
| `desktop/src-tauri/src/secure_storage.rs` | 密钥加密存储参考 |
| `desktop/src-tauri/src/lib.rs` | Tauri 命令模式参考 |