cc-haha/PLAN.md
程序员阿江(Relakkes) 985b1a8b7d feat: add Provider management system — multi-provider CRUD, activation, and connectivity test
Implement a full Provider management layer compatible with Claude Code's settings.json protocol.

- ProviderService: CRUD + activation + settings.json sync + HTTP connectivity test
- Provider REST API: 8 endpoints (list, get, create, update, delete, activate, test)
- API key masking in GET responses (sk-a****xyz)
- Models API refactored to read from active Provider dynamically
- 42 unit tests + 6 real-config integration tests, all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 01:29:52 +08:00

243 lines
7.6 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.

# 模型配置重构计划 — Provider 管理系统
## 背景
当前项目的模型配置存在以下问题:
- 模型列表硬编码在 `src/server/api/models.ts``AVAILABLE_MODELS` 数组中
- 不支持自定义 Provider供应商
- 不支持自定义 Base URL 和 API Key
- 无法测试模型连通性
- 无法管理多个 Provider 并在它们之间切换
## 设计原则
1. **非侵入性** — 不修改 Claude Code 原生 settings.json 的 schema通过 `env` 字段注入环境变量
2. **简洁** — 不过度设计只实现核心功能Provider CRUD、激活切换、连通性测试
3. **兼容** — 借鉴 cc-switch 的激活机制,通过写入 `settings.json``env` 字段实现 Provider 切换
## 核心机制
Claude Code 的 settings.json 支持 `env` 字段,会在启动时注入到 `process.env`
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com",
"ANTHROPIC_AUTH_TOKEN": "sk-xxx"
},
"model": "claude-opus-4-6"
}
```
**激活 Provider = 将其 baseUrl/apiKey/model 写入 settings.json 的 env 字段**
---
## 数据模型
### Provider 类型定义
```typescript
// src/server/types/provider.ts
interface ProviderModel {
id: string // 模型 ID如 "claude-opus-4-6"
name: string // 显示名称,如 "Opus 4.6"
description?: string // 简短描述
context?: string // 上下文窗口,如 "200k"
}
interface Provider {
id: string // UUID
name: string // 显示名称,如 "Anthropic 官方"、"OpenRouter"
baseUrl: string // API Base URL
apiKey: string // API Key
models: ProviderModel[] // 该 Provider 支持的模型列表
isActive: boolean // 是否为当前激活的 Provider
createdAt: number // 创建时间戳
updatedAt: number // 更新时间戳
notes?: string // 备注
}
```
### 存储格式
文件路径:`~/.claude/providers.json`
```json
{
"providers": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Anthropic 官方",
"baseUrl": "https://api.anthropic.com",
"apiKey": "sk-ant-xxx",
"models": [
{ "id": "claude-opus-4-6", "name": "Opus 4.6", "description": "Most capable", "context": "200k" },
{ "id": "claude-sonnet-4-6", "name": "Sonnet 4.6", "description": "Most efficient", "context": "200k" },
{ "id": "claude-haiku-4-5", "name": "Haiku 4.5", "description": "Fastest", "context": "200k" }
],
"isActive": true,
"createdAt": 1712476800000,
"updatedAt": 1712476800000
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "OpenRouter",
"baseUrl": "https://openrouter.ai/api/v1",
"apiKey": "sk-or-xxx",
"models": [
{ "id": "anthropic/claude-opus-4-6", "name": "Claude Opus 4.6", "context": "200k" }
],
"isActive": false,
"createdAt": 1712476800000,
"updatedAt": 1712476800000
}
],
"activeModel": "claude-opus-4-6",
"version": 1
}
```
---
## 实现步骤
### Step 1: Provider 服务层 (`src/server/services/providerService.ts`)
创建 `ProviderService` 类,负责:
- `listProviders()` — 读取 `~/.claude/providers.json` 并返回 provider 列表
- `getProvider(id)` — 获取单个 provider
- `getActiveProvider()` — 获取当前激活的 provider
- `addProvider(data)` — 添加新 provider自动生成 UUID
- `updateProvider(id, data)` — 更新 provider 信息
- `deleteProvider(id)` — 删除 provider不允许删除激活中的 provider
- `activateProvider(id, modelId)` — 激活 provider 并选择模型
- 将旧 provider 设为 `isActive: false`
- 将新 provider 设为 `isActive: true`
- 写入 `~/.claude/settings.json``env` 字段:
```json
{
"env": {
"ANTHROPIC_BASE_URL": "<provider.baseUrl>",
"ANTHROPIC_AUTH_TOKEN": "<provider.apiKey>"
},
"model": "<modelId>"
}
```
- `testProvider(id)` / `testProviderConfig(baseUrl, apiKey, modelId)` — 连通性测试
- 向 `baseUrl/v1/messages` 发送一个最小请求max_tokens=1, "Hi"
- 返回 `{ success, latencyMs, error?, modelUsed? }`
### Step 2: Provider REST API (`src/server/api/providers.ts`)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/api/providers` | 获取 provider 列表 |
| GET | `/api/providers/:id` | 获取单个 provider |
| POST | `/api/providers` | 添加 provider |
| PUT | `/api/providers/:id` | 更新 provider |
| DELETE | `/api/providers/:id` | 删除 provider |
| POST | `/api/providers/:id/activate` | 激活 provider 并选择模型 |
| POST | `/api/providers/:id/test` | 测试已保存 provider 的连通性 |
| POST | `/api/providers/test` | 测试未保存配置的连通性(用于添加时预检) |
### Step 3: 注册路由 (`src/server/router.ts`)
在 router 中添加 `providers` 路由:
```typescript
case 'providers':
return handleProvidersApi(req, url, segments)
```
### Step 4: 重构 Models API (`src/server/api/models.ts`)
修改现有的 `/api/models` 端点:
- **GET `/api/models`** — 不再返回硬编码列表,而是从当前激活的 Provider 读取模型列表
- **GET `/api/models/current`** — 从 providers.json 的 `activeModel` 读取
- **PUT `/api/models/current`** — 更新 `activeModel` 并同步到 settings.json
保留 Effort Level 相关 API 不变。
### Step 5: Provider 类型定义 (`src/server/types/provider.ts`)
独立的类型文件,包含:
- `Provider` 接口
- `ProviderModel` 接口
- `ProviderTestResult` 接口
- `ProvidersConfig` 接口providers.json 的根类型)
- Zod schema 验证
---
## 文件变更清单
| 操作 | 文件路径 | 说明 |
|------|---------|------|
| 新建 | `src/server/types/provider.ts` | Provider 类型定义和 Zod schema |
| 新建 | `src/server/services/providerService.ts` | Provider 服务层CRUD + 激活 + 测试) |
| 新建 | `src/server/api/providers.ts` | Provider REST API 路由处理 |
| 修改 | `src/server/router.ts` | 注册 `/api/providers` 路由 |
| 修改 | `src/server/api/models.ts` | 从 Provider 动态读取模型列表 |
**总计**: 3 个新文件 + 2 个修改文件
---
## 激活流程图
```
用户选择 Provider "OpenRouter" + 模型 "claude-opus-4-6"
├─ 1. 更新 providers.json
│ - 旧 provider: isActive = false
│ - 新 provider: isActive = true
│ - activeModel = "claude-opus-4-6"
├─ 2. 读取当前 ~/.claude/settings.json
├─ 3. 合并写入 settings.json
│ {
│ ...existingSettings,
│ "env": {
│ ...existingEnv,
│ "ANTHROPIC_BASE_URL": "https://openrouter.ai/api/v1",
│ "ANTHROPIC_AUTH_TOKEN": "sk-or-xxx"
│ },
│ "model": "claude-opus-4-6"
│ }
└─ 4. 返回成功响应
```
## 连通性测试流程
```
POST /api/providers/test
Body: { baseUrl, apiKey, modelId }
├─ 1. 构造最小请求
│ POST {baseUrl}/v1/messages
│ Headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
│ Body: { model: modelId, max_tokens: 1, messages: [{ role: "user", content: "Hi" }] }
├─ 2. 记录开始时间
├─ 3. 发送请求(超时 15 秒)
└─ 4. 返回结果
成功: { success: true, latencyMs: 850, modelUsed: "claude-opus-4-6" }
失败: { success: false, error: "401 Unauthorized", latencyMs: 200 }
```
---
## 不在本次范围内
- 前端 UI 组件(后续单独实现)
- Provider 图标管理
- API Key 加密存储V2 考虑)
- 多 API 格式支持OpenAI 兼容等V2 考虑)
- Provider 导入/导出
- 自动故障转移failover