Files
metapi/PR_PREPARATION.md
fa25a501a8 feat: 实现全局模型白名单功能 (#301)
* feat: 实现全局模型白名单功能

## 功能概述
实现全局模型白名单,允许管理员配置只启用特定模型的路由,实现精细化的模型访问控制。

## 主要变更

### 后端实现
- config.ts: 新增 globalAllowedModels 配置项
- settings.ts:
  - 新增 RuntimeSettingsBody 类型支持
  - 实现数据库加载和持久化
  - 配置变更自动触发路由重建
- modelService.ts:
  - 在路由重建中添加白名单过滤逻辑
  - 大小写不敏感匹配,自动trim空格
  - 空白名单时允许所有模型(向后兼容)
- stats.ts:
  - 候选API过滤 models/modelsWithoutToken/modelsMissingTokenGroups

### 前端实现
- Settings.tsx:
  - 新增"全局模型白名单"配置卡片
  - 支持手动输入和点击选择两种添加方式
  - 绿色徽章显示已选模型,支持删除
  - 保存后自动触发路由重建

## 技术特性
- ✅ 向后兼容(默认为空数组,允许所有模型)
- ✅ 大小写不敏感匹配
- ✅ 自动trim和去重
- ✅ 输入验证(类型检查、空值过滤)
- ✅ 配置变更自动重建路由

## 测试验证
- 白名单为空时所有模型可见
- 白名单有值时只显示白名单模型
- 路由重建正确过滤模型
- 候选API正确返回过滤结果
- 前端UI交互正常

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add getModelTokenCandidates mock to Settings tests

* fix: add getModelTokenCandidates mock to proxy-transport test

---------

Co-authored-by: Ding <ding@local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 17:19:02 +08:00

253 lines
7.0 KiB
Markdown
Raw Permalink 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.
# 模型白名单功能 - PR 准备文档
## 功能概述
实现全局模型白名单功能,允许管理员配置只启用特定模型的路由。当白名单配置后,不在白名单中的模型路由会被自动移除,实现精细化的模型访问控制。
## 业务场景
- **成本控制**: 只允许使用价格合理的模型
- **合规要求**: 限制只使用审批通过的模型
- **简化管理**: 隐藏不需要的模型,减少用户困惑
- **测试验证**: 在特定场景下只开放测试模型
## 技术实现
### 1. 配置层 (src/server/config.ts)
```typescript
globalAllowedModels: [] as string[]
```
- 默认为空数组(向后兼容)
- 存储在内存中,启动时从数据库加载
### 2. 数据持久化 (src/server/routes/api/settings.ts)
**数据库存储:**
- Key: `global_allowed_models`
- Value: JSON 字符串数组
**API 接口:**
#### GET /api/settings/runtime
返回当前白名单配置
#### PUT /api/settings/runtime
```json
{
"globalAllowedModels": ["gpt-4", "gpt-4o", "claude-3-sonnet"]
}
```
**处理流程:**
1. 验证输入(必须是字符串数组)
2. 去重、trim、过滤空值
3. 比较新旧配置
4. 更新数据库和内存配置
5. 如果配置变更,自动触发路由重建
### 3. 路由重建 (src/server/services/modelService.ts)
**核心函数:** `rebuildTokenRoutesFromAvailability()`
```typescript
// 加载白名单
const globalAllowedModels = new Set(
config.globalAllowedModels.map(m => m.toLowerCase().trim()).filter(Boolean)
);
// 判断模型是否允许
function isModelAllowedByWhitelist(modelName: string): boolean {
if (globalAllowedModels.size === 0) return true; // 向后兼容
return globalAllowedModels.has(modelName.toLowerCase().trim());
}
// 添加模型候选时过滤
const addModelCandidate = (modelNameRaw, accountId, tokenId, siteId) => {
const modelName = (modelNameRaw || '').trim();
if (!modelName) return;
if (!isModelAllowedByWhitelist(modelName)) return; // 白名单过滤
// ... 其他过滤逻辑
};
```
**处理逻辑:**
1. 从 `token_model_availability` 加载所有可用模型
2. 通过 `addModelCandidate()` 过滤,只保留白名单中的模型
3. 创建/更新路由和渠道
4. 删除不在白名单中的旧路由
### 4. 模型候选API (src/server/routes/api/stats.ts)
**接口:** GET /api/models/token-candidates
**返回字段:**
- `models`: 有token的模型
- `modelsWithoutToken`: 无token的模型
- `modelsMissingTokenGroups`: 缺少token分组的模型
**过滤逻辑:**
```typescript
if (globalAllowedModels.size > 0) {
// 白名单模式:只返回白名单中的模型
for (const [modelName, candidates] of Object.entries(result)) {
if (globalAllowedModels.has(modelName.toLowerCase().trim())) {
filteredResult[modelName] = candidates;
}
}
} else {
// 向后兼容:返回所有模型
Object.assign(filteredResult, result);
}
```
### 5. 前端UI (src/web/pages/Settings.tsx)
**新增卡片:** "全局模型白名单"
**功能特性:**
- 输入框手动添加模型名称(支持回车)
- 可用模型列表展示(点击快速添加)
- 已选模型徽章显示(绿色徽章)
- 点击 × 删除模型
- "保存模型白名单"按钮
- 保存成功后显示提示并自动刷新页面
**交互流程:**
1. 加载当前白名单配置
2. 加载可用模型列表
3. 用户添加/删除模型
4. 点击保存触发API调用
5. 后端自动重建路由
6. 前端显示成功提示
## 数据流程图
```
用户输入 → Settings API
↓
数据库存储 (global_allowed_models)
↓
内存配置更新 (config.globalAllowedModels)
↓
触发路由重建 (异步后台任务)
↓
加载 token_model_availability
↓
白名单过滤 (isModelAllowedByWhitelist)
↓
创建/更新/删除路由
↓
候选API过滤 (token-candidates)
↓
前端显示过滤后的模型列表
```
## 向后兼容性
| 场景 | 行为 |
|------|------|
| 白名单为空 | 允许所有模型(原有行为) |
| 白名单有值 | 只允许白名单中的模型 |
| 数据库无配置 | 默认为空数组 |
| API未传该字段 | 不修改现有配置 |
## 测试验证
### 功能测试清单
- [x] 白名单为空时,所有模型可见
- [x] 设置白名单后,只显示白名单模型
- [x] 大小写不敏感匹配(GPT-4 = gpt-4)
- [x] 自动trim空格
- [x] 保存后自动触发路由重建
- [x] 路由重建成功后,路由正确过滤
- [x] 候选API正确过滤三个返回字段
- [ ] 前端UI交互正常
- [ ] 输入框支持回车添加
- [ ] 可用模型列表正确显示
- [ ] 点击模型快速添加
- [ ] 删除模型功能正常
- [ ] 保存成功提示显示
### API 测试
```bash
# 查看白名单
curl -H "Authorization: Bearer test-admin-token" \
http://localhost:4000/api/settings/runtime
# 设置白名单
curl -X PUT \
-H "Authorization: Bearer test-admin-token" \
-H "Content-Type: application/json" \
-d '{"globalAllowedModels": ["gpt-4", "gpt-4o"]}' \
http://localhost:4000/api/settings/runtime
# 查看模型候选
curl -H "Authorization: Bearer test-admin-token" \
http://localhost:4000/api/models/token-candidates
```
## 文件修改列表
| 文件 | 修改内容 | 行数 |
|------|---------|-----|
| src/server/config.ts | 新增 globalAllowedModels 配置 | 1 |
| src/server/routes/api/settings.ts | 类型定义、加载、保存、触发重建 | ~80 |
| src/server/services/modelService.ts | 白名单过滤逻辑 | ~20 |
| src/server/routes/api/stats.ts | 候选API过滤 | ~30 |
| src/web/pages/Settings.tsx | 前端UI组件 | ~150 |
**总计:** 约 280 行代码新增/修改
## 性能影响
- **内存:** 新增一个 Set 数据结构,大小为白名单模型数量
- **路由重建:** 无额外开销(仅增加一个 O(1) 的 Set 查找)
- **候选API:** 增加过滤循环,复杂度 O(n),n为模型数量
**结论:** 性能影响可忽略不计
## 安全考虑
- ✅ 仅管理员可配置(需要 admin token)
- ✅ 输入验证(类型检查、trim、去重)
- ✅ 无SQL注入风险(使用 ORM)
- ✅ 配置变更自动记录日志
## 后续优化建议
1. **批量导入**: 支持从文件批量导入白名单
2. **预设模板**: 提供常用模型组合模板
3. **分组管理**: 支持多个白名单分组
4. **权限控制**: 不同用户看到不同白名单
5. **审计日志**: 记录白名单变更历史
## 已知限制
1. 白名单只支持精确匹配,不支持通配符或正则
2. 模型名称大小写不敏感,但保留原始大小写显示
3. 白名单全局生效,不支持按站点或账号单独配置
## 相关 Issue
- Issue #XXX: 模型访问控制需求
- Issue #XXX: 简化模型列表显示
## 测试环境
- Node.js: v20.x
- pnpm: 8.x
- SQLite: 3.x
- 测试服务器: http://localhost:4000
- 真实模型数据已验证
## 下一步计划
1. ✅ 完成功能开发和测试
2. ✅ 编写PR文档
3. ⏳ 等待用户前端测试验证
4. ⏳ 合并到主仓库
5. ⏳ 开始第二个功能:新站点跳转选择