Compare commits
3 Commits
3fa5827de3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f3a207fa16 | |||
| 56ecc73d1b | |||
| 1ae9336cbe |
@@ -26,7 +26,7 @@ AI 网关后端服务,提供统一的大模型 API 代理接口。
|
||||
- **ORM**: GORM
|
||||
- **数据库**: SQLite
|
||||
- **日志**: zap + lumberjack
|
||||
- **配置**: gopkg.in/yaml.v3
|
||||
- **配置**: Viper + pflag(多层配置:CLI > 环境变量 > 配置文件 > 默认值)
|
||||
- **验证**: go-playground/validator/v10
|
||||
- **迁移**: goose
|
||||
|
||||
@@ -39,7 +39,7 @@ backend/
|
||||
│ └── main.go # 主程序入口(依赖注入)
|
||||
├── internal/
|
||||
│ ├── config/ # 配置管理
|
||||
│ │ ├── config.go # 配置加载/保存/验证
|
||||
│ │ ├── config.go # Viper 多层配置加载/验证
|
||||
│ │ └── models.go # GORM 数据模型
|
||||
│ ├── domain/ # 领域模型
|
||||
│ │ ├── provider.go
|
||||
@@ -47,17 +47,17 @@ backend/
|
||||
│ │ ├── stats.go
|
||||
│ │ └── route.go
|
||||
│ ├── handler/ # HTTP 处理器
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ ├── middleware/ # 中间件
|
||||
│ │ │ ├── request_id.go
|
||||
│ │ │ ├── logging.go
|
||||
│ │ │ ├── recovery.go
|
||||
│ │ │ └── cors.go
|
||||
│ │ ├── proxy_handler.go # 统一代理处理器
|
||||
│ │ ├── proxy_handler.go # 统一代理处理器
|
||||
│ │ ├── provider_handler.go
|
||||
│ │ ├── model_handler.go
|
||||
│ │ └── stats_handler.go
|
||||
│ ├── conversion/ # 协议转换引擎
|
||||
│ │ ├── canonical/ # Canonical Model
|
||||
│ ├── conversion/ # 协议转换引擎
|
||||
│ │ ├── canonical/ # Canonical Model
|
||||
│ │ │ ├── types.go # 核心请求/响应类型
|
||||
│ │ │ ├── stream.go # 流式事件类型
|
||||
│ │ │ └── extended.go # 扩展层 Models
|
||||
@@ -111,13 +111,12 @@ backend/
|
||||
│ └── validator/ # 验证器
|
||||
│ └── validator.go
|
||||
├── migrations/ # 数据库迁移
|
||||
│ ├── 001_initial_schema.sql
|
||||
│ └── 002_add_indexes.sql
|
||||
├── tests/ # 测试
|
||||
│ ├── 20260401000001_initial_schema.sql
|
||||
│ ├── 20260401000002_add_indexes.sql
|
||||
│ └── 20260419000001_add_provider_protocol.sql
|
||||
├── tests/ # 集成测试
|
||||
│ ├── helpers.go
|
||||
│ ├── integration/
|
||||
│ ├── unit/
|
||||
│ └── testdata/
|
||||
│ └── integration/
|
||||
├── Makefile
|
||||
├── go.mod
|
||||
└── README.md
|
||||
@@ -133,6 +132,19 @@ handler(HTTP 请求处理)
|
||||
→ repository(数据访问)
|
||||
```
|
||||
|
||||
代理请求通过 ConversionEngine 进行协议转换:
|
||||
|
||||
```
|
||||
Client Request (clientProtocol)
|
||||
→ ProxyHandler 路由到上游 provider
|
||||
→ ConversionEngine 请求转换 (clientProtocol → providerProtocol)
|
||||
→ ProviderClient 发送请求
|
||||
→ ConversionEngine 响应转换 (providerProtocol → clientProtocol)
|
||||
→ Client Response
|
||||
```
|
||||
|
||||
同协议时自动透传,跳过序列化开销。
|
||||
|
||||
## 运行方式
|
||||
|
||||
### 安装依赖
|
||||
@@ -186,78 +198,44 @@ log:
|
||||
export NEX_SERVER_PORT=9000
|
||||
export NEX_DATABASE_PATH=/data/nex.db
|
||||
export NEX_LOG_LEVEL=debug
|
||||
./server
|
||||
```
|
||||
|
||||
环境变量命名规则:将配置路径转换为大写,用下划线连接,加 `NEX_` 前缀:
|
||||
- `server.port` → `NEX_SERVER_PORT`
|
||||
- `database.path` → `NEX_DATABASE_PATH`
|
||||
- `log.level` → `NEX_LOG_LEVEL`
|
||||
命名规则:配置路径转大写 + 下划线 + `NEX_` 前缀(如 `server.port` → `NEX_SERVER_PORT`)。
|
||||
|
||||
### 命令行参数
|
||||
|
||||
所有配置项都支持命令行参数:
|
||||
|
||||
```bash
|
||||
./server --server-port 9000 --log-level debug --database-path /tmp/test.db
|
||||
```
|
||||
|
||||
CLI 参数命名规则:将配置路径转换为 kebab-case,用连字符连接:
|
||||
- `server.port` → `--server-port`
|
||||
- `database.path` → `--database-path`
|
||||
- `log.level` → `--log-level`
|
||||
命名规则:配置路径转 kebab-case + `--` 前缀(如 `server.port` → `--server-port`)。
|
||||
|
||||
完整参数列表:
|
||||
|
||||
```
|
||||
服务器配置:
|
||||
--server-port int 服务器端口(默认 9826)
|
||||
--server-read-timeout duration 读超时(默认 30s)
|
||||
--server-write-timeout duration 写超时(默认 30s)
|
||||
|
||||
数据库配置:
|
||||
--database-path string 数据库文件路径(默认 ~/.nex/config.db)
|
||||
--database-max-idle-conns int 最大空闲连接数(默认 10)
|
||||
--database-max-open-conns int 最大打开连接数(默认 100)
|
||||
--database-conn-max-lifetime duration 连接最大生命周期(默认 1h)
|
||||
|
||||
日志配置:
|
||||
--log-level string 日志级别:debug/info/warn/error(默认 info)
|
||||
--log-path string 日志文件目录(默认 ~/.nex/log)
|
||||
--log-max-size int 单个日志文件最大大小 MB(默认 100)
|
||||
--log-max-backups int 保留的旧日志文件最大数量(默认 10)
|
||||
--log-max-age int 保留旧日志文件的最大天数(默认 30)
|
||||
--log-compress 是否压缩旧日志文件(默认 true)
|
||||
|
||||
通用选项:
|
||||
--config string 配置文件路径(默认 ~/.nex/config.yaml)
|
||||
服务器: --server-port, --server-read-timeout, --server-write-timeout
|
||||
数据库: --database-path, --database-max-idle-conns, --database-max-open-conns, --database-conn-max-lifetime
|
||||
日志: --log-level, --log-path, --log-max-size, --log-max-backups, --log-max-age, --log-compress
|
||||
通用: --config (指定配置文件路径)
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 1. 使用默认配置
|
||||
# 默认配置
|
||||
./server
|
||||
|
||||
# 2. 临时修改端口(不修改配置文件)
|
||||
# 临时修改端口
|
||||
./server --server-port 9000
|
||||
|
||||
# 3. 测试场景(临时数据库)
|
||||
# 测试场景
|
||||
./server --database-path /tmp/test.db --log-level debug
|
||||
|
||||
# 4. Docker 部署(环境变量)
|
||||
docker run -d \
|
||||
-e NEX_SERVER_PORT=9000 \
|
||||
-e NEX_DATABASE_PATH=/data/nex.db \
|
||||
-e NEX_LOG_LEVEL=info \
|
||||
nex-server
|
||||
# Docker 部署
|
||||
docker run -d -e NEX_SERVER_PORT=9000 -e NEX_LOG_LEVEL=info nex-server
|
||||
|
||||
# 5. 使用自定义配置文件
|
||||
# 自定义配置文件
|
||||
./server --config /path/to/custom.yaml
|
||||
|
||||
# 6. 混合使用(优先级:CLI > ENV > File)
|
||||
export NEX_LOG_LEVEL=debug
|
||||
./server --server-port 9000 # port 用 CLI,log.level 用 ENV
|
||||
```
|
||||
|
||||
数据文件:
|
||||
@@ -283,6 +261,9 @@ make migrate-up DB_PATH=~/.nex/config.db
|
||||
make migrate-down DB_PATH=~/.nex/config.db
|
||||
make migrate-status DB_PATH=~/.nex/config.db
|
||||
|
||||
# 创建新迁移
|
||||
make migrate-create
|
||||
|
||||
# 或直接使用 goose
|
||||
goose -dir migrations sqlite3 ~/.nex/config.db up
|
||||
```
|
||||
@@ -293,7 +274,7 @@ goose -dir migrations sqlite3 ~/.nex/config.db up
|
||||
|
||||
使用 `/{protocol}/v1/{path}` URL 前缀路由:
|
||||
|
||||
#### OpenAI 协议代理
|
||||
#### OpenAI 协议
|
||||
|
||||
```
|
||||
POST /openai/v1/chat/completions
|
||||
@@ -302,37 +283,13 @@ POST /openai/v1/embeddings
|
||||
POST /openai/v1/rerank
|
||||
```
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
#### Anthropic 协议代理
|
||||
#### Anthropic 协议
|
||||
|
||||
```
|
||||
POST /anthropic/v1/messages
|
||||
GET /anthropic/v1/models
|
||||
```
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "claude-3-opus",
|
||||
"max_tokens": 1024,
|
||||
"messages": [
|
||||
{"role": "user", "content": [{"type": "text", "text": "Hello"}]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**协议转换**:网关支持任意协议间的双向转换。客户端使用 OpenAI 协议请求,上游供应商可以是 Anthropic 协议(反之亦然)。同协议时自动透传,零序列化开销。
|
||||
|
||||
### 管理接口
|
||||
@@ -345,8 +302,6 @@ GET /anthropic/v1/models
|
||||
- `PUT /api/providers/:id` - 更新供应商
|
||||
- `DELETE /api/providers/:id` - 删除供应商
|
||||
|
||||
创建供应商示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "openai",
|
||||
@@ -357,15 +312,9 @@ GET /anthropic/v1/models
|
||||
}
|
||||
```
|
||||
|
||||
**Protocol 字段说明:**
|
||||
- `protocol` 标识上游供应商使用的协议类型,可选值:`"openai"`(默认)、`"anthropic"`
|
||||
- 同协议透传时,请求体和响应体原样转发,零序列化开销
|
||||
**Protocol 字段**:标识上游供应商使用的协议类型,可选值 `"openai"`(默认)、`"anthropic"`。
|
||||
|
||||
**重要说明:**
|
||||
- `base_url` 应配置到 API 版本路径,不包含具体端点
|
||||
- OpenAI: `https://api.openai.com/v1`
|
||||
- GLM: `https://open.bigmodel.cn/api/paas/v4`
|
||||
- 其他 OpenAI 兼容供应商根据其文档配置版本路径
|
||||
**base_url 说明**:应配置到 API 版本路径,不包含具体端点(如 OpenAI: `https://api.openai.com/v1`,GLM: `https://open.bigmodel.cn/api/paas/v4`)。
|
||||
|
||||
#### 模型管理
|
||||
|
||||
@@ -375,8 +324,6 @@ GET /anthropic/v1/models
|
||||
- `PUT /api/models/:id` - 更新模型
|
||||
- `DELETE /api/models/:id` - 删除模型
|
||||
|
||||
创建模型示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "gpt-4",
|
||||
@@ -390,61 +337,43 @@ GET /anthropic/v1/models
|
||||
- `GET /api/stats` - 查询统计
|
||||
- `GET /api/stats/aggregate` - 聚合统计
|
||||
|
||||
查询参数:
|
||||
查询参数:`provider_id`、`model_name`、`start_date`(YYYY-MM-DD)、`end_date`、`group_by`(provider/model/date)
|
||||
|
||||
- `provider_id` - 供应商 ID
|
||||
- `model_name` - 模型名称
|
||||
- `start_date` - 开始日期(YYYY-MM-DD)
|
||||
- `end_date` - 结束日期(YYYY-MM-DD)
|
||||
- `group_by` - 聚合维度(provider/model/date)
|
||||
#### 健康检查
|
||||
|
||||
- `GET /health` - 返回 `{"status": "ok"}`
|
||||
|
||||
## 开发
|
||||
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
make build
|
||||
make build # 构建
|
||||
make lint # 代码检查
|
||||
make deps # 整理依赖
|
||||
```
|
||||
|
||||
### 代码检查
|
||||
|
||||
```bash
|
||||
make lint
|
||||
```
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Go 1.26 或更高版本
|
||||
环境要求:Go 1.26 或更高版本
|
||||
|
||||
## 公共库使用指南
|
||||
|
||||
### pkg/errors — 结构化错误
|
||||
|
||||
使用预定义的错误类型,配合 `errors.Is` / `errors.As` 判断错误:
|
||||
|
||||
```go
|
||||
import (
|
||||
"errors"
|
||||
pkgErrors "nex/backend/pkg/errors"
|
||||
)
|
||||
|
||||
// 使用预定义错误
|
||||
return pkgErrors.ErrRequestSend.WithCause(err)
|
||||
|
||||
// 判断错误类型
|
||||
var appErr *pkgErrors.AppError
|
||||
if errors.As(err, &appErr) {
|
||||
// appErr.Code, appErr.HTTPStatus, appErr.Message
|
||||
}
|
||||
```
|
||||
|
||||
可用函数:`NewAppError`、`Wrap`、`WithContext`、`WithMessage`、`AsAppError`
|
||||
|
||||
预定义错误:`ErrModelNotFound`、`ErrProviderNotFound`、`ErrInvalidRequest`、`ErrRequestCreate`、`ErrRequestSend`、`ErrResponseRead` 等
|
||||
|
||||
### pkg/logger — 日志系统
|
||||
|
||||
使用依赖注入模式,构造函数接受 `*zap.Logger` 参数,nil 时回退到 `zap.L()`:
|
||||
构造函数接受 `*zap.Logger` 参数,nil 时回退到 `zap.L()`:
|
||||
|
||||
```go
|
||||
func NewMyService(repo Repository, logger *zap.Logger) *MyService {
|
||||
@@ -455,8 +384,6 @@ func NewMyService(repo Repository, logger *zap.Logger) *MyService {
|
||||
}
|
||||
```
|
||||
|
||||
禁止直接在业务代码中使用 `zap.L()` 全局 logger,应通过构造函数注入。
|
||||
|
||||
### pkg/validator — 请求验证
|
||||
|
||||
```go
|
||||
@@ -468,8 +395,8 @@ err := v.Validate(myStruct)
|
||||
|
||||
## 编码规范
|
||||
|
||||
- **JSON 解析**:使用 `encoding/json` 标准库(`json.Unmarshal` / `json.Marshal`),不手动扫描字节
|
||||
- **JSON 解析**:使用 `encoding/json` 标准库,不手动扫描字节
|
||||
- **字符串拼接**:使用 `strings.Join`,不手写循环拼接
|
||||
- **错误判断**:使用 `errors.Is` / `errors.As`,不使用字符串匹配(`strings.Contains(err.Error(), ...)`)
|
||||
- **错误判断**:使用 `errors.Is` / `errors.As`,不使用字符串匹配
|
||||
- **日志使用**:通过依赖注入 `*zap.Logger`,不直接调用 `zap.L()`
|
||||
- **字符串分割**:使用 `strings.SplitN(key, "/", 2)` 等精确分割,不使用索引切片
|
||||
- **字符串分割**:使用 `strings.SplitN` 等精确分割,不使用索引切片
|
||||
|
||||
106
docs/prompts/规范整理.md
Normal file
106
docs/prompts/规范整理.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 规范文件整理流程
|
||||
|
||||
## 使用方式
|
||||
|
||||
将下方提示词完整复制给 AI 工具,即可启动一次规范文件的全面审查和整理。
|
||||
|
||||
---
|
||||
|
||||
## 提示词
|
||||
|
||||
```
|
||||
请对 openspec/specs/ 下的所有规范文件进行审查和整理,按以下流程执行:
|
||||
|
||||
## 第一步:全面阅读
|
||||
|
||||
1. 逐个读取 openspec/specs/ 下每个子目录的 spec.md,理解每个规范的覆盖范围
|
||||
2. 读取项目源码,理解实际代码实现
|
||||
3. 读取 openspec/config.yaml,了解项目约束和规范
|
||||
|
||||
## 第二步:对比分析
|
||||
|
||||
将每个规范与实际代码对比,按以下维度逐项检查:
|
||||
|
||||
### A. 过时检查
|
||||
- 规范描述的功能/组件/样式是否在当前代码中仍然存在
|
||||
- 规范引用的文件路径、类名、API 接口是否与代码一致
|
||||
- 规范描述的交互流程是否仍是当前的实现方式
|
||||
|
||||
### B. 重复检查
|
||||
- 不同规范是否描述了相同的组件/功能/场景
|
||||
- 场景级别的重复(A 规范的 Scenario 与 B 规范的 Scenario 重复)
|
||||
- 概念级别的重复(A 规范整体描述的就是 B 规范已覆盖的内容)
|
||||
|
||||
### C. 错位检查
|
||||
- A 规范中是否有场景应该属于 B 规范
|
||||
- 某个 Requirement 是否放在了错误的功能域下
|
||||
|
||||
### D. 合并检查
|
||||
- 描述同一类主题的规范是否分散在多个文件中
|
||||
- 某个规范是否可以作为子集被另一个更大的规范吸收
|
||||
|
||||
### E. 命名检查
|
||||
- 规范名称是否准确反映其实际内容
|
||||
- 命名是否遵循统一的前缀约定(平台前缀:admin- / developer- / console-)
|
||||
- 名称是否便于 AI 工具搜索匹配(暴露关键业务词和组件名)
|
||||
|
||||
### F. 格式检查
|
||||
- 是否使用标准的 SHALL/WHEN/THEN 规范格式
|
||||
- 是否混入了变更记录(如"移除以下列"、"ADDED Requirements")而非功能规范
|
||||
- 是否存在空目录
|
||||
|
||||
## 第三步:输出分析报告
|
||||
|
||||
按以下结构输出:
|
||||
|
||||
1. 问题总览表(问题类型 × 涉及规范数)
|
||||
2. 逐项分析(每个有问题的规范,说明具体问题和建议)
|
||||
3. 重构方案(删除/合并/重命名/内容调整的具体操作)
|
||||
4. 重构后的规范目录结构
|
||||
|
||||
## 第四步:执行重构
|
||||
|
||||
按优先级分批执行:
|
||||
- P0:删除空目录和完全冗余的规范
|
||||
- P1:合并重复/子集规范到主规范中
|
||||
- P2:重命名不精准的规范、拆分错位的内容
|
||||
- P3:修正与代码不匹配的细节描述
|
||||
|
||||
每步执行后确认目录结构完整。
|
||||
|
||||
## 命名约定
|
||||
|
||||
规范目录命名遵循以下规则,确保 AI 工具搜索时能精准匹配:
|
||||
|
||||
| 类型 | 命名模式 | 示例 |
|
||||
|------|---------|------|
|
||||
| 平台专属功能 | `{平台}-{功能}` | `admin-platform`、`console-my-skills`、`developer-platform` |
|
||||
| 跨平台组件/架构 | `{类别}` | `component-library`、`layout-system`、`design-tokens` |
|
||||
| 技能领域 | `skill-{方面}` | `skill-market`、`skill-status-rules`、`skill-version-management` |
|
||||
| 业务功能 | `{业务名词}` | `account-management`、`chat-scenarios` |
|
||||
|
||||
命名原则(提升 AI 检索命中率):
|
||||
- 名称中暴露可搜索的业务关键词(如 skill、modal、toast、account)
|
||||
- 同一平台的功能使用统一前缀(admin- / console- / developer-)
|
||||
- 同一领域的功能使用统一领域词前缀(skill-)
|
||||
- 避免泛化词(display → rules/behavior,basic → 删掉,general → 删掉)
|
||||
- 避免实现模式词(crud、list、table)而使用业务领域词
|
||||
- 避免同一关键词在不同规范中重复出现导致歧义(如 layout 只出现在一个规范名中)
|
||||
- 长度控制在 2-3 个词,去掉不影响检索的冗余词(info、data 等)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 补充说明
|
||||
|
||||
### 审查时的判断边界
|
||||
|
||||
- **规范 vs 代码**:规范描述"应该是什么",不描述"代码怎么写"。如果规范中出现了具体文件路径(如 `src/data/adminData.js`),通常是实现细节而非规范,应该清理
|
||||
- **规范 vs 变更记录**:规范用 SHALL/WHEN/THEN 格式描述功能需求。如果出现"移除以下列"、"保持现有样式"、"ADDED/MODIFIED Requirements"等措辞,说明混入了变更指令,需要改写
|
||||
- **规范 vs 文档**:规范不替代 README 或开发文档,不需要描述项目背景、技术选型等宏观信息
|
||||
|
||||
### 建议的定期审查节奏
|
||||
|
||||
- 每完成一批功能变更后,对照新代码检查相关规范是否需要更新
|
||||
- 规范数量超过 30 个时,建议做一次全面审查
|
||||
- 新增规范前,先搜索现有规范名称和内容,确认是否有可复用/扩展的规范
|
||||
2
openspec/changes/unified-model-id/.openspec.yaml
Normal file
2
openspec/changes/unified-model-id/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-20
|
||||
137
openspec/changes/unified-model-id/design.md
Normal file
137
openspec/changes/unified-model-id/design.md
Normal file
@@ -0,0 +1,137 @@
|
||||
## Context
|
||||
|
||||
Nex 是一个 AI 网关,屏蔽多个 AI 供应商(OpenAI、Anthropic 等)的差异,提供统一的 API 接口。当前后端直接透传上游供应商的原始模型名称(如 `gpt-4`),通过 `models` 表的 `model_name` 字段路由。`models` 表的 `id` 字段当前语义是用户自定义标识符,与上游模型名 `model_name` 之间没有明确的职责分离。
|
||||
|
||||
当前架构:
|
||||
- `ProxyHandler` 从请求体中提取 `model` 字段 → `RoutingService.Route(modelName)` 按 `model_name` 查询
|
||||
- `GET /v1/models` 直接透传到第一个供应商的上游接口
|
||||
- `GET /v1/models/{id}` 直接透传到上游
|
||||
- `TargetProvider.ModelName` 在 encoder 中覆盖请求体的 `model` 字段
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 定义统一模型 ID 格式 `provider_id/model_name`,全局唯一标识一个模型
|
||||
- 拦截 `/v1/models` 和 `/v1/models/{unified_id}` 接口,从数据库聚合返回,不再透传上游
|
||||
- 所有代理接口(Chat、Embeddings、Rerank)使用统一模型 ID 路由,响应中 `model` 字段覆写为统一 ID
|
||||
- `models.id` 改为 UUID(内部标识),`models.model_name` 存储上游供应商的模型名称
|
||||
- `provider_id` 约束为 `[a-zA-Z0-9_]+`,防止特殊字符影响 URL 和 JSON 交互
|
||||
- 保持协议无关、供应商无关的设计
|
||||
|
||||
**Non-Goals:**
|
||||
- 不支持供应商别名或模型别名
|
||||
- 不做上游模型列表自动同步(管理员手动配置可见模型)
|
||||
- 不适配前端(后续统一适配)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: 统一模型 ID 格式 — `provider_id/model_name`
|
||||
|
||||
格式: `{provider_id}/{model_name}`,例如 `openai/gpt-4`、`anthropic/claude-3-opus-20240229`。
|
||||
|
||||
- 使用 `strings.SplitN(id, "/", 2)` 解析,只在第一个 `/` 处分割
|
||||
- `provider_id` 约束为 `[a-zA-Z0-9_]+`,保证不含 `/`,解析安全
|
||||
- `model_name`(上游模型名)不受字符约束,因为它不出现在管理 API 的 URL 主键中
|
||||
|
||||
选择此格式而非 `provider_id:model_name`(冒号分隔)的原因:斜杠在 JSON 字符串中天然安全,且在 URL 路径中语义清晰(`/v1/models/openai/gpt-4`),更符合 REST 风格。
|
||||
|
||||
### D2: models 表 schema 变更
|
||||
|
||||
```
|
||||
旧: id(TEXT PK, 用户自定义), provider_id, model_name(上游模型名), enabled, created_at
|
||||
新: id(UUID PK, 自动生成), provider_id, model_name(上游模型名), enabled, created_at
|
||||
UNIQUE(provider_id, model_name)
|
||||
```
|
||||
|
||||
关键语义变化:
|
||||
- `id` 从用户自定义标识符变为 UUID 内部主键(自动生成),用于管理接口 CRUD
|
||||
- `model_name` 语义不变,始终存储上游供应商的模型名称,发给上游的实际值
|
||||
- 新增联合唯一约束 `UNIQUE(provider_id, model_name)` 保证同一供应商内模型不重复
|
||||
|
||||
选择保留 `id` 作为 PK 而非使用 `(provider_id, model_name)` 联合主键的原因:上游模型名可能含 `/` 等特殊字符(如 Azure OpenAI 的 deployment 路径),不适合作为管理接口的 URL 参数。`id` 为 UUID 可以避免所有特殊字符问题。
|
||||
|
||||
### D3: Models/ModelInfo 接口本地聚合
|
||||
|
||||
`GET /v1/models` 从数据库查询所有 `enabled` 的模型(JOIN providers),组装为 `CanonicalModelList`,`ID` 字段使用统一模型 ID,通过客户端协议的 adapter 编码返回。不请求上游。
|
||||
|
||||
`GET /v1/models/{provider_id}/{model_name}` 从 URL 提取统一模型 ID,解析后查询数据库,组装为 `CanonicalModelInfo` 返回。不请求上游。
|
||||
|
||||
选择纯 DB 聚合而非实时查询上游的原因:
|
||||
1. 管理员通过 `/api/models` 控制哪些模型对用户可见,网关的意义在于控制可见性
|
||||
2. 响应速度快,不依赖上游可用性
|
||||
3. 符合当前架构中管理员手动配置 provider 和 model 的设计哲学
|
||||
|
||||
### D4: 跨协议响应 model 字段覆写
|
||||
|
||||
跨协议场景下,上游返回的响应经过 decode → encode 全量转换。上游响应中的 `model` 字段是原生模型名(如 `gpt-4`),需要在返回给客户端前覆写为统一模型 ID。
|
||||
|
||||
实现位置:`ConversionEngine.ConvertHttpResponse` 新增 `modelOverride string` 参数。在解码上游响应到 canonical 后、编码客户端响应前,将 `canonical.Model` 设为 `modelOverride`。流式场景同理,`CreateStreamConverter` 同样接收 `modelOverride` 参数。
|
||||
|
||||
此方案仅在跨协议转换路径使用。选择在 canonical 层面处理的原因:
|
||||
1. 跨协议必须全量 decode → encode,canonical 的 Model 字段天然可覆写
|
||||
2. 不侵入各协议 adapter 的实现
|
||||
3. 与 Smart Passthrough 互补——跨协议不可保真,canonical 覆写是自然的
|
||||
|
||||
### D5: ProtocolAdapter 接口扩展
|
||||
|
||||
在 `ProtocolAdapter` 接口新增四个方法,将所有协议相关的 model 字段知识归属到 adapter:
|
||||
|
||||
1. `ExtractUnifiedModelID(nativePath string) (string, error)` — 从路径中提取统一模型 ID
|
||||
2. `ExtractModelName(body []byte, ifaceType InterfaceType) (string, error)` — 从请求体中提取 model 值(所有流程复用,替代 handler 层硬编码的 `extractModelName`)
|
||||
3. `RewriteRequestModelName(body []byte, newModel string, ifaceType InterfaceType) ([]byte, error)` — 最小化 JSON 改写请求体中的 model 字段(Smart Passthrough 请求方向)
|
||||
4. `RewriteResponseModelName(body []byte, newModel string, ifaceType InterfaceType) ([]byte, error)` — 最小化 JSON 改写响应体中的 model 字段(Smart Passthrough 响应方向)
|
||||
|
||||
拆分请求/响应方向的原因:请求体和响应体的 JSON 结构可能不同,model 字段的位置可能不同(当前 OpenAI/Anthropic 协议碰巧都在顶层 `"model"`,但未来协议不一定)。拆分后 adapter 各自独立实现,各自按 ifaceType 分派。
|
||||
|
||||
`ExtractModelName` 和两个 `Rewrite*` 方法均接收 `InterfaceType` 参数,因为不同接口类型的请求体/响应体结构可能不同,adapter 按 ifaceType 分派具体的定位和改写逻辑。
|
||||
|
||||
对于 `isModelInfoPath` 的调整:允许 suffix 中包含 `/`,因为统一模型 ID 格式为 `provider_id/model_name`。
|
||||
|
||||
将此方法放在适配器接口而非 handler 中通用实现的原因:不同协议的模型详情路径格式和请求体结构可能不同,各自拥有独立演进能力。
|
||||
|
||||
### D6: provider_id 字符集约束
|
||||
|
||||
创建供应商时校验 `id` 字段必须匹配 `^[a-zA-Z0-9_]+$`,长度 1-64。
|
||||
|
||||
选择严格限制而非仅排除 `/` 的原因:统一模型 ID 出现在 URL 路径和 JSON 中,`?`、`#`、`&`、`=` 等字符会在 URL 中引起解析问题。限制为字母数字下划线后,URL 中永远安全,不需要编码。
|
||||
|
||||
### D7: pkg/modelid 工具包
|
||||
|
||||
新增 `pkg/modelid` 包,提供:
|
||||
- `ParseUnifiedModelID(id string) (providerID, modelName string, error)` — 解析
|
||||
- `FormatUnifiedModelID(providerID, modelName string) string` — 格式化
|
||||
- `ValidateProviderID(id string) error` — 校验供应商 ID
|
||||
- `IsValidUnifiedModelID(id string) bool` — 校验统一模型 ID
|
||||
|
||||
使用标准库 `strings.SplitN` 和 `regexp` 实现,不引入新依赖。
|
||||
|
||||
### D8: 同协议 Smart Passthrough
|
||||
|
||||
当前同协议透传将请求体原样转发,跳过 decode → encode,保持参数完全保真。但统一模型 ID 要求改写 model 字段,原样透传无法满足。
|
||||
|
||||
**Smart Passthrough**:保留同协议透传的保真优势,通过 `json.RawMessage` 做最小化改写。
|
||||
|
||||
实现方式:adapter 的 `RewriteRequestModelName` 和 `RewriteResponseModelName` 方法各自解析 JSON 为 `map[string]json.RawMessage`,只替换 model 字段的 value,其余字段保留原始 bytes,不经过任何类型转换。参数保真、不丢精度、不改字段顺序。
|
||||
|
||||
各接口类型策略:
|
||||
- Chat/Embedding/Rerank(同协议):Smart Passthrough — 请求改写 model(统一 ID → 上游名),响应改写 model(上游名 → 统一 ID)
|
||||
- Chat/Embedding/Rerank(跨协议):全量 decode → encode + modelOverride
|
||||
- Models/ModelInfo:本地数据库聚合,不请求上游
|
||||
- Passthrough(未知路径):原样透传,不改写 model
|
||||
|
||||
选择让 adapter 拥有完整协议知识(而非通用 json hack)的原因:
|
||||
1. 不同协议的 model 字段位置可能不同,adapter 按 InterfaceType 分派
|
||||
2. 请求和响应的 model 字段位置可能不同,拆分 RewriteRequestModelName/RewriteResponseModelName 各自独立实现
|
||||
3. adapter 内部实现 `ExtractModelName` 和两个 `Rewrite*` 方法可共享同一份"model 在哪"的定位逻辑
|
||||
4. 所有流程复用 `ExtractModelName`,同协议额外复用 `RewriteRequestModelName` + `RewriteResponseModelName`
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[BREAKING CHANGE]** 代理接口 model 字段格式变更,现有客户端必须适配 → 统一 ID 格式简单直观,服务尚未上线无旧客户端
|
||||
- **[联合唯一约束]** 同一供应商下相同 model_name 不允许重复 → 这是正确的行为,语义上就不应该重复
|
||||
- **[model_name 含特殊字符]** 上游模型名可能含 `/`(如 Azure deployment 路径)→ 解析用 `SplitN("/", 2)` 安全,管理接口用 `id` 定位不受影响,代理接口中统一 ID 出现在 JSON body 和 URL 路径中均安全
|
||||
- **[流式响应覆写]** 同协议流式场景需逐 SSE chunk 调用 RewriteResponseModelName → 每个 chunk 多一次轻量 JSON 解析,用 json.RawMessage 保证开销极小
|
||||
|
||||
## Open Questions
|
||||
|
||||
无。所有关键决策已在探索阶段确认。
|
||||
41
openspec/changes/unified-model-id/proposal.md
Normal file
41
openspec/changes/unified-model-id/proposal.md
Normal file
@@ -0,0 +1,41 @@
|
||||
## Why
|
||||
|
||||
当前网关直接透传上游供应商的原始模型名称(如 `gpt-4`、`claude-3-opus`),无法在多供应商场景下唯一标识一个模型。不同供应商可能存在同名模型,客户端无法区分应路由到哪个供应商。网关作为屏蔽供应商差异的统一入口,需要定义自有的模型标识体系,让客户端通过统一的 model ID 访问任意供应商的模型,同时拦截 `/v1/models` 等模型查询接口,聚合所有供应商的模型信息返回。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: 引入统一模型 ID 格式 `provider_id/model_name`(如 `openai/gpt-4`),所有代理接口(Chat、Embeddings、Rerank)的 `model` 字段必须使用此格式
|
||||
- **BREAKING**: `models` 表主键 `id` 改为 UUID 自动生成(不再由用户提供),`model_name` 字段语义保持不变(存储上游供应商模型名称),新增 `UNIQUE(provider_id, model_name)` 联合唯一约束
|
||||
- **BREAKING**: `provider_id` 限制为 `[a-zA-Z0-9_]+` 字符集,禁止特殊字符
|
||||
- `GET /v1/models` 改为从数据库聚合返回所有已启用模型,不再透传到上游供应商
|
||||
- `GET /v1/models/{unified_id}` 改为从数据库查询返回模型详情,不再透传到上游供应商
|
||||
- 同协议透传改为 Smart Passthrough:通过 `json.RawMessage` 最小化改写 model 字段,保持其余参数完全保真
|
||||
- 跨协议转换路径:通过 canonical 层面 modelOverride 参数覆写响应 model 字段
|
||||
- 管理 API (`/api/models`) 请求体字段适配,响应中新增 `unified_id` 字段
|
||||
- 新增 `pkg/modelid` 工具包,提供统一模型 ID 的解析、格式化、校验
|
||||
- ProtocolAdapter 接口新增 `ExtractUnifiedModelID`、`ExtractModelName`、`RewriteRequestModelName`、`RewriteResponseModelName` 方法,协议无关地处理 model 字段
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `unified-model-id`: 统一模型 ID 的解析、格式化、校验工具包,以及 `provider_id` 字符集约束
|
||||
|
||||
### Modified Capabilities
|
||||
- `model-management`: 模型表结构调整(id 改 UUID 自动生成、新增联合唯一约束),CRUD 接口字段变更(创建不再提供 id)
|
||||
- `provider-management`: provider_id 创建时增加字符集校验(`[a-zA-Z0-9_]+`)
|
||||
- `unified-proxy-handler`: 统一模型 ID 解析路由、Models/ModelInfo 接口改为本地聚合、同协议 Smart Passthrough、跨协议 modelOverride 覆写
|
||||
- `conversion-engine`: 跨协议场景下 ConvertHttpResponse 支持 model 覆写参数
|
||||
- `protocol-adapter-openai`: isModelInfoPath 适配含 `/` 路径、新增 ExtractUnifiedModelID、ExtractModelName、RewriteRequestModelName、RewriteResponseModelName
|
||||
- `protocol-adapter-anthropic`: isModelInfoPath 适配含 `/` 路径、新增 ExtractUnifiedModelID、ExtractModelName、RewriteRequestModelName、RewriteResponseModelName
|
||||
- `request-validation`: provider_id 字符集校验规则、模型创建校验适配
|
||||
- `database-migration`: models 表 schema 变更迁移(DROP + CREATE 重建)
|
||||
- `usage-statistics`: 明确统计记录使用 providerID + modelName 的上游模型名
|
||||
|
||||
## Impact
|
||||
|
||||
- **数据库**: models 表 schema 变更(DROP + CREATE 重建)
|
||||
- **API 兼容性**: 代理接口 model 字段格式为 BREAKING CHANGE,需客户端适配
|
||||
- **管理 API**: `/api/models` 请求体变更(创建不再提供 id,自动生成 UUID),响应新增 unified_id 字段
|
||||
- **代码模块**: domain、repository、service、handler、conversion、adapter 层均有改动
|
||||
- **测试**: routing service、proxy handler、adapter、model handler 需要新增/更新测试
|
||||
- **前端**: 本次变更不涉及前端适配,前端后续统一适配
|
||||
@@ -0,0 +1,49 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 跨协议响应转换支持 model 覆写
|
||||
|
||||
ConversionEngine SHALL 在跨协议响应转换时支持 model 字段覆写。
|
||||
|
||||
#### Scenario: ConvertHttpResponse 接收 modelOverride 参数
|
||||
|
||||
- **WHEN** 调用 `ConvertHttpResponse` 时传入 `modelOverride` 参数(跨协议场景,非空字符串)
|
||||
- **THEN** SHALL 在解码上游响应到 canonical 后,将 `Model` 字段设为 `modelOverride`
|
||||
- **THEN** SHALL 使用覆写后的 canonical 编码为客户端协议格式
|
||||
|
||||
#### Scenario: modelOverride 为空
|
||||
|
||||
- **WHEN** 调用 `ConvertHttpResponse` 时 `modelOverride` 为空字符串
|
||||
- **THEN** SHALL NOT 覆写 canonical 的 Model 字段,保持上游原始值
|
||||
|
||||
#### Scenario: Chat 响应 model 覆写
|
||||
|
||||
- **WHEN** 跨协议转换 Chat 类型响应且 `modelOverride` 非空
|
||||
- **THEN** `CanonicalResponse.Model` SHALL 被设为 `modelOverride`
|
||||
|
||||
#### Scenario: Embedding 响应 model 覆写
|
||||
|
||||
- **WHEN** 跨协议转换 Embedding 类型响应且 `modelOverride` 非空
|
||||
- **THEN** `CanonicalEmbeddingResponse.Model` SHALL 被设为 `modelOverride`
|
||||
|
||||
#### Scenario: Rerank 响应 model 覆写
|
||||
|
||||
- **WHEN** 跨协议转换 Rerank 类型响应且 `modelOverride` 非空
|
||||
- **THEN** `CanonicalRerankResponse.Model` SHALL 被设为 `modelOverride`
|
||||
|
||||
### Requirement: 跨协议流式转换支持 model 覆写
|
||||
|
||||
ConversionEngine SHALL 在跨协议流式转换时支持 model 字段覆写。
|
||||
|
||||
#### Scenario: CreateStreamConverter 接收 modelOverride 参数
|
||||
|
||||
- **WHEN** 调用 `CreateStreamConverter` 时传入 `modelOverride` 参数(跨协议场景)
|
||||
- **THEN** SHALL 在流式 canonical 事件中将 `Model` 字段设为 `modelOverride`
|
||||
|
||||
### Requirement: TargetProvider 字段语义
|
||||
|
||||
TargetProvider 的 ModelName 字段 SHALL 存储上游供应商的模型名称(即 `model_name` 字段值),语义保持不变。
|
||||
|
||||
#### Scenario: encoder 使用 TargetProvider.ModelName
|
||||
|
||||
- **WHEN** 协议适配器编码请求时
|
||||
- **THEN** SHALL 使用 `TargetProvider.ModelName` 作为发给上游的 `model` 字段值(值为路由结果中的 model_name)
|
||||
@@ -0,0 +1,13 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: models 表 schema 变更
|
||||
|
||||
系统 SHALL 通过迁移脚本重建 models 表结构(服务未上线,无需考虑数据迁移)。
|
||||
|
||||
#### Scenario: 迁移后 models 表结构
|
||||
|
||||
- **WHEN** 执行迁移
|
||||
- **THEN** SHALL 先 DROP 已有的 models 表(无旧数据)
|
||||
- **THEN** SHALL CREATE 新的 models 表,包含字段:id(TEXT PRIMARY KEY)、provider_id(TEXT NOT NULL)、model_name(TEXT NOT NULL)、enabled(INTEGER DEFAULT 1)、created_at(DATETIME)
|
||||
- **THEN** SHALL 存在 UNIQUE(provider_id, model_name) 约束
|
||||
- **THEN** SHALL 存在 FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
||||
105
openspec/changes/unified-model-id/specs/model-management/spec.md
Normal file
105
openspec/changes/unified-model-id/specs/model-management/spec.md
Normal file
@@ -0,0 +1,105 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 创建模型配置
|
||||
|
||||
网关 SHALL 允许为供应商创建新的模型配置。
|
||||
|
||||
#### Scenario: 使用有效数据创建模型
|
||||
|
||||
- **WHEN** 向 `/api/models` 发送 POST 请求,携带有效的模型数据(provider_id, model_name),不提供 id 字段
|
||||
- **THEN** 网关 SHALL 自动生成 UUID 作为模型 id
|
||||
- **THEN** 网关 SHALL 在数据库中创建新的模型记录
|
||||
- **THEN** 网关 SHALL 返回创建的模型,状态码为 201
|
||||
- **THEN** 模型 SHALL 默认启用
|
||||
- **THEN** 返回的模型 SHALL 包含 `unified_id` 字段,值为 `{provider_id}/{model_name}`
|
||||
|
||||
#### Scenario: 使用不存在的供应商创建模型
|
||||
|
||||
- **WHEN** 向 `/api/models` 发送 POST 请求,携带不存在的 provider_id
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||||
- **THEN** 错误 SHALL 指示供应商不存在
|
||||
|
||||
#### Scenario: 创建重复模型
|
||||
|
||||
- **WHEN** 向 `/api/models` 发送 POST 请求,携带已存在的 provider_id + model_name 组合
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
|
||||
- **THEN** 错误 SHALL 指示该供应商下已存在相同模型
|
||||
|
||||
### Requirement: 列出所有模型
|
||||
|
||||
网关 SHALL 允许获取所有模型配置。
|
||||
|
||||
#### Scenario: 成功列出模型
|
||||
|
||||
- **WHEN** 向 `/api/models` 发送 GET 请求
|
||||
- **THEN** 网关 SHALL 返回所有模型的列表
|
||||
- **THEN** 每个模型 SHALL 包含 id, provider_id, model_name, unified_id, enabled, created_at
|
||||
|
||||
**变更说明:** 响应新增 unified_id 字段,移除旧语义的 id 自定义输入。
|
||||
|
||||
### Requirement: 按供应商列出模型
|
||||
|
||||
网关 SHALL 允许获取特定供应商的模型。
|
||||
|
||||
#### Scenario: 列出存在供应商的模型
|
||||
|
||||
- **WHEN** 向 `/api/models?provider_id=<provider_id>` 发送 GET 请求
|
||||
- **THEN** 网关 SHALL 返回指定供应商的模型列表
|
||||
- **THEN** 每个模型 SHALL 包含 unified_id 字段
|
||||
|
||||
### Requirement: 更新模型配置
|
||||
|
||||
网关 SHALL 允许更新现有模型配置。
|
||||
|
||||
#### Scenario: 使用有效数据更新模型
|
||||
|
||||
- **WHEN** 向 `/api/models/:id` 发送 PUT 请求,携带有效的模型数据
|
||||
- **THEN** 网关 SHALL 更新数据库中的模型记录
|
||||
- **THEN** 网关 SHALL 返回更新后的模型
|
||||
- **THEN** 返回的模型 SHALL 包含更新后的 unified_id
|
||||
|
||||
#### Scenario: 更新模型为重复组合
|
||||
|
||||
- **WHEN** 向 `/api/models/:id` 发送 PUT 请求,更新 provider_id 或 model_name 导致与已有记录重复
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
|
||||
|
||||
### Requirement: 删除模型配置
|
||||
|
||||
网关 SHALL 允许删除模型配置。
|
||||
|
||||
#### Scenario: 删除存在的模型
|
||||
|
||||
- **WHEN** 向 `/api/models/:id` 发送 DELETE 请求,携带有效的模型 ID
|
||||
- **THEN** 网关 SHALL 删除模型记录
|
||||
- **THEN** 网关 SHALL 返回状态码 204 (No Content)
|
||||
|
||||
### Requirement: 使用 service 层处理业务逻辑
|
||||
|
||||
Handler SHALL 通过 ModelService 处理业务逻辑。
|
||||
|
||||
#### Scenario: 调用 service 方法
|
||||
|
||||
- **WHEN** handler 收到请求
|
||||
- **THEN** SHALL 调用对应的 ModelService 方法(Create、Get、List、Update、Delete)
|
||||
- **THEN** SHALL 使用 domain.Model 类型
|
||||
- **THEN** Create 时 SHALL 调用 `uuid.New()` 生成 id
|
||||
|
||||
#### Scenario: 供应商验证和唯一性校验
|
||||
|
||||
- **WHEN** 创建或更新模型
|
||||
- **THEN** SHALL 在 service 层验证供应商存在
|
||||
- **THEN** SHALL 在 service 层验证 provider_id + model_name 联合唯一
|
||||
|
||||
### Requirement: 使用 repository 层访问数据
|
||||
|
||||
Service SHALL 通过 ModelRepository 访问数据。
|
||||
|
||||
#### Scenario: 联合查询
|
||||
|
||||
- **WHEN** service 需要按 provider 和 model_name 查询模型
|
||||
- **THEN** SHALL 调用 `FindByProviderAndModelName(providerID, modelName)` 方法
|
||||
|
||||
#### Scenario: 查询所有启用模型
|
||||
|
||||
- **WHEN** proxy handler 需要聚合模型列表
|
||||
- **THEN** SHALL 调用 `ListEnabled()` 方法,返回所有 enabled 的模型(关联 enabled 的供应商)
|
||||
@@ -0,0 +1,71 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 模型详情路径识别
|
||||
|
||||
Anthropic 适配器 SHALL 正确识别包含统一模型 ID 的模型详情路径。
|
||||
|
||||
#### Scenario: 含斜杠的统一模型 ID 路径
|
||||
|
||||
- **WHEN** 路径为 `/v1/models/anthropic/claude-3-opus`
|
||||
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`
|
||||
|
||||
#### Scenario: 含多段斜杠的统一模型 ID 路径
|
||||
|
||||
- **WHEN** 路径为 `/v1/models/azure/deployments/gpt-4`
|
||||
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`
|
||||
|
||||
#### Scenario: 模型列表路径不受影响
|
||||
|
||||
- **WHEN** 路径为 `/v1/models`
|
||||
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModels`
|
||||
|
||||
### Requirement: 提取统一模型 ID
|
||||
|
||||
Anthropic 适配器 SHALL 从路径中提取统一模型 ID。
|
||||
|
||||
#### Scenario: 标准路径提取
|
||||
|
||||
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/anthropic/claude-3-opus")`
|
||||
- **THEN** SHALL 返回 `"anthropic/claude-3-opus"`
|
||||
|
||||
#### Scenario: 复杂路径提取
|
||||
|
||||
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/azure/accounts/org/models/gpt-4")`
|
||||
- **THEN** SHALL 返回 `"azure/accounts/org/models/gpt-4"`
|
||||
|
||||
#### Scenario: 非模型详情路径
|
||||
|
||||
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models")`
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
### Requirement: 从请求体提取 model
|
||||
|
||||
Anthropic 适配器 SHALL 按 InterfaceType 从请求体中提取 model 值。
|
||||
|
||||
#### Scenario: Chat 请求提取 model
|
||||
|
||||
- **WHEN** 调用 `ExtractModelName(body, InterfaceTypeChat)`,body 为 `{"model":"anthropic/claude-3-opus","messages":[...]}`
|
||||
- **THEN** SHALL 返回 `"anthropic/claude-3-opus"`
|
||||
|
||||
#### Scenario: 无 model 字段
|
||||
|
||||
- **WHEN** 调用 `ExtractModelName(body, ifaceType)`,body 中不含 model 字段
|
||||
- **THEN** SHALL 返回空字符串,不返回错误
|
||||
|
||||
### Requirement: 最小化改写请求体 model 字段
|
||||
|
||||
Anthropic 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写请求体中的 model 字段,其余字段保持原始 bytes。
|
||||
|
||||
#### Scenario: 请求体 model 改写(统一 ID → 上游名)
|
||||
|
||||
- **WHEN** 调用 `RewriteRequestModelName(body, "claude-3-opus-20240229", InterfaceTypeChat)`
|
||||
- **THEN** SHALL 将请求体中 model 字段替换为 `"claude-3-opus-20240229"`,其余字段原样保留
|
||||
|
||||
### Requirement: 最小化改写响应体 model 字段
|
||||
|
||||
Anthropic 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写响应体中的 model 字段,其余字段保持原始 bytes。请求体和响应体的 model 字段位置可能不同,各自独立实现。
|
||||
|
||||
#### Scenario: 响应体 model 改写(上游名 → 统一 ID)
|
||||
|
||||
- **WHEN** 调用 `RewriteResponseModelName(body, "anthropic/claude-3-opus", InterfaceTypeChat)`
|
||||
- **THEN** SHALL 将响应体中 model 字段替换为 `"anthropic/claude-3-opus"`,其余字段原样保留
|
||||
@@ -0,0 +1,89 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 模型详情路径识别
|
||||
|
||||
OpenAI 适配器 SHALL 正确识别包含统一模型 ID 的模型详情路径。
|
||||
|
||||
#### Scenario: 含斜杠的统一模型 ID 路径
|
||||
|
||||
- **WHEN** 路径为 `/v1/models/openai/gpt-4`
|
||||
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`
|
||||
|
||||
#### Scenario: 含多段斜杠的统一模型 ID 路径
|
||||
|
||||
- **WHEN** 路径为 `/v1/models/azure/accounts/org-123/models/gpt-4`
|
||||
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModelInfo`
|
||||
|
||||
#### Scenario: 模型列表路径不受影响
|
||||
|
||||
- **WHEN** 路径为 `/v1/models`
|
||||
- **THEN** `DetectInterfaceType` SHALL 返回 `InterfaceTypeModels`
|
||||
|
||||
### Requirement: 提取统一模型 ID
|
||||
|
||||
OpenAI 适配器 SHALL 从路径中提取统一模型 ID。
|
||||
|
||||
#### Scenario: 标准路径提取
|
||||
|
||||
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/openai/gpt-4")`
|
||||
- **THEN** SHALL 返回 `"openai/gpt-4"`
|
||||
|
||||
#### Scenario: 复杂路径提取
|
||||
|
||||
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models/azure/accounts/org/models/gpt-4")`
|
||||
- **THEN** SHALL 返回 `"azure/accounts/org/models/gpt-4"`
|
||||
|
||||
#### Scenario: 非模型详情路径
|
||||
|
||||
- **WHEN** 调用 `ExtractUnifiedModelID("/v1/models")`
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
### Requirement: 从请求体提取 model
|
||||
|
||||
OpenAI 适配器 SHALL 按 InterfaceType 从请求体中提取 model 值。
|
||||
|
||||
#### Scenario: Chat 请求提取 model
|
||||
|
||||
- **WHEN** 调用 `ExtractModelName(body, InterfaceTypeChat)`,body 为 `{"model":"openai/gpt-4","messages":[...]}`
|
||||
- **THEN** SHALL 返回 `"openai/gpt-4"`
|
||||
|
||||
#### Scenario: Embedding 请求提取 model
|
||||
|
||||
- **WHEN** 调用 `ExtractModelName(body, InterfaceTypeEmbeddings)`,body 为 `{"model":"openai/text-embedding-3","input":"text"}`
|
||||
- **THEN** SHALL 返回 `"openai/text-embedding-3"`
|
||||
|
||||
#### Scenario: 无 model 字段
|
||||
|
||||
- **WHEN** 调用 `ExtractModelName(body, ifaceType)`,body 中不含 model 字段
|
||||
- **THEN** SHALL 返回空字符串,不返回错误
|
||||
|
||||
### Requirement: 最小化改写请求体 model 字段
|
||||
|
||||
OpenAI 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写请求体中的 model 字段,其余字段保持原始 bytes。
|
||||
|
||||
#### Scenario: 请求体 model 改写(统一 ID → 上游名)
|
||||
|
||||
- **WHEN** 调用 `RewriteRequestModelName(body, "gpt-4", InterfaceTypeChat)`,body 为 `{"model":"openai/gpt-4","messages":[...],"some_param":"value"}`
|
||||
- **THEN** SHALL 返回 `{"model":"gpt-4","messages":[...],"some_param":"value"}`
|
||||
- **THEN** 除 model 外的字段 SHALL 保持原始 bytes 不变
|
||||
|
||||
#### Scenario: 不同 InterfaceType 的请求改写
|
||||
|
||||
- **WHEN** 调用 `RewriteRequestModelName(body, "gpt-4", InterfaceTypeEmbeddings)`
|
||||
- **THEN** SHALL 按 Embedding 接口的请求体 model 字段位置进行改写
|
||||
- **WHEN** 调用 `RewriteRequestModelName(body, "gpt-4", InterfaceTypeRerank)`
|
||||
- **THEN** SHALL 按 Rerank 接口的请求体 model 字段位置进行改写
|
||||
|
||||
### Requirement: 最小化改写响应体 model 字段
|
||||
|
||||
OpenAI 适配器 SHALL 按 InterfaceType 用 `json.RawMessage` 最小化改写响应体中的 model 字段,其余字段保持原始 bytes。请求体和响应体的 model 字段位置可能不同,各自独立实现。
|
||||
|
||||
#### Scenario: 响应体 model 改写(上游名 → 统一 ID)
|
||||
|
||||
- **WHEN** 调用 `RewriteResponseModelName(body, "openai/gpt-4", InterfaceTypeChat)`,body 为上游 Chat 响应
|
||||
- **THEN** SHALL 将 model 字段替换为 `"openai/gpt-4"`,其余字段原样保留
|
||||
|
||||
#### Scenario: 不同 InterfaceType 的响应改写
|
||||
|
||||
- **WHEN** 调用 `RewriteResponseModelName(body, "openai/gpt-4", InterfaceTypeEmbeddings)`
|
||||
- **THEN** SHALL 按 Embedding 接口的响应体 model 字段位置进行改写
|
||||
@@ -0,0 +1,35 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 创建供应商配置
|
||||
|
||||
网关 SHALL 允许通过管理 API 创建新的供应商配置。
|
||||
|
||||
#### Scenario: 使用有效数据创建供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,携带有效的供应商数据(id, name, api_key, base_url, protocol)
|
||||
- **THEN** 网关 SHALL 在数据库中创建新的供应商记录
|
||||
- **THEN** 网关 SHALL 返回创建的供应商,状态码为 201
|
||||
- **THEN** 供应商 SHALL 默认启用
|
||||
- **THEN** protocol 字段 SHALL 默认为 "openai"
|
||||
|
||||
#### Scenario: 使用重复 ID 创建供应商
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,携带已存在的 ID
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
|
||||
|
||||
#### Scenario: 创建供应商时缺少必需字段
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,缺少必需字段(id, name, api_key 或 base_url)
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||||
- **THEN** 错误 SHALL 指示缺少哪些字段
|
||||
|
||||
#### Scenario: 创建供应商时 ID 包含非法字符
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,id 包含非 `[a-zA-Z0-9_]` 字符
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||||
- **THEN** 错误 SHALL 指示 id 仅允许字母、数字、下划线
|
||||
|
||||
#### Scenario: 创建供应商时 ID 过长
|
||||
|
||||
- **WHEN** 向 `/api/providers` 发送 POST 请求,id 长度超过 64
|
||||
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
|
||||
@@ -0,0 +1,39 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 供应商 ID 校验
|
||||
|
||||
创建供应商时,SHALL 对 `id` 字段进行字符集校验。
|
||||
|
||||
#### Scenario: 合法字符集
|
||||
|
||||
- **WHEN** 创建供应商,id 仅包含 `[a-zA-Z0-9_]` 字符
|
||||
- **THEN** SHALL 校验通过
|
||||
|
||||
#### Scenario: 非法字符
|
||||
|
||||
- **WHEN** 创建供应商,id 包含 `-`、`.`、`/`、空格、中文等非 `[a-zA-Z0-9_]` 字符
|
||||
- **THEN** SHALL 返回 400 错误
|
||||
|
||||
#### Scenario: 长度限制
|
||||
|
||||
- **WHEN** 创建供应商,id 长度超过 64
|
||||
- **THEN** SHALL 返回 400 错误
|
||||
|
||||
### Requirement: 模型创建校验
|
||||
|
||||
创建模型时,SHALL 对 `provider_id` + `model_name` 进行联合唯一性校验。
|
||||
|
||||
#### Scenario: 正常创建
|
||||
|
||||
- **WHEN** 创建模型,provider_id 存在且 provider_id + model_name 组合唯一
|
||||
- **THEN** SHALL 校验通过
|
||||
|
||||
#### Scenario: 联合唯一冲突
|
||||
|
||||
- **WHEN** 创建模型,provider_id + model_name 组合已存在
|
||||
- **THEN** SHALL 返回 409 错误
|
||||
|
||||
#### Scenario: model_name 为空
|
||||
|
||||
- **WHEN** 创建模型,未提供 model_name
|
||||
- **THEN** SHALL 返回 400 错误
|
||||
@@ -0,0 +1,79 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 解析统一模型 ID
|
||||
|
||||
系统 SHALL 提供 `ParseUnifiedModelID` 函数,将 `provider_id/model_name` 格式的字符串解析为独立的 providerID 和 modelName。
|
||||
|
||||
#### Scenario: 标准格式解析
|
||||
|
||||
- **WHEN** 传入 `"openai/gpt-4"`
|
||||
- **THEN** SHALL 返回 providerID=`"openai"`, modelName=`"gpt-4"`
|
||||
|
||||
#### Scenario: model_name 含斜杠的解析
|
||||
|
||||
- **WHEN** 传入 `"azure/accounts/org-123/models/gpt-4"`
|
||||
- **THEN** SHALL 在第一个 `/` 处分割,返回 providerID=`"azure"`, modelName=`"accounts/org-123/models/gpt-4"`
|
||||
|
||||
#### Scenario: 缺少分隔符
|
||||
|
||||
- **WHEN** 传入不含 `/` 的字符串(如 `"gpt-4"`)
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
#### Scenario: 空字符串
|
||||
|
||||
- **WHEN** 传入空字符串
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
#### Scenario: 只有分隔符
|
||||
|
||||
- **WHEN** 传入 `"/model"` 或 `"provider/"` 或 `"/"`
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
#### Scenario: providerID 不符合字符集
|
||||
|
||||
- **WHEN** 传入 `"open-ai/gpt-4"` 或 `"open.ai/gpt-4"` 或 `"供应商/gpt-4"`(providerID 含非 `[a-zA-Z0-9_]` 字符)
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
### Requirement: 格式化统一模型 ID
|
||||
|
||||
系统 SHALL 提供 `FormatUnifiedModelID` 函数,将 providerID 和 modelName 组合格式化为统一模型 ID。
|
||||
|
||||
#### Scenario: 格式化
|
||||
|
||||
- **WHEN** 传入 providerID=`"openai"`, modelName=`"gpt-4"`
|
||||
- **THEN** SHALL 返回 `"openai/gpt-4"`
|
||||
|
||||
### Requirement: 校验供应商 ID
|
||||
|
||||
系统 SHALL 提供 `ValidateProviderID` 函数,校验 providerID 仅包含字母、数字、下划线。
|
||||
|
||||
#### Scenario: 合法 ID
|
||||
|
||||
- **WHEN** 传入 `"openai"`, `"deep_seek"`, `"provider01"`, `"OpenAI"`
|
||||
- **THEN** SHALL 校验通过
|
||||
|
||||
#### Scenario: 含非法字符
|
||||
|
||||
- **WHEN** 传入含 `-`、`.`、`/`、空格、中文等非 `[a-zA-Z0-9_]` 字符的 ID
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
#### Scenario: 空字符串或过长
|
||||
|
||||
- **WHEN** 传入空字符串
|
||||
- **THEN** SHALL 返回错误
|
||||
- **WHEN** 传入超过 64 个字符的 ID
|
||||
- **THEN** SHALL 返回错误
|
||||
|
||||
### Requirement: 校验统一模型 ID
|
||||
|
||||
系统 SHALL 提供 `IsValidUnifiedModelID` 函数,判断字符串是否为合法的统一模型 ID 格式。
|
||||
|
||||
#### Scenario: 合法 ID
|
||||
|
||||
- **WHEN** 传入 `"openai/gpt-4"`
|
||||
- **THEN** SHALL 返回 `true`
|
||||
|
||||
#### Scenario: 非法 ID
|
||||
|
||||
- **WHEN** 传入不含 `/` 的字符串、空字符串、providerID 不符合 `[a-zA-Z0-9_]+` 的字符串
|
||||
- **THEN** SHALL 返回 `false`
|
||||
@@ -0,0 +1,123 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 代理请求路由
|
||||
|
||||
ProxyHandler SHALL 使用统一模型 ID 路由所有代理请求。
|
||||
|
||||
#### Scenario: 提取统一模型 ID
|
||||
|
||||
- **WHEN** 收到 Chat、Embeddings 或 Rerank 接口的 POST 请求(含请求体)
|
||||
- **THEN** SHALL 调用客户端协议 adapter 的 `ExtractModelName(body, ifaceType)` 提取 model 值
|
||||
- **THEN** SHALL 调用 `ParseUnifiedModelID` 解析得到 providerID 和 modelName
|
||||
- **THEN** SHALL 调用 `RoutingService.RouteByModelName(providerID, modelName)` 路由
|
||||
|
||||
#### Scenario: GET 请求或无请求体
|
||||
|
||||
- **WHEN** 收到 GET 请求或请求体为空
|
||||
- **THEN** SHALL 返回错误响应,状态码为 400,提示缺少 model 字段
|
||||
|
||||
#### Scenario: 无效的统一模型 ID
|
||||
|
||||
- **WHEN** 请求体中 `model` 字段不是有效的统一模型 ID 格式
|
||||
- **THEN** SHALL 返回错误响应,状态码为 400
|
||||
|
||||
#### Scenario: 模型不存在
|
||||
|
||||
- **WHEN** 解析统一模型 ID 后,数据库中找不到对应的 provider_id + model_name 组合
|
||||
- **THEN** SHALL 返回错误响应,状态码为 404
|
||||
|
||||
#### Scenario: 模型已禁用
|
||||
|
||||
- **WHEN** 解析统一模型 ID 后,对应的模型 enabled 为 false
|
||||
- **THEN** SHALL 返回错误响应,状态码为 404
|
||||
|
||||
#### Scenario: 供应商已禁用
|
||||
|
||||
- **WHEN** 解析统一模型 ID 后,对应的供应商 enabled 为 false
|
||||
- **THEN** SHALL 返回错误响应,状态码为 404
|
||||
|
||||
### Requirement: 同协议 Smart Passthrough
|
||||
|
||||
当客户端协议与供应商协议相同时,ProxyHandler SHALL 使用 Smart Passthrough 处理 Chat、Embedding、Rerank 请求。
|
||||
|
||||
#### Scenario: 同协议非流式请求
|
||||
|
||||
- **WHEN** 客户端协议 == 供应商协议,且为非流式请求
|
||||
- **THEN** SHALL 调用 adapter 的 `RewriteRequestModelName(body, modelName, ifaceType)` 将请求体中 model 从统一 ID 改写为上游模型名
|
||||
- **THEN** SHALL 构建 URL 和 Headers(同当前透传逻辑)
|
||||
- **THEN** SHALL 发送改写后的请求体到上游
|
||||
- **THEN** SHALL 调用 adapter 的 `RewriteResponseModelName(resp.Body, unifiedModelID, ifaceType)` 将响应中 model 从上游名改写为统一 ID
|
||||
- **THEN** SHALL NOT 对 body 做全量 decode → encode,保持未改写字段的原始 bytes
|
||||
|
||||
#### Scenario: 同协议流式请求
|
||||
|
||||
- **WHEN** 客户端协议 == 供应商协议,且为流式请求
|
||||
- **THEN** SHALL 对请求体做 `RewriteRequestModelName` 改写 model 字段
|
||||
- **THEN** SHALL 逐 SSE chunk 调用 `RewriteResponseModelName` 改写响应中 model 字段
|
||||
- **THEN** SHALL NOT 对 chunk 做全量 decode → encode
|
||||
|
||||
#### Scenario: Smart Passthrough 保真性
|
||||
|
||||
- **WHEN** 客户端发送含未知参数的请求(如 `{"model":"openai/gpt-4","some_new_param":"value"}`)
|
||||
- **THEN** 上游 SHALL 收到 `{"model":"gpt-4","some_new_param":"value"}`
|
||||
- **THEN** `some_new_param` SHALL 保持原始值不变,不丢失、不改变类型
|
||||
|
||||
### Requirement: 跨协议完整转换
|
||||
|
||||
当客户端协议与供应商协议不同时,ProxyHandler SHALL 使用全量转换路径。
|
||||
|
||||
#### Scenario: 跨协议非流式请求
|
||||
|
||||
- **WHEN** 客户端协议 != 供应商协议
|
||||
- **THEN** SHALL 走 `ConvertHttpRequest` 全量转换,encoder 中 provider.ModelName 覆盖 model
|
||||
- **THEN** SHALL 走 `ConvertHttpResponse` 全量转换,modelOverride 参数覆写 canonical.Model
|
||||
|
||||
#### Scenario: 跨协议流式请求
|
||||
|
||||
- **WHEN** 客户端协议 != 供应商协议,且为流式请求
|
||||
- **THEN** SHALL 走 `CreateStreamConverter` 全量转换,modelOverride 参数覆写流式 canonical 事件中的 Model
|
||||
|
||||
### Requirement: 模型列表本地聚合
|
||||
|
||||
ProxyHandler SHALL 从数据库聚合返回模型列表,不再透传上游。
|
||||
|
||||
#### Scenario: GET /v1/models
|
||||
|
||||
- **WHEN** 收到 `GET /{protocol}/v1/models` 请求
|
||||
- **THEN** SHALL 从数据库查询所有 enabled 的模型(关联 enabled 的供应商)
|
||||
- **THEN** SHALL 组装 `CanonicalModelList`,每个模型的 ID 字段为统一模型 ID(`provider_id/model_name`),Name 字段为 model_name,OwnedBy 字段为 provider_id
|
||||
- **THEN** SHALL 使用客户端协议的 adapter 编码响应
|
||||
- **THEN** SHALL NOT 请求上游供应商
|
||||
|
||||
#### Scenario: 无可用模型
|
||||
|
||||
- **WHEN** 数据库中没有 enabled 的模型
|
||||
- **THEN** SHALL 返回空列表
|
||||
|
||||
### Requirement: 模型详情本地查询
|
||||
|
||||
ProxyHandler SHALL 从数据库查询返回模型详情,不再透传上游。
|
||||
|
||||
#### Scenario: GET /v1/models/{unified_id}
|
||||
|
||||
- **WHEN** 收到 `GET /{protocol}/v1/models/{provider_id}/{model_name}` 请求
|
||||
- **THEN** SHALL 调用 adapter 的 `ExtractUnifiedModelID` 提取统一模型 ID
|
||||
- **THEN** SHALL 解析统一模型 ID 得到 providerID 和 modelName
|
||||
- **THEN** SHALL 从数据库查询对应的模型和供应商
|
||||
- **THEN** SHALL 组装 `CanonicalModelInfo`,ID 字段为统一模型 ID(`provider_id/model_name`),Name 字段为 model_name,OwnedBy 字段为 provider_id
|
||||
- **THEN** SHALL 使用客户端协议的 adapter 编码响应
|
||||
- **THEN** SHALL NOT 请求上游供应商
|
||||
|
||||
#### Scenario: 模型详情不存在
|
||||
|
||||
- **WHEN** 统一模型 ID 对应的模型不存在或已禁用
|
||||
- **THEN** SHALL 返回错误响应,状态码为 404
|
||||
|
||||
### Requirement: 统计记录
|
||||
|
||||
ProxyHandler SHALL 使用 providerID 和 modelName 记录使用统计。
|
||||
|
||||
#### Scenario: 异步记录统计
|
||||
|
||||
- **WHEN** 代理请求成功完成
|
||||
- **THEN** SHALL 异步调用 `StatsService.Record(providerID, modelName)`
|
||||
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 使用统计记录统一模型标识
|
||||
|
||||
系统 SHALL 使用 providerID 和 modelName(上游模型名)记录使用统计。
|
||||
|
||||
#### Scenario: 代理请求统计记录
|
||||
|
||||
- **WHEN** 代理请求成功完成
|
||||
- **THEN** SHALL 记录 provider_id 和 model_name 到 usage_stats 表(参数来自路由结果)
|
||||
- **THEN** SHALL 异步执行,不阻塞响应
|
||||
|
||||
#### Scenario: 查询统计
|
||||
|
||||
- **WHEN** 查询统计数据
|
||||
- **THEN** 支持按 provider_id 和 model_name 过滤
|
||||
53
openspec/changes/unified-model-id/tasks.md
Normal file
53
openspec/changes/unified-model-id/tasks.md
Normal file
@@ -0,0 +1,53 @@
|
||||
## 1. 数据库迁移
|
||||
|
||||
- [ ] 1.1 新增迁移脚本:DROP 旧 models 表 + CREATE 新 models 表(id UUID PK, provider_id, model_name, enabled, created_at),UNIQUE(provider_id, model_name)
|
||||
- [ ] 1.2 更新 config/models.go:Model 结构体适配(id 改为 UUID 自动生成,model_name 保持不变)
|
||||
- [ ] 1.3 编写迁移脚本测试
|
||||
|
||||
## 2. 统一模型 ID 工具包
|
||||
|
||||
- [ ] 2.1 新增 pkg/modelid/model_id.go:实现 ParseUnifiedModelID、FormatUnifiedModelID、ValidateProviderID、IsValidUnifiedModelID
|
||||
- [ ] 2.2 新增 pkg/modelid/model_id_test.go:覆盖标准格式、含斜杠 model_name、空字符串、非法字符等边界情况
|
||||
|
||||
## 3. Domain 层适配
|
||||
|
||||
- [ ] 3.1 修改 domain/model.go:Model 结构体字段适配,新增 UnifiedModelID() 方法
|
||||
- [ ] 3.2 修改 domain/route.go:RouteResult 适配新字段
|
||||
|
||||
## 4. Repository 层适配
|
||||
|
||||
- [ ] 4.1 修改 repository/model_repo.go:接口变更 — GetByModelName 改为 FindByProviderAndModelName,新增 ListEnabled
|
||||
- [ ] 4.2 修改 repository/model_repo_impl.go:实现 FindByProviderAndModelName(WHERE provider_id=? AND model_name=?)、ListEnabled(JOIN providers WHERE enabled)
|
||||
- [ ] 4.3 编写 repository 层测试
|
||||
|
||||
## 5. Service 层适配
|
||||
|
||||
- [ ] 5.1 修改 service/routing_service.go:Route 接口改为 RouteByModelName(providerID, modelName string)
|
||||
- [ ] 5.2 修改 service/routing_service_impl.go:调用 FindByProviderAndModelName 替代 GetByModelName
|
||||
- [ ] 5.3 修改 service/model_service.go:Create 生成 UUID、新增联合唯一校验方法
|
||||
- [ ] 5.4 修改 service/model_service_impl.go:实现联合唯一校验、UUID 生成
|
||||
- [ ] 5.5 修改 service/provider_service_impl.go:Create 时调用 ValidateProviderID 校验 ID 字符集
|
||||
- [ ] 5.6 编写 service 层测试
|
||||
|
||||
## 6. Conversion 层适配
|
||||
|
||||
- [ ] 6.1 修改 conversion/adapter.go:ProtocolAdapter 接口新增 ExtractUnifiedModelID、ExtractModelName、RewriteRequestModelName、RewriteResponseModelName 四个方法
|
||||
- [ ] 6.2 修改 conversion/engine.go:ConvertHttpResponse 新增 modelOverride 参数(跨协议场景),各 convert*ResponseBody 中覆写 canonical Model;CreateStreamConverter 新增 modelOverride 参数
|
||||
- [ ] 6.3 修改 conversion/openai/adapter.go:实现 ExtractUnifiedModelID、ExtractModelName(按 ifaceType 提取 model)、RewriteRequestModelName 和 RewriteResponseModelName(json.RawMessage 最小化改写,按 ifaceType 定位 model 字段,请求/响应独立实现),修改 isModelInfoPath 允许 suffix 含 "/"
|
||||
- [ ] 6.4 修改 conversion/anthropic/adapter.go:实现 ExtractUnifiedModelID、ExtractModelName、RewriteRequestModelName、RewriteResponseModelName,修改 isModelInfoPath 允许 suffix 含 "/"
|
||||
- [ ] 6.5 编写 conversion 层测试:ExtractUnifiedModelID、ExtractModelName 各 ifaceType、RewriteRequestModelName/RewriteResponseModelName 保真性(含未知参数不丢失)、isModelInfoPath 含斜杠路径、modelOverride 覆写
|
||||
|
||||
## 7. Handler 层改造
|
||||
|
||||
- [ ] 7.1 修改 handler/proxy_handler.go:HandleProxy 按接口类型分发 — Models/ModelInfo 本地聚合;Chat/Embed/Rerank 用 adapter.ExtractModelName 提取统一 ID 路由,同协议走 Smart Passthrough(adapter.RewriteRequestModelName 改写请求、adapter.RewriteResponseModelName 改写响应),跨协议走全量转换(modelOverride);删除 forwardPassthrough 和硬编码的 extractModelName
|
||||
- [ ] 7.2 修改 handler/model_handler.go:请求体字段适配(移除 id 输入、保留 provider_id 和 model_name),响应新增 unified_id,Create 使用 UUID
|
||||
- [ ] 7.3 修改 handler/provider_handler.go:CreateProvider 校验 ID 字符集
|
||||
- [ ] 7.4 编写 handler 层测试:统一模型 ID 路由、同协议 Smart Passthrough 保真性、跨协议 modelOverride、Models 聚合、ModelInfo 查询、流式场景 model 覆写、provider ID 校验
|
||||
|
||||
## 8. 路由注册适配
|
||||
|
||||
- [ ] 8.1 修改 cmd/server/main.go:setupRoutes 适配 handler 签名变更,传递新增依赖
|
||||
|
||||
## 9. 文档更新
|
||||
|
||||
- [ ] 9.1 按需更新 README.md:同步 models 表结构、API 接口字段、统一模型 ID 格式、Smart Passthrough 策略等变更说明
|
||||
@@ -1,6 +1,10 @@
|
||||
# Anthropic Protocol Proxy
|
||||
|
||||
## MODIFIED Requirements
|
||||
## Purpose
|
||||
|
||||
定义 Anthropic Messages API 端点的协议代理行为,包括请求处理流程、同协议透传和双向协议转换。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 支持 Anthropic Messages API 端点
|
||||
|
||||
@@ -38,50 +42,10 @@
|
||||
- **THEN** SHALL 将 OpenAI ChatCompletionResponse 解码为 CanonicalResponse
|
||||
- **THEN** SHALL 将 CanonicalResponse 编码为 Anthropic MessagesResponse
|
||||
|
||||
#### Scenario: OpenAI 客户端 → Anthropic 供应<EFBFBD><EFBFBD>
|
||||
#### Scenario: OpenAI 客户端 → Anthropic 供应商
|
||||
|
||||
- **WHEN** 客户端使用 OpenAI 协议且供应商使用 Anthropic 协议
|
||||
- **THEN** SHALL 将 OpenAI ChatCompletionRequest 解码为 CanonicalRequest
|
||||
- **THEN** SHALL 将 CanonicalRequest 编码为 Anthropic MessagesRequest
|
||||
- **THEN** SHALL 将 Anthropic MessagesResponse 解码为 CanonicalResponse
|
||||
- **THEN** SHALL 将 CanonicalResponse 编码为 OpenAI ChatCompletionResponse
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 使用 service 层处理请求
|
||||
|
||||
Handler SHALL 通过 service 层处理业务逻辑。
|
||||
|
||||
#### Scenario: 调用 routing service
|
||||
|
||||
- **WHEN** ProxyHandler 收到 Anthropic 协议请求
|
||||
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
|
||||
- **THEN** SHALL 从路由结果获取 Provider(含 protocol 字段)
|
||||
|
||||
#### Scenario: 调用 stats service
|
||||
|
||||
- **WHEN** 请求成功完成
|
||||
- **THEN** SHALL 调用 StatsService.Record() 记录统计
|
||||
- **THEN** SHALL 异步记录统计(不阻塞响应)
|
||||
|
||||
### Requirement: 使用结构化错误处理
|
||||
|
||||
ProxyHandler SHALL 使用 ConversionError 和 Anthropic 的 encodeError 处理错误。
|
||||
|
||||
#### Scenario: 协议转换错误
|
||||
|
||||
- **WHEN** ConversionEngine 返回 ConversionError
|
||||
- **THEN** SHALL 使用 Anthropic 的 Adapter.encodeError 编码错误响应
|
||||
- **THEN** SHALL 使用 Anthropic 错误格式(`{type: "error", error: {type, message}}`)
|
||||
|
||||
#### Scenario: 路由错误处理
|
||||
|
||||
- **WHEN** RoutingService 返回错误
|
||||
- **THEN** SHALL 转换为 ConversionError
|
||||
- **THEN** SHALL 使用 Anthropic 错误格式返回
|
||||
|
||||
#### Scenario: 供应商错误处理
|
||||
|
||||
- **WHEN** ProviderClient 返回错误
|
||||
- **THEN** SHALL 包装为 ConversionError
|
||||
- **THEN** SHALL 使用 Anthropic 错误格式返回
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# CLI Config
|
||||
|
||||
## Purpose
|
||||
|
||||
提供命令行参数配置支持,允许用户通过 CLI 参数临时覆盖配置,方便测试、调试和一次性使用场景。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 命令行参数配置支持
|
||||
|
||||
系统 SHALL 支持通过命令行参数设置所有配置项。
|
||||
|
||||
#### Scenario: 基本参数解析
|
||||
|
||||
- **WHEN** 应用启动时传入命令行参数
|
||||
- **THEN** SHALL 解析所有 CLI 参数
|
||||
- **THEN** SHALL 将参数值应用到对应配置项
|
||||
|
||||
#### Scenario: 参数命名规范
|
||||
|
||||
- **WHEN** 使用命令行参数
|
||||
- **THEN** SHALL 使用 kebab-case 命名(如 `--server-port`)
|
||||
- **THEN** SHALL 保持完整的层次结构(如 `--database-max-idle-conns`)
|
||||
- **THEN** SHALL 与配置文件路径对应(`database.max_idle_conns` → `--database-max-idle-conns`)
|
||||
|
||||
#### Scenario: 参数类型支持
|
||||
|
||||
- **WHEN** 解析不同类型的参数
|
||||
- **THEN** SHALL 支持 int 类型(如 `--server-port 9000`)
|
||||
- **THEN** SHALL 支持 string 类型(如 `--database-path /data/nex.db`)
|
||||
- **THEN** SHALL 支持 duration 类型(如 `--server-read-timeout 60s`)
|
||||
- **THEN** SHALL 支持 bool 类型(如 `--log-compress`)
|
||||
|
||||
### Requirement: 配置文件路径参数
|
||||
|
||||
系统 SHALL 支持通过 CLI 参数指定配置文件路径。
|
||||
|
||||
#### Scenario: 自定义配置文件路径
|
||||
|
||||
- **WHEN** 启动时指定 `--config /path/to/custom.yaml`
|
||||
- **THEN** SHALL 从指定路径加载配置文件
|
||||
- **THEN** SHALL NOT 使用默认路径 `~/.nex/config.yaml`
|
||||
|
||||
#### Scenario: 未指定配置文件路径
|
||||
|
||||
- **WHEN** 启动时未指定 `--config` 参数
|
||||
- **THEN** SHALL 使用默认路径 `~/.nex/config.yaml`
|
||||
|
||||
### Requirement: 完整配置覆盖
|
||||
|
||||
系统 SHALL 支持通过 CLI 参数覆盖所有配置项。
|
||||
|
||||
#### Scenario: 服务器配置参数
|
||||
|
||||
- **WHEN** 使用服务器相关参数
|
||||
- **THEN** SHALL 支持 `--server-port`
|
||||
- **THEN** SHALL 支持 `--server-read-timeout`
|
||||
- **THEN** SHALL 支持 `--server-write-timeout`
|
||||
|
||||
#### Scenario: 数据库配置参数
|
||||
|
||||
- **WHEN** 使用数据库相关参数
|
||||
- **THEN** SHALL 支持 `--database-path`
|
||||
- **THEN** SHALL 支持 `--database-max-idle-conns`
|
||||
- **THEN** SHALL 支持 `--database-max-open-conns`
|
||||
- **THEN** SHALL 支持 `--database-conn-max-lifetime`
|
||||
|
||||
#### Scenario: 日志配置参数
|
||||
|
||||
- **WHEN** 使用日志相关参数
|
||||
- **THEN** SHALL 支持 `--log-level`
|
||||
- **THEN** SHALL 支持 `--log-path`
|
||||
- **THEN** SHALL 支持 `--log-max-size`
|
||||
- **THEN** SHALL 支持 `--log-max-backups`
|
||||
- **THEN** SHALL 支持 `--log-max-age`
|
||||
- **THEN** SHALL 支持 `--log-compress`
|
||||
|
||||
### Requirement: 参数帮助信息
|
||||
|
||||
系统 SHALL 提供完整的参数帮助信息。
|
||||
|
||||
#### Scenario: 帮助文档生成
|
||||
|
||||
- **WHEN** 使用 `--help` 参数
|
||||
- **THEN** SHALL 显示所有支持的参数
|
||||
- **THEN** SHALL 按功能分组展示参数(服务器、数据库、日志)
|
||||
- **THEN** SHALL 显示每个参数的默认值
|
||||
- **THEN** SHALL 显示每个参数的说明
|
||||
|
||||
### Requirement: 参数错误处理
|
||||
|
||||
系统 SHALL 正确处理参数错误。
|
||||
|
||||
#### Scenario: 无效参数值
|
||||
|
||||
- **WHEN** 传入无效的参数值(如 `--server-port abc`)
|
||||
- **THEN** SHALL 返回明确的错误信息
|
||||
- **THEN** SHALL 指示参数名称和期望类型
|
||||
- **THEN** SHALL NOT 启动应用
|
||||
|
||||
#### Scenario: 未知参数
|
||||
|
||||
- **WHEN** 传入未定义的参数(如 `--unknown-param value`)
|
||||
- **THEN** SHALL 返回错误信息
|
||||
- **THEN** SHALL 指示未知参数名称
|
||||
- **THEN** SHALL NOT 启动应用
|
||||
@@ -1,6 +1,10 @@
|
||||
# Config Management
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
统一配置管理系统,支持 YAML 配置文件、CLI 参数、环境变量和默认值四种配置源,提供优先级管理、来源追踪和配置验证。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 使用 YAML 配置文件
|
||||
|
||||
@@ -66,6 +70,21 @@
|
||||
- **THEN** SHALL 指示哪些字段无效
|
||||
- **THEN** SHALL 应用 SHALL NOT 启动
|
||||
|
||||
#### Scenario: 验证规则定义
|
||||
|
||||
- **WHEN** 定义配置结构体
|
||||
- **THEN** SHALL 使用 `validate` tag 定义验证规则
|
||||
- **THEN** SHALL 支持 `required` 规则
|
||||
- **THEN** SHALL 支持 `min`、`max` 规则
|
||||
- **THEN** SHALL 支持 `oneof` 规则
|
||||
|
||||
#### Scenario: 验证执行
|
||||
|
||||
- **WHEN** 加载配置后
|
||||
- **THEN** SHALL 自动执行结构体验证
|
||||
- **THEN** SHALL 返回验证错误
|
||||
- **THEN** SHALL NOT 启动应用(如果验证失败)
|
||||
|
||||
### Requirement: 配置结构定义
|
||||
|
||||
系统 SHALL 定义清晰的配置结构。
|
||||
@@ -117,37 +136,6 @@
|
||||
- **THEN** log.max_age SHALL 为 30 (days)
|
||||
- **THEN** log.compress SHALL 为 true
|
||||
|
||||
### Requirement: 配置重载支持
|
||||
|
||||
系统 SHALL 支持配置重载(未来扩展)。
|
||||
|
||||
#### Scenario: 配置热重载
|
||||
|
||||
- **WHEN** 配置文件修改(未来功能)
|
||||
- **THEN** SHALL 支持重新加载配置
|
||||
- **THEN** SHALL 应用新配置到可动态调整的参数
|
||||
|
||||
注:当前版本不支持,仅为未来扩展预留接口。
|
||||
|
||||
### Requirement: 多层配置源支持
|
||||
|
||||
系统 SHALL 支持多种配置源。
|
||||
|
||||
#### Scenario: 配置源类型
|
||||
|
||||
- **WHEN** 加载配置
|
||||
- **THEN** SHALL 支持命令行参数配置源
|
||||
- **THEN** SHALL 支持环境变量配置源
|
||||
- **THEN** SHALL 支持配置文件配置源
|
||||
- **THEN** SHALL 支持默认值配置源
|
||||
|
||||
#### Scenario: 配置源合并
|
||||
|
||||
- **WHEN** 多个配置源同时存在
|
||||
- **THEN** SHALL 合并所有配置源
|
||||
- **THEN** SHALL 按优先级处理冲突
|
||||
- **THEN** SHALL 生成最终配置
|
||||
|
||||
### Requirement: 配置加载流程
|
||||
|
||||
系统 SHALL 实现标准化的配置加载流程。
|
||||
@@ -173,6 +161,93 @@
|
||||
- **THEN** SHALL 指示失败步骤
|
||||
- **THEN** SHALL NOT 启动应用
|
||||
|
||||
### Requirement: 配置优先级管理
|
||||
|
||||
系统 SHALL 实现明确的配置优先级机制。
|
||||
|
||||
#### Scenario: 优先级顺序
|
||||
|
||||
- **WHEN** 同一配置项在多个配置源中设置
|
||||
- **THEN** SHALL 按以下优先级顺序(从高到低):
|
||||
1. CLI 参数
|
||||
2. 环境变量
|
||||
3. 配置文件
|
||||
4. 默认值
|
||||
|
||||
#### Scenario: CLI 参数最高优先级
|
||||
|
||||
- **WHEN** 配置文件设置 `server.port: 9826`
|
||||
- **AND** 环境变量设置 `NEX_SERVER_PORT=9000`
|
||||
- **AND** CLI 参数设置 `--server-port 8080`
|
||||
- **THEN** SHALL 使用 CLI 参数值 8080
|
||||
|
||||
#### Scenario: 环境变量次高优先级
|
||||
|
||||
- **WHEN** 配置文件设置 `server.port: 9826`
|
||||
- **AND** 环境变量设置 `NEX_SERVER_PORT=9000`
|
||||
- **AND** 未设置 CLI 参数
|
||||
- **THEN** SHALL 使用环境变量值 9000
|
||||
|
||||
#### Scenario: 配置文件次低优先级
|
||||
|
||||
- **WHEN** 配置文件设置 `server.port: 9826`
|
||||
- **AND** 未设置环境变量
|
||||
- **AND** 未设置 CLI 参数
|
||||
- **THEN** SHALL 使用配置文件值 9826
|
||||
|
||||
#### Scenario: 默认值最低优先级
|
||||
|
||||
- **WHEN** 配置文件中未设置某配置项
|
||||
- **AND** 未设置环境变量
|
||||
- **AND** 未设置 CLI 参数
|
||||
- **THEN** SHALL 使用默认值
|
||||
|
||||
#### Scenario: 部分配置覆盖
|
||||
|
||||
- **WHEN** 配置文件设置完整配置
|
||||
- **AND** CLI 参数仅覆盖部分配置项
|
||||
- **THEN** SHALL 合并所有配置源
|
||||
- **THEN** SHALL 使用高优先级源覆盖指定项
|
||||
- **THEN** SHALL 保留其他配置源中的未覆盖项
|
||||
|
||||
#### Scenario: 配置项独立覆盖
|
||||
|
||||
- **WHEN** 仅通过 CLI 参数设置 `--server-port 9000`
|
||||
- **THEN** SHALL 仅覆盖 server.port 配置项
|
||||
- **THEN** SHALL NOT 影响其他配置项
|
||||
- **THEN** SHALL 其他配置项使用配置文件或默认值
|
||||
|
||||
#### Scenario: 启动后配置锁定
|
||||
|
||||
- **WHEN** 应用启动完成
|
||||
- **THEN** SHALL 锁定配置值
|
||||
- **THEN** SHALL NOT 支持运行时修改配置优先级
|
||||
- **THEN** SHALL NOT 支持运行时添加新配置源
|
||||
|
||||
### Requirement: 配置来源追踪
|
||||
|
||||
系统 SHALL 追踪每个配置值的来源,提供配置覆盖的透明信息。
|
||||
|
||||
#### Scenario: 来源记录
|
||||
|
||||
- **WHEN** 加载配置完成
|
||||
- **THEN** SHALL 记录每个配置项的来源(CLI/ENV/File/Default)
|
||||
- **THEN** SHALL 在配置摘要中显示来源信息
|
||||
|
||||
#### Scenario: 来源统计
|
||||
|
||||
- **WHEN** 打印配置摘要
|
||||
- **THEN** SHALL 统计来自 CLI 参数的配置项数量
|
||||
- **THEN** SHALL 统计来自环境变量的配置项数量
|
||||
- **THEN** SHALL 统计来自配置文件的配置项数量
|
||||
- **THEN** SHALL 统计使用默认值的配置项数量
|
||||
|
||||
#### Scenario: 覆盖提示
|
||||
|
||||
- **WHEN** CLI 参数覆盖配置文件值
|
||||
- **THEN** SHALL 在日志中记录覆盖信息
|
||||
- **THEN** SHALL 显示被覆盖的配置项名称
|
||||
|
||||
### Requirement: 配置摘要输出
|
||||
|
||||
系统 SHALL 在启动时输出配置摘要。
|
||||
@@ -192,21 +267,105 @@
|
||||
- **THEN** SHALL 使用分隔线和分组
|
||||
- **THEN** SHALL 易于阅读和理解
|
||||
|
||||
### Requirement: 配置结构体验证
|
||||
### Requirement: CLI 参数配置支持
|
||||
|
||||
系统 SHALL 使用结构体 tag 进行配置验证。
|
||||
系统 SHALL 支持通过命令行参数设置所有配置项。
|
||||
|
||||
#### Scenario: 验证规则定义
|
||||
#### Scenario: 基本参数解析
|
||||
|
||||
- **WHEN** 定义配置结构体
|
||||
- **THEN** SHALL 使用 `validate` tag 定义验证规则
|
||||
- **THEN** SHALL 支持 `required` 规则
|
||||
- **THEN** SHALL 支持 `min`、`max` 规则
|
||||
- **THEN** SHALL 支持 `oneof` 规则
|
||||
- **WHEN** 应用启动时传入命令行参数
|
||||
- **THEN** SHALL 解析所有 CLI 参数
|
||||
- **THEN** SHALL 将参数值应用到对应配置项
|
||||
|
||||
#### Scenario: 验证执行
|
||||
#### Scenario: 参数命名规范
|
||||
|
||||
- **WHEN** 加载配置后
|
||||
- **THEN** SHALL 自动执行结构体验证
|
||||
- **THEN** SHALL 返回验证错误
|
||||
- **THEN** SHALL NOT 启动应用(如果验证失败)
|
||||
- **WHEN** 使用命令行参数
|
||||
- **THEN** SHALL 使用 kebab-case 命名(如 `--server-port`)
|
||||
- **THEN** SHALL 保持完整的层次结构(如 `--database-max-idle-conns`)
|
||||
- **THEN** SHALL 与配置文件路径对应(`database.max_idle_conns` → `--database-max-idle-conns`)
|
||||
|
||||
#### Scenario: 参数类型支持
|
||||
|
||||
- **WHEN** 解析不同类型的参数
|
||||
- **THEN** SHALL 支持 int 类型(如 `--server-port 9000`)
|
||||
- **THEN** SHALL 支持 string 类型(如 `--database-path /data/nex.db`)
|
||||
- **THEN** SHALL 支持 duration 类型(如 `--server-read-timeout 60s`)
|
||||
- **THEN** SHALL 支持 bool 类型(如 `--log-compress`)
|
||||
|
||||
#### Scenario: 完整配置覆盖
|
||||
|
||||
- **WHEN** 使用服务器相关参数
|
||||
- **THEN** SHALL 支持 `--server-port`、`--server-read-timeout`、`--server-write-timeout`
|
||||
- **WHEN** 使用数据库相关参数
|
||||
- **THEN** SHALL 支持 `--database-path`、`--database-max-idle-conns`、`--database-max-open-conns`、`--database-conn-max-lifetime`
|
||||
- **WHEN** 使用日志相关参数
|
||||
- **THEN** SHALL 支持 `--log-level`、`--log-path`、`--log-max-size`、`--log-max-backups`、`--log-max-age`、`--log-compress`
|
||||
|
||||
#### Scenario: 参数帮助信息
|
||||
|
||||
- **WHEN** 使用 `--help` 参数
|
||||
- **THEN** SHALL 显示所有支持的参数
|
||||
- **THEN** SHALL 按功能分组展示参数(服务器、数据库、日志)
|
||||
- **THEN** SHALL 显示每个参数的默认值和说明
|
||||
|
||||
#### Scenario: 参数错误处理
|
||||
|
||||
- **WHEN** 传入无效的参数值(如 `--server-port abc`)
|
||||
- **THEN** SHALL 返回明确的错误信息,指示参数名称和期望类型
|
||||
- **THEN** SHALL NOT 启动应用
|
||||
- **WHEN** 传入未定义的参数(如 `--unknown-param value`)
|
||||
- **THEN** SHALL 返回错误信息,指示未知参数名称
|
||||
- **THEN** SHALL NOT 启动应用
|
||||
|
||||
### Requirement: 环境变量配置支持
|
||||
|
||||
系统 SHALL 支持通过环境变量设置所有配置项,符合 12-Factor App 原则。
|
||||
|
||||
#### Scenario: 环境变量读取
|
||||
|
||||
- **WHEN** 应用启动时存在环境变量
|
||||
- **THEN** SHALL 自动读取所有 `NEX_` 前缀的环境变量
|
||||
- **THEN** SHALL 将环境变量值应用到对应配置项
|
||||
|
||||
#### Scenario: 环境变量命名规范
|
||||
|
||||
- **WHEN** 使用环境变量配置
|
||||
- **THEN** SHALL 使用 `NEX_` 前缀
|
||||
- **THEN** SHALL 使用大写字母和下划线分隔(如 `NEX_SERVER_PORT`)
|
||||
- **THEN** SHALL 保持完整层次结构(如 `NEX_DATABASE_MAX_IDLE_CONNS`)
|
||||
- **THEN** SHALL 与配置文件路径对应(`database.max_idle_conns` → `NEX_DATABASE_MAX_IDLE_CONNS`)
|
||||
|
||||
#### Scenario: 环境变量类型转换
|
||||
|
||||
- **WHEN** 解析不同类型的环境变量
|
||||
- **THEN** SHALL 支持 int 类型(如 `NEX_SERVER_PORT=9000`)
|
||||
- **THEN** SHALL 支持 string 类型(如 `NEX_DATABASE_PATH=/data/nex.db`)
|
||||
- **THEN** SHALL 支持 duration 类型(如 `NEX_SERVER_READ_TIMEOUT=60s`)
|
||||
- **THEN** SHALL 支持 bool 类型(如 `NEX_LOG_COMPRESS=true`)
|
||||
|
||||
#### Scenario: 完整环境变量覆盖
|
||||
|
||||
- **WHEN** 设置服务器相关环境变量
|
||||
- **THEN** SHALL 支持 `NEX_SERVER_PORT`、`NEX_SERVER_READ_TIMEOUT`、`NEX_SERVER_WRITE_TIMEOUT`
|
||||
- **WHEN** 设置数据库相关环境变量
|
||||
- **THEN** SHALL 支持 `NEX_DATABASE_PATH`、`NEX_DATABASE_MAX_IDLE_CONNS`、`NEX_DATABASE_MAX_OPEN_CONNS`、`NEX_DATABASE_CONN_MAX_LIFETIME`
|
||||
- **WHEN** 设置日志相关环境变量
|
||||
- **THEN** SHALL 支持 `NEX_LOG_LEVEL`、`NEX_LOG_PATH`、`NEX_LOG_MAX_SIZE`、`NEX_LOG_MAX_BACKUPS`、`NEX_LOG_MAX_AGE`、`NEX_LOG_COMPRESS`
|
||||
|
||||
#### Scenario: 12-Factor App 合规
|
||||
|
||||
- **WHEN** 应用部署到不同环境
|
||||
- **THEN** SHALL 通过环境变量区分环境配置
|
||||
- **THEN** SHALL NOT 修改代码或配置文件
|
||||
- **WHEN** 配置包含敏感信息(如密钥、密码)
|
||||
- **THEN** SHALL 通过环境变量传递
|
||||
- **THEN** SHALL NOT 存储在配置文件中
|
||||
|
||||
#### Scenario: 环境变量错误处理
|
||||
|
||||
- **WHEN** 环境变量值格式无效(如 `NEX_SERVER_PORT=abc`)
|
||||
- **THEN** SHALL 返回明确的错误信息,指示环境变量名称和期望类型
|
||||
- **THEN** SHALL NOT 启动应用
|
||||
- **WHEN** 必需配置项既无配置文件也无环境变量
|
||||
- **THEN** SHALL 使用默认值
|
||||
- **THEN** SHALL 正常启动应用
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# Config Priority
|
||||
|
||||
## Purpose
|
||||
|
||||
实现配置优先级管理,确保多层配置源的正确合并和覆盖,提供配置来源追踪和透明性。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 配置优先级管理
|
||||
|
||||
系统 SHALL 实现明确的配置优先级机制。
|
||||
|
||||
#### Scenario: 优先级顺序
|
||||
|
||||
- **WHEN** 同一配置项在多个配置源中设置
|
||||
- **THEN** SHALL 按以下优先级顺序(从高到低):
|
||||
1. CLI 参数
|
||||
2. 环境变量
|
||||
3. 配置文件
|
||||
4. 默认值
|
||||
|
||||
#### Scenario: CLI 参数最高优先级
|
||||
|
||||
- **WHEN** 配置文件设置 `server.port: 9826`
|
||||
- **AND** 环境变量设置 `NEX_SERVER_PORT=9000`
|
||||
- **AND** CLI 参数设置 `--server-port 8080`
|
||||
- **THEN** SHALL 使用 CLI 参数值 8080
|
||||
|
||||
#### Scenario: 环境变量次高优先级
|
||||
|
||||
- **WHEN** 配置文件设置 `server.port: 9826`
|
||||
- **AND** 环境变量设置 `NEX_SERVER_PORT=9000`
|
||||
- **AND** 未设置 CLI 参数
|
||||
- **THEN** SHALL 使用环境变量值 9000
|
||||
|
||||
#### Scenario: 配置文件次低优先级
|
||||
|
||||
- **WHEN** 配置文件设置 `server.port: 9826`
|
||||
- **AND** 未设置环境变量
|
||||
- **AND** 未设置 CLI 参数
|
||||
- **THEN** SHALL 使用配置文件值 9826
|
||||
|
||||
#### Scenario: 默认值最低优先级
|
||||
|
||||
- **WHEN** 配置文件中未设置某配置项
|
||||
- **AND** 未设置环境变量
|
||||
- **AND** 未设置 CLI 参数
|
||||
- **THEN** SHALL 使用默认值
|
||||
|
||||
### Requirement: 配置来源追踪
|
||||
|
||||
系统 SHALL 追踪每个配置值的来源。
|
||||
|
||||
#### Scenario: 来源记录
|
||||
|
||||
- **WHEN** 加载配置完成
|
||||
- **THEN** SHALL 记录每个配置项的来源(CLI/ENV/File/Default)
|
||||
- **THEN** SHALL 在配置摘要中显示来源信息
|
||||
|
||||
#### Scenario: 来源统计
|
||||
|
||||
- **WHEN** 打印配置摘要
|
||||
- **THEN** SHALL 统计来自 CLI 参数的配置项数量
|
||||
- **THEN** SHALL 统计来自环境变量的配置项数量
|
||||
- **THEN** SHALL 统计来自配置文件的配置项数量
|
||||
- **THEN** SHALL 统计使用默认值的配置项数量
|
||||
|
||||
### Requirement: 配置覆盖透明性
|
||||
|
||||
系统 SHALL 提供配置覆盖的透明信息。
|
||||
|
||||
#### Scenario: 覆盖提示
|
||||
|
||||
- **WHEN** CLI 参数覆盖配置文件值
|
||||
- **THEN** SHALL 在日志中记录覆盖信息
|
||||
- **THEN** SHALL 显示被覆盖的配置项名称
|
||||
|
||||
#### Scenario: 配置摘要展示
|
||||
|
||||
- **WHEN** 应用启动完成
|
||||
- **THEN** SHALL 打印配置摘要
|
||||
- **THEN** SHALL 显示关键配置项的最终值
|
||||
- **THEN** SHALL 显示配置文件路径
|
||||
- **THEN** SHALL 显示环境变量数量
|
||||
- **THEN** SHALL 显示 CLI 参数数量
|
||||
|
||||
### Requirement: 部分配置覆盖
|
||||
|
||||
系统 SHALL 支持部分配置覆盖。
|
||||
|
||||
#### Scenario: 混合配置源
|
||||
|
||||
- **WHEN** 配置文件设置完整配置
|
||||
- **AND** CLI 参数仅覆盖部分配置项
|
||||
- **THEN** SHALL 合并所有配置源
|
||||
- **THEN** SHALL 使用 CLI 参数覆盖指定项
|
||||
- **THEN** SHALL 保留配置文件中的其他配置项
|
||||
|
||||
#### Scenario: 配置项独立覆盖
|
||||
|
||||
- **WHEN** 仅通过 CLI 参数设置 `--server-port 9000`
|
||||
- **THEN** SHALL 仅覆盖 server.port 配置项
|
||||
- **THEN** SHALL NOT 影响其他配置项
|
||||
- **THEN** SHALL 其他配置项使用配置文件或默认值
|
||||
|
||||
### Requirement: 配置优先级不可变性
|
||||
|
||||
系统 SHALL 确保配置优先级在运行时不可变。
|
||||
|
||||
#### Scenario: 启动后配置锁定
|
||||
|
||||
- **WHEN** 应用启动完成
|
||||
- **THEN** SHALL 锁定配置值
|
||||
- **THEN** SHALL NOT 支持运行时修改配置优先级
|
||||
- **THEN** SHALL NOT 支持运行时添加新配置源
|
||||
|
||||
注:配置热重载为未来扩展功能,当前版本不支持。
|
||||
@@ -1,6 +1,8 @@
|
||||
# Conversion Engine
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
定义协议无关的 Canonical Model 和 ConversionEngine 转换引擎,作为所有协议间请求/响应转换的统一枢纽。
|
||||
|
||||
### Requirement: 定义 CanonicalRequest 规范模型
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Database Migration
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
使用 goose 管理数据库迁移,支持自动执行、回滚和版本化管理。
|
||||
|
||||
### Requirement: 使用 goose 迁移工具
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
# Env Config
|
||||
|
||||
## Purpose
|
||||
|
||||
提供环境变量配置支持,符合 12-Factor App 原则,便于容器化部署和多环境配置管理。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 环境变量配置支持
|
||||
|
||||
系统 SHALL 支持通过环境变量设置所有配置项。
|
||||
|
||||
#### Scenario: 环境变量读取
|
||||
|
||||
- **WHEN** 应用启动时存在环境变量
|
||||
- **THEN** SHALL 自动读取所有 `NEX_` 前缀的环境变量
|
||||
- **THEN** SHALL 将环境变量值应用到对应配置项
|
||||
|
||||
#### Scenario: 环境变量命名规范
|
||||
|
||||
- **WHEN** 使用环境变量配置
|
||||
- **THEN** SHALL 使用 `NEX_` 前缀
|
||||
- **THEN** SHALL 使用大写字母和下划线分隔(如 `NEX_SERVER_PORT`)
|
||||
- **THEN** SHALL 保持完整层次结构(如 `NEX_DATABASE_MAX_IDLE_CONNS`)
|
||||
- **THEN** SHALL 与配置文件路径对应(`database.max_idle_conns` → `NEX_DATABASE_MAX_IDLE_CONNS`)
|
||||
|
||||
#### Scenario: 环境变量类型转换
|
||||
|
||||
- **WHEN** 解析不同类型的环境变量
|
||||
- **THEN** SHALL 支持 int 类型(如 `NEX_SERVER_PORT=9000`)
|
||||
- **THEN** SHALL 支持 string 类型(如 `NEX_DATABASE_PATH=/data/nex.db`)
|
||||
- **THEN** SHALL 支持 duration 类型(如 `NEX_SERVER_READ_TIMEOUT=60s`)
|
||||
- **THEN** SHALL 支持 bool 类型(如 `NEX_LOG_COMPRESS=true`)
|
||||
|
||||
### Requirement: 完整配置覆盖
|
||||
|
||||
系统 SHALL 支持通过环境变量覆盖所有配置项。
|
||||
|
||||
#### Scenario: 服务器配置环境变量
|
||||
|
||||
- **WHEN** 设置服务器相关环境变量
|
||||
- **THEN** SHALL 支持 `NEX_SERVER_PORT`
|
||||
- **THEN** SHALL 支持 `NEX_SERVER_READ_TIMEOUT`
|
||||
- **THEN** SHALL 支持 `NEX_SERVER_WRITE_TIMEOUT`
|
||||
|
||||
#### Scenario: 数据库配置环境变量
|
||||
|
||||
- **WHEN** 设置数据库相关环境变量
|
||||
- **THEN** SHALL 支持 `NEX_DATABASE_PATH`
|
||||
- **THEN** SHALL 支持 `NEX_DATABASE_MAX_IDLE_CONNS`
|
||||
- **THEN** SHALL 支持 `NEX_DATABASE_MAX_OPEN_CONNS`
|
||||
- **THEN** SHALL 支持 `NEX_DATABASE_CONN_MAX_LIFETIME`
|
||||
|
||||
#### Scenario: 日志配置环境变量
|
||||
|
||||
- **WHEN** 设置日志相关环境变量
|
||||
- **THEN** SHALL 支持 `NEX_LOG_LEVEL`
|
||||
- **THEN** SHALL 支持 `NEX_LOG_PATH`
|
||||
- **THEN** SHALL 支持 `NEX_LOG_MAX_SIZE`
|
||||
- **THEN** SHALL 支持 `NEX_LOG_MAX_BACKUPS`
|
||||
- **THEN** SHALL 支持 `NEX_LOG_MAX_AGE`
|
||||
- **THEN** SHALL 支持 `NEX_LOG_COMPRESS`
|
||||
|
||||
### Requirement: 环境变量优先级
|
||||
|
||||
系统 SHALL 确保环境变量优先级高于配置文件但低于 CLI 参数。
|
||||
|
||||
#### Scenario: 环境变量覆盖配置文件
|
||||
|
||||
- **WHEN** 配置文件设置 `server.port: 9826`
|
||||
- **AND** 环境变量设置 `NEX_SERVER_PORT=9000`
|
||||
- **THEN** SHALL 使用环境变量值 9000
|
||||
|
||||
#### Scenario: CLI 参数覆盖环境变量
|
||||
|
||||
- **WHEN** 环境变量设置 `NEX_SERVER_PORT=9000`
|
||||
- **AND** CLI 参数设置 `--server-port 8080`
|
||||
- **THEN** SHALL 使用 CLI 参数值 8080
|
||||
|
||||
### Requirement: 12-Factor App 合规
|
||||
|
||||
系统 SHALL 符合 12-Factor App 配置原则。
|
||||
|
||||
#### Scenario: 配置与代码分离
|
||||
|
||||
- **WHEN** 应用部署到不同环境
|
||||
- **THEN** SHALL 通过环境变量区分环境配置
|
||||
- **THEN** SHALL NOT 修改代码或配置文件
|
||||
|
||||
#### Scenario: 敏感信息保护
|
||||
|
||||
- **WHEN** 配置包含敏感信息(如密钥、密码)
|
||||
- **THEN** SHALL 通过环境变量传递
|
||||
- **THEN** SHALL NOT 存储在配置文件中
|
||||
|
||||
### Requirement: 环境变量错误处理
|
||||
|
||||
系统 SHALL 正确处理环境变量错误。
|
||||
|
||||
#### Scenario: 无效环境变量值
|
||||
|
||||
- **WHEN** 环境变量值格式无效(如 `NEX_SERVER_PORT=abc`)
|
||||
- **THEN** SHALL 返回明确的错误信息
|
||||
- **THEN** SHALL 指示环境变量名称和期望类型
|
||||
- **THEN** SHALL NOT 启动应用
|
||||
|
||||
#### Scenario: 环境变量缺失
|
||||
|
||||
- **WHEN** 必需配置项既无配置文件也无环境变量
|
||||
- **THEN** SHALL 使用默认值
|
||||
- **THEN** SHALL 正常启动应用
|
||||
@@ -1,6 +1,8 @@
|
||||
# Error Handling
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
定义结构化错误类型和统一错误响应机制,支持错误包装、类型安全判断和协议级错误编码,确保全链路错误处理的一致性和可追踪性。
|
||||
|
||||
### Requirement: 定义结构化错误类型
|
||||
|
||||
@@ -169,8 +171,6 @@
|
||||
- **THEN** SHALL 使用 errors.Is 进行链式判断
|
||||
- **THEN** SHALL 使用 errors.As 提取特定类型错误
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 定义 ConversionError 错误类型
|
||||
|
||||
系统 SHALL 定义 ConversionError 结构体和 ErrorCode 枚举。
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
# 前端测试体系
|
||||
|
||||
## Purpose
|
||||
|
||||
建立前端测试体系,覆盖单元测试、组件测试和 E2E 测试,确保前端代码质量和功能正确性。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 建立前端单元测试体系
|
||||
|
||||
前端 SHALL 使用 Vitest 建立单元测试体系,覆盖 API 层和自定义 Hooks。
|
||||
|
||||
#### Scenario: API 客户端单测
|
||||
|
||||
- **WHEN** 运行 api/client.ts 的单元测试
|
||||
- **THEN** SHALL 覆盖 request<T>() 的正常响应解析
|
||||
- **THEN** SHALL 覆盖 HTTP 错误状态码(4xx、5xx)的 ApiError 抛出
|
||||
- **THEN** SHALL 覆盖网络错误的错误处理
|
||||
- **THEN** SHALL 覆盖 snake_case → camelCase 响应字段转换
|
||||
- **THEN** SHALL 覆盖 camelCase → snake_case 请求字段转换
|
||||
|
||||
#### Scenario: API 模块单测
|
||||
|
||||
- **WHEN** 运行 api/providers.ts、api/models.ts、api/stats.ts 的单元测试
|
||||
- **THEN** SHALL 覆盖所有 CRUD 函数的正确调用
|
||||
- **THEN** SHALL 覆盖参数传递和返回值类型
|
||||
|
||||
#### Scenario: 自定义 Hooks 单测
|
||||
|
||||
- **WHEN** 运行 hooks/ 目录下的单元测试
|
||||
- **THEN** SHALL 使用 @tanstack/react-query 的 renderHook 测试工具
|
||||
- **THEN** SHALL 覆盖 useQuery 数据获取(成功和失败)
|
||||
- **THEN** SHALL 覆盖 useMutation 写操作后的缓存失效
|
||||
- **THEN** SHALL 使用独立的测试 QueryClient(关闭 retry 和缓存)
|
||||
|
||||
### Requirement: 建立前端组件测试体系
|
||||
|
||||
前端 SHALL 使用 React Testing Library 建立组件测试体系。
|
||||
|
||||
#### Scenario: ProviderTable 组件测试
|
||||
|
||||
- **WHEN** 运行 ProviderTable 组件测试
|
||||
- **THEN** SHALL 验证供应商列表正确渲染
|
||||
- **THEN** SHALL 验证点击"添加"按钮弹出 Modal
|
||||
- **THEN** SHALL 验证点击"编辑"按钮弹出预填充的 Modal
|
||||
- **THEN** SHALL 验证删除操作触发 Popconfirm 确认
|
||||
|
||||
#### Scenario: ProviderForm 组件测试
|
||||
|
||||
- **WHEN** 运行 ProviderForm 组件测试
|
||||
- **THEN** SHALL 验证表单校验规则(必填字段、URL 格式)
|
||||
- **THEN** SHALL 验证提交成功后调用 onSave 回调
|
||||
- **THEN** SHALL 验证编辑模式下字段预填充
|
||||
|
||||
#### Scenario: StatsTable 组件测试
|
||||
|
||||
- **WHEN** 运行 StatsTable 组件测试
|
||||
- **THEN** SHALL 验证筛选条件交互(供应商选择、日期范围)
|
||||
- **THEN** SHALL 验证统计数据正确展示
|
||||
|
||||
### Requirement: 建立 E2E 测试体系
|
||||
|
||||
前端 SHALL 使用 Playwright 建立端到端测试体系。
|
||||
|
||||
#### Scenario: 供应商管理 E2E 测试
|
||||
|
||||
- **WHEN** 运行供应商管理的 E2E 测试
|
||||
- **THEN** SHALL 测试完整的供应商创建流程
|
||||
- **THEN** SHALL 测试供应商编辑流程
|
||||
- **THEN** SHALL 测试供应商删除流程(含确认弹窗)
|
||||
- **THEN** SHALL 测试供应商展开后的模型管理
|
||||
|
||||
#### Scenario: 统计查询 E2E 测试
|
||||
|
||||
- **WHEN** 运行统计查询的 E2E 测试
|
||||
- **THEN** SHALL 测试页面加载和默认数据展示
|
||||
- **THEN** SHALL 测试按供应商筛选
|
||||
- **THEN** SHALL 测试按日期范围筛选
|
||||
|
||||
### Requirement: 使用 MSW 进行 API Mock
|
||||
|
||||
前端测试 SHALL 使用 MSW (Mock Service Worker) 模拟后端 API 响应。
|
||||
|
||||
#### Scenario: 测试环境 MSW 配置
|
||||
|
||||
- **WHEN** 初始化测试环境
|
||||
- **THEN** SHALL 配置 MSW server 处理所有 /api/* 请求
|
||||
- **THEN** SHALL 在 setup.ts 中全局启动和清理 MSW server
|
||||
|
||||
#### Scenario: Mock 响应定义
|
||||
|
||||
- **WHEN** 编写需要 API 交互的测试
|
||||
- **THEN** SHALL 使用 MSW handler 定义期望的 API 响应
|
||||
- **THEN** SHALL 支持成功和失败两种响应场景
|
||||
- **THEN** SHALL 在每个测试用例后重置 handler
|
||||
|
||||
### Requirement: 测试文件组织
|
||||
|
||||
前端测试文件 SHALL 按层级组织在 src/__tests__/ 目录下。
|
||||
|
||||
#### Scenario: 目录结构
|
||||
|
||||
- **WHEN** 组织测试文件
|
||||
- **THEN** src/__tests__/setup.ts SHALL 包含全局测试配置
|
||||
- **THEN** src/__tests__/api/ SHALL 包含 API 层单测
|
||||
- **THEN** src/__tests__/hooks/ SHALL 包含 Hooks 单测
|
||||
- **THEN** src/__tests__/components/ SHALL 包含组件测试
|
||||
- **THEN** e2e/ 目录(项目根目录下)SHALL 包含 Playwright E2E 测试
|
||||
|
||||
### Requirement: Vitest 配置
|
||||
|
||||
前端 SHALL 配置 Vitest 作为测试运行器。
|
||||
|
||||
#### Scenario: 测试环境配置
|
||||
|
||||
- **WHEN** 运行 vitest
|
||||
- **THEN** SHALL 使用 jsdom 作为测试环境
|
||||
- **THEN** SHALL 配置 setupFiles 指向 src/__tests__/setup.ts
|
||||
- **THEN** SHALL 配置路径别名 @/ 与 tsconfig 一致
|
||||
- **THEN** SHALL 配置 @testing-library/jest-dom 匹配器
|
||||
|
||||
#### Scenario: 覆盖率配置
|
||||
|
||||
- **WHEN** 运行覆盖率报告
|
||||
- **THEN** SHALL 使用 @vitest/coverage-v8 提供者
|
||||
- **THEN** SHALL 覆盖 src/ 下的所有源文件
|
||||
|
||||
### Requirement: 建立前端单元测试覆盖
|
||||
|
||||
前端代码 SHALL 建立单元测试覆盖,纳入整体测试覆盖率统计。
|
||||
|
||||
#### Scenario: API 层测试覆盖
|
||||
|
||||
- **WHEN** 运行前端 API 层的单元测试
|
||||
- **THEN** SHALL 覆盖 api/client.ts 的请求封装和字段转换逻辑
|
||||
- **THEN** SHALL 覆盖 api/providers.ts、api/models.ts、api/stats.ts 的所有函数
|
||||
- **THEN** SHALL 使用 MSW mock API 响应
|
||||
|
||||
#### Scenario: Hooks 测试覆盖
|
||||
|
||||
- **WHEN** 运行前端 Hooks 的单元测试
|
||||
- **THEN** SHALL 覆盖 useProviders、useModels、useStats 的查询和变更逻辑
|
||||
- **THEN** SHALL 验证缓存失效和自动刷新行为
|
||||
|
||||
### Requirement: 统计仪表盘组件测试
|
||||
|
||||
前端 SHALL 为统计仪表盘的新增组件建立测试覆盖。
|
||||
|
||||
#### Scenario: StatCards 组件测试
|
||||
|
||||
- **WHEN** 运行 StatCards 组件测试
|
||||
- **THEN** SHALL 验证统计数据正确渲染(总请求量、活跃模型数、活跃供应商数、今日请求量)
|
||||
- **THEN** SHALL 验证空数据时的渲染行为
|
||||
|
||||
#### Scenario: UsageChart 组件测试
|
||||
|
||||
- **WHEN** 运行 UsageChart 组件测试
|
||||
- **THEN** SHALL 验证图表组件正确渲染
|
||||
- **THEN** SHALL 验证空数据时的渲染行为
|
||||
|
||||
### Requirement: 建立前端组件测试覆盖
|
||||
|
||||
前端 SHALL 使用 React Testing Library 建立组件测试覆盖。
|
||||
|
||||
#### Scenario: 页面组件测试覆盖
|
||||
|
||||
- **WHEN** 运行前端组件测试
|
||||
- **THEN** SHALL 覆盖 ProviderTable 的列表渲染和交互操作
|
||||
- **THEN** SHALL 覆盖 ProviderForm 的表单校验和提交
|
||||
- **THEN** SHALL 覆盖 ModelForm 的表单校验和提交
|
||||
- **THEN** SHALL 覆盖 StatsTable 的筛选和数据展示
|
||||
- **THEN** SHALL 覆盖 StatCards 的统计摘要渲染
|
||||
- **THEN** SHALL 覆盖 UsageChart 的趋势图表渲染
|
||||
|
||||
### Requirement: 建立前端 E2E 测试覆盖
|
||||
|
||||
前端 SHALL 使用 Playwright 建立 E2E 测试覆盖。
|
||||
|
||||
#### Scenario: 供应商管理 E2E 覆盖
|
||||
|
||||
- **WHEN** 运行 E2E 测试
|
||||
- **THEN** SHALL 覆盖供应商创建、编辑、删除的完整用户流程
|
||||
- **THEN** SHALL 覆盖模型创建、编辑、删除的完整用户流程
|
||||
|
||||
#### Scenario: 统计查询 E2E 覆盖
|
||||
|
||||
- **WHEN** 运行 E2E 测试
|
||||
- **THEN** SHALL 覆盖统计页面的加载和筛选查询流程
|
||||
- **THEN** SHALL 覆盖页面间的导航切换
|
||||
- **THEN** SHALL 覆盖统计摘要卡片的展示
|
||||
|
||||
#### Scenario: 侧边栏导航 E2E 覆盖
|
||||
|
||||
- **WHEN** 运行 E2E 测试
|
||||
- **THEN** SHALL 覆盖侧边栏菜单导航的页面切换
|
||||
- **THEN** SHALL 覆盖侧边栏折叠和展开操作
|
||||
|
||||
### Requirement: 前端测试集成到构建流程
|
||||
|
||||
前端测试 SHALL 集成到项目的构建和验证流程中。
|
||||
|
||||
#### Scenario: 运行前端测试命令
|
||||
|
||||
- **WHEN** 在 frontend/ 目录执行测试命令
|
||||
- **THEN** SHALL 运行所有 Vitest 单元测试和组件测试
|
||||
- **THEN** SHALL 显示测试结果
|
||||
- **THEN** SHALL 在测试失败时返回非零退出码
|
||||
|
||||
#### Scenario: 运行前端 E2E 测试命令
|
||||
|
||||
- **WHEN** 在 frontend/ 目录执行 E2E 测试命令
|
||||
- **THEN** SHALL 启动 Playwright 运行 E2E 测试
|
||||
- **THEN** SHALL 在测试失败时返回非零退出码
|
||||
@@ -138,6 +138,20 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
||||
- **THEN** 前端 SHALL 自动查询并过滤统计
|
||||
- **THEN** 统计摘要卡片和趋势图表 SHALL 同步更新
|
||||
|
||||
#### Scenario: 仪表盘区域排列
|
||||
|
||||
- **WHEN** 渲染统计页面
|
||||
- **THEN** 页面 SHALL 按以下顺序从上到下排列:统计摘要卡片、趋势图表、筛选栏和数据表格
|
||||
- **THEN** 各区域之间 SHALL 有合理的垂直间距
|
||||
- **THEN** 筛选栏和数据表格 SHALL 保持在同一个卡片中
|
||||
|
||||
#### Scenario: 数据联动
|
||||
|
||||
- **WHEN** 用户通过筛选栏修改筛选条件
|
||||
- **THEN** 统计摘要卡片和趋势图表 SHALL 随筛选条件变化更新
|
||||
- **THEN** 数据表格 SHALL 同步更新
|
||||
- **THEN** 所有区域 SHALL 共享同一份筛选后的数据
|
||||
|
||||
### Requirement: 优雅处理 API 错误
|
||||
|
||||
前端 SHALL 处理 API 错误并显示用户友好的消息。
|
||||
@@ -224,13 +238,6 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
||||
- **WHEN** 用户点击导航中的"设置"
|
||||
- **THEN** 前端 SHALL 导航到 `/settings` 并高亮当前菜单项
|
||||
|
||||
#### Scenario: 导航菜单交互
|
||||
|
||||
- **WHEN** 用户点击导航中的"供应商管理"
|
||||
- **THEN** 前端 SHALL 导航到 `/providers` 并高亮当前菜单项
|
||||
- **WHEN** 用户点击导航中的"用量统计"
|
||||
- **THEN** 前端 SHALL 导航到 `/stats` 并高亮当前菜单项
|
||||
|
||||
### Requirement: 提供导航
|
||||
|
||||
前端 SHALL 使用 React Router v7 提供导航。
|
||||
@@ -1,6 +1,8 @@
|
||||
# Layered Architecture
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
实现 handler → service → repository 三层架构,并在 handler 和 provider 之间新增 conversion 层,通过依赖注入和清晰的接口边界提高代码可维护性和可测试性。
|
||||
|
||||
### Requirement: 实现三层架构
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Middleware System
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
实现 HTTP 中间件体系,包括请求 ID、日志记录、错误恢复和 CORS 处理,确保请求的可追踪性、稳定性和跨域支持。
|
||||
|
||||
### Requirement: 实现请求 ID 中间件
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Model Management
|
||||
|
||||
## MODIFIED Requirements
|
||||
## Purpose
|
||||
|
||||
管理模型的增删改查,通过 handler → service → repository 分层实现业务逻辑和数据访问,支持供应商关联验证。
|
||||
|
||||
### Requirement: 创建模型配置
|
||||
|
||||
@@ -74,8 +76,6 @@
|
||||
|
||||
**变更说明:** 通过 ModelService 和 ModelRepository 实现。API 接口保持不变。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 使用 service 层处理业务逻辑
|
||||
|
||||
Handler SHALL 通过 ModelService 处理业务逻辑。
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# OpenAI Protocol Proxy
|
||||
|
||||
## MODIFIED Requirements
|
||||
## Purpose
|
||||
|
||||
定义 OpenAI Chat Completions API 端点的协议代理行为,包括请求处理流程、模型路由、同协议透传和跨协议转换。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 支持 OpenAI Chat Completions API 端点
|
||||
|
||||
@@ -47,9 +51,9 @@
|
||||
- **WHEN** 请求包含已禁用模型的 `model` 字段
|
||||
- **THEN** 网关 SHALL 使用 OpenAI 格式返回错误响应
|
||||
|
||||
### Requirement: 对 OpenAI 兼容供应商透明代理
|
||||
### Requirement: 跨协议请求转换
|
||||
|
||||
网关 SHALL 对 OpenAI 兼容供应商的请求和响应通过 ConversionEngine 进行转换处理。
|
||||
网关 SHALL 对非 OpenAI 兼容供应商的请求和响应通过 ConversionEngine 进行转换处理。
|
||||
|
||||
#### Scenario: 跨协议请求转发
|
||||
|
||||
@@ -61,59 +65,3 @@
|
||||
|
||||
- **WHEN** 网关收到 `/openai/v1/models` 等 GET 请求
|
||||
- **THEN** 网关 SHALL 通过 ConversionEngine 转换扩展层接口的响应格式
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 使用 service 层处理请求
|
||||
|
||||
Handler SHALL 通过 service 层处理业务逻辑。
|
||||
|
||||
#### Scenario: 调用 routing service
|
||||
|
||||
- **WHEN** handler 收到请求
|
||||
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
|
||||
- **THEN** SHALL 使用路由结果中的供应商信息
|
||||
|
||||
#### Scenario: 调用 stats service
|
||||
|
||||
- **WHEN** 请求成功完成
|
||||
- **THEN** SHALL 调用 StatsService.Record() 记录统计
|
||||
- **THEN** SHALL 异步记录统计(不阻塞响应)
|
||||
|
||||
### Requirement: 使用 service 层处理请求
|
||||
|
||||
Handler SHALL 通过 service 层处理业务逻辑。
|
||||
|
||||
#### Scenario: 调用 routing service
|
||||
|
||||
- **WHEN** ProxyHandler 收到请求
|
||||
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
|
||||
- **THEN** SHALL 从路由结果获取 Provider(含 protocol 字段)
|
||||
|
||||
#### Scenario: 调用 stats service
|
||||
|
||||
- **WHEN** 请求成功完成
|
||||
- **THEN** SHALL 调用 StatsService.Record() 记录统计
|
||||
- **THEN** SHALL 异步记录统计(不阻塞响应)
|
||||
|
||||
### Requirement: 使用结构化错误处理
|
||||
|
||||
ProxyHandler SHALL 使用 ConversionError 和协议对应的 encodeError 处理错误。
|
||||
|
||||
#### Scenario: 转换错误
|
||||
|
||||
- **WHEN** ConversionEngine 返回 ConversionError
|
||||
- **THEN** SHALL 使用 clientProtocol 的 Adapter.encodeError 编码错误响应
|
||||
- **THEN** SHALL 使用 OpenAI 错误格式(`{error: {message, type, code}}`)
|
||||
|
||||
#### Scenario: 路由错误处理
|
||||
|
||||
- **WHEN** RoutingService 返回错误
|
||||
- **THEN** SHALL 转换为 ConversionError
|
||||
- **THEN** SHALL 使用 OpenAI 错误格式返回
|
||||
|
||||
#### Scenario: 供应商错误处理
|
||||
|
||||
- **WHEN** ProviderClient 返回错误
|
||||
- **THEN** SHALL 包装为 ConversionError
|
||||
- **THEN** SHALL 使用 OpenAI 错误格式返回
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Protocol Adapter - Anthropic
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
实现 Anthropic 协议的完整 ProtocolAdapter,支持请求/响应编解码、流式转换和错误处理,遵循 Anthropic Messages API 规范。
|
||||
|
||||
### Requirement: 实现 Anthropic ProtocolAdapter
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Protocol Adapter - OpenAI
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
实现 OpenAI 协议的完整 ProtocolAdapter,支持请求/响应编解码、流式转换和错误处理,遵循 OpenAI Chat Completions API 规范。
|
||||
|
||||
### Requirement: 实现 OpenAI ProtocolAdapter
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Provider Management
|
||||
|
||||
## MODIFIED Requirements
|
||||
## Purpose
|
||||
|
||||
管理 AI 供应商的完整生命周期,包括创建、查询、更新、删除供应商配置,通过分层架构(Handler -> Service -> Repository)处理业务逻辑和数据访问。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 创建供应商配置
|
||||
|
||||
@@ -84,8 +88,6 @@
|
||||
|
||||
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 使用 service 层处理业务逻辑
|
||||
|
||||
Handler SHALL 通过 ProviderService 处理业务逻辑。
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
# Recharts Integration
|
||||
|
||||
## Purpose
|
||||
|
||||
TBD - 集成Recharts图表库,实现数据可视化功能,替代@ant-design/charts。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Recharts依赖集成
|
||||
系统SHALL正确集成Recharts图表库,作为数据可视化的基础依赖。
|
||||
|
||||
#### Scenario: 安装Recharts依赖
|
||||
- **WHEN** 依赖安装完成
|
||||
- **THEN** package.json中SHALL存在recharts依赖
|
||||
- **AND** recharts版本SHALL为2.x最新稳定版
|
||||
- **AND** Recharts组件SHALL可正常导入使用
|
||||
|
||||
### Requirement: UsageChart图表重写
|
||||
系统SHALL使用Recharts重新实现UsageChart组件,保持原有功能。
|
||||
|
||||
#### Scenario: 折线图渲染
|
||||
- **WHEN** UsageChart组件接收统计数据
|
||||
- **THEN** SHALL使用Recharts的LineChart组件渲染
|
||||
- **AND** X轴SHALL显示日期数据
|
||||
- **AND** Y轴SHALL显示请求数据
|
||||
- **AND** 图表SHALL支持鼠标悬停显示详情(Tooltip)
|
||||
- **AND** 图表SHALL包含网格线(CartesianGrid)
|
||||
|
||||
#### Scenario: 响应式图表容器
|
||||
- **WHEN** 图表渲染
|
||||
- **THEN** SHALL使用ResponsiveContainer包裹
|
||||
- **AND** 图表宽度SHALL自适应容器宽度
|
||||
- **AND** 图表高度SHALL为固定300px
|
||||
|
||||
#### Scenario: 空数据状态处理
|
||||
- **WHEN** 统计数据为空数组
|
||||
- **THEN** SHALL显示"暂无数据"提示
|
||||
- **AND** 不SHALL渲染图表组件
|
||||
|
||||
#### Scenario: 数据格式转换
|
||||
- **WHEN** 接收UsageStats数组数据
|
||||
- **THEN** SHALL按日期聚合计数
|
||||
- **AND** 数据SHALL按日期升序排序
|
||||
- **AND** 数据格式SHALL适配Recharts要求
|
||||
|
||||
### Requirement: 图表样式配置
|
||||
系统SHALL配置Recharts图表样式,确保视觉一致性。
|
||||
|
||||
#### Scenario: 图表颜色配置
|
||||
- **WHEN** 图表渲染
|
||||
- **THEN** 折线颜色SHALL为TDesign主色(#0052D9)
|
||||
- **AND** 线条宽度SHALL为2px
|
||||
- **AND** 数据点样式SHALL正常显示
|
||||
|
||||
#### Scenario: 网格线配置
|
||||
- **WHEN** 图表渲染
|
||||
- **THEN** 网格线SHALL使用虚线样式(strokeDasharray="3 3")
|
||||
- **AND** 网格线SHALL不遮挡数据线
|
||||
|
||||
### Requirement: 移除Ant Design Charts
|
||||
系统SHALL完全移除@ant-design/charts的使用。
|
||||
|
||||
#### Scenario: 移除Line组件
|
||||
- **WHEN** 迁移完成
|
||||
- **THEN** UsageChart中SHALL不存在@ant-design/charts的Line组件
|
||||
- **AND** UsageChart中SHALL不存在任何@ant-design/charts导入
|
||||
|
||||
#### Scenario: 移除配置对象模式
|
||||
- **WHEN** 图表实现
|
||||
- **THEN** SHALL使用Recharts声明式组件模式
|
||||
- **AND** 不SHALL使用Ant Design Charts的配置对象模式(xField、yField等)
|
||||
|
||||
### Requirement: 图表交互功能
|
||||
系统SHALL保持图表的基本交互功能。
|
||||
|
||||
#### Scenario: 鼠标悬停提示
|
||||
- **WHEN** 用户鼠标悬停在数据点上
|
||||
- **THEN** SHALL显示Tooltip提示框
|
||||
- **AND** 提示框SHALL显示日期和请求数
|
||||
- **AND** 提示框SHALL不遮挡图表内容
|
||||
|
||||
#### Scenario: 平滑曲线效果
|
||||
- **WHEN** 折线渲染
|
||||
- **THEN** SHALL使用monotone类型平滑曲线
|
||||
- **AND** 曲线SHALL自然流畅
|
||||
- **AND** 数据点SHALL准确对应
|
||||
|
||||
### Requirement: 图表性能
|
||||
系统SHALL确保图表渲染性能满足要求。
|
||||
|
||||
#### Scenario: 大数据量渲染
|
||||
- **WHEN** 统计数据包含大量数据点(>100)
|
||||
- **THEN** 图表SHALL在1秒内完成渲染
|
||||
- **AND** 交互响应SHALL流畅无卡顿
|
||||
|
||||
#### Scenario: 组件重渲染优化
|
||||
- **WHEN** 父组件重新渲染
|
||||
- **THEN** 图表组件SHALL避免不必要的重渲染
|
||||
- **AND** 数据未变化时SHALL保持稳定
|
||||
|
||||
### Requirement: 图表测试覆盖
|
||||
系统SHALL为Recharts图表组件提供完整的测试覆盖。
|
||||
|
||||
#### Scenario: 组件渲染测试
|
||||
- **WHEN** 运行单元测试
|
||||
- **THEN** UsageChart测试SHALL验证正常渲染
|
||||
- **AND** UsageChart测试SHALL验证空数据状态
|
||||
- **AND** UsageChart测试SHALL验证数据格式转换
|
||||
|
||||
#### Scenario: 数据处理测试
|
||||
- **WHEN** 运行单元测试
|
||||
- **THEN** 测试SHALL覆盖日期聚合逻辑
|
||||
- **AND** 测试SHALL覆盖数据排序逻辑
|
||||
- **AND** 测试SHALL验证边界情况
|
||||
@@ -1,6 +1,10 @@
|
||||
# Request Validation
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
定义请求验证规范,覆盖 AI 协议请求(OpenAI、Anthropic)和管理 API 请求的验证规则,确保输入数据的合法性和安全性。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 使用 validator 库
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# 统计仪表盘
|
||||
|
||||
## Purpose
|
||||
|
||||
TBD - 提供统计仪表盘页面,展示统计摘要卡片、请求趋势图表和详细数据表格
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 提供统计摘要卡片
|
||||
|
||||
前端 SHALL 在统计页面顶部展示统计摘要卡片行,展示关键指标的聚合数据。
|
||||
|
||||
#### Scenario: 显示统计摘要
|
||||
|
||||
- **WHEN** 加载统计页面
|
||||
- **THEN** 前端 SHALL 在页面顶部使用 Ant Design `Row` + `Col` 栅格布局展示统计卡片
|
||||
- **THEN** 每个卡片 SHALL 使用 Ant Design `Card` + `Statistic` 组件
|
||||
- **THEN** 卡片 SHALL 展示以下指标:总请求量、活跃模型数、活跃供应商数、今日请求量
|
||||
- **THEN** 指标数据 SHALL 从 `useStats` 返回的 `UsageStats[]` 数据中前端聚合计算
|
||||
|
||||
#### Scenario: 统计数据聚合计算
|
||||
|
||||
- **WHEN** 前端接收到 stats 数据
|
||||
- **THEN** 总请求量 SHALL 为所有记录的 requestCount 之和
|
||||
- **THEN** 活跃模型数 SHALL 为去重后不同 modelName 的数量
|
||||
- **THEN** 活跃供应商数 SHALL 为去重后不同 providerId 的数量
|
||||
- **THEN** 今日请求量 SHALL 为 date 等于当日日期的记录的 requestCount 之和
|
||||
|
||||
#### Scenario: 统计卡片响应式布局
|
||||
|
||||
- **WHEN** 在不同屏幕宽度下查看统计卡片
|
||||
- **THEN** 卡片 SHALL 使用 Ant Design `Col` 的响应式 span 适配不同宽度
|
||||
- **THEN** 卡片之间 SHALL 有合理的间距(Row 的 gutter)
|
||||
|
||||
### Requirement: 提供请求趋势图表
|
||||
|
||||
前端 SHALL 在统计页面展示请求量随时间变化的趋势图表。
|
||||
|
||||
#### Scenario: 显示趋势图表
|
||||
|
||||
- **WHEN** 加载统计页面
|
||||
- **THEN** 前端 SHALL 在统计摘要卡片下方展示趋势图表
|
||||
- **THEN** 图表 SHALL 使用 `@ant-design/charts` 的 `Line` 组件
|
||||
- **THEN** 图表 SHALL 包裹在 Ant Design `Card` 组件中
|
||||
- **THEN** 图表 SHALL 展示请求量随日期的变化趋势
|
||||
|
||||
#### Scenario: 图表数据格式
|
||||
|
||||
- **WHEN** 准备图表数据
|
||||
- **THEN** X 轴 SHALL 为日期(date 字段)
|
||||
- **THEN** Y 轴 SHALL 为请求计数(requestCount)
|
||||
- **THEN** 数据 SHALL 按日期聚合,同一天的请求量求和
|
||||
- **THEN** 数据 SHALL 按日期升序排列
|
||||
|
||||
#### Scenario: 图表主题适配
|
||||
|
||||
- **WHEN** 用户切换亮色/暗色主题
|
||||
- **THEN** 图表 SHALL 自动适配当前主题
|
||||
- **THEN** 图表文字和背景 SHALL 与当前主题协调
|
||||
|
||||
### Requirement: 仪表盘页面布局
|
||||
|
||||
前端 SHALL 将统计页面组织为仪表盘布局,按从上到下的顺序排列各区域。
|
||||
|
||||
#### Scenario: 仪表盘区域排列
|
||||
|
||||
- **WHEN** 渲染统计页面
|
||||
- **THEN** 页面 SHALL 按以下顺序从上到下排列:统计摘要卡片、趋势图表、筛选栏和数据表格
|
||||
- **THEN** 各区域之间 SHALL 有合理的垂直间距
|
||||
- **THEN** 筛选栏和数据表格 SHALL 保持在同一个 `Card` 中
|
||||
|
||||
#### Scenario: 数据联动
|
||||
|
||||
- **WHEN** 用户通过筛选栏修改筛选条件
|
||||
- **THEN** 统计摘要卡片和趋势图表 SHALL 随筛选条件变化更新
|
||||
- **THEN** 数据表格 SHALL 同步更新
|
||||
- **THEN** 所有区域 SHALL 共享同一份筛选后的数据
|
||||
@@ -1,6 +1,10 @@
|
||||
# Structured Logging
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
定义结构化日志规范,使用 zap 日志库提供 JSON 格式日志输出、日志文件滚动、请求 ID 追踪和多级别日志控制。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 使用 zap 结构化日志
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
# TDesign Integration
|
||||
|
||||
## Purpose
|
||||
|
||||
TBD - 集成TDesign React组件库,提供统一的UI设计系统和组件支持,替代Ant Design。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: TDesign全局配置
|
||||
系统SHALL正确配置TDesign ConfigProvider作为全局配置容器,确保所有TDesign组件使用统一的配置。
|
||||
|
||||
#### Scenario: 应用启动时加载TDesign配置
|
||||
- **WHEN** 应用初始化启动
|
||||
- **THEN** ConfigProvider组件SHALL包裹整个应用
|
||||
- **AND** TDesign全局样式SHALL被正确引入
|
||||
- **AND** 所有TDesign组件SHALL使用统一配置
|
||||
|
||||
#### Scenario: 移除Ant Design配置
|
||||
- **WHEN** 迁移完成
|
||||
- **THEN** 应用中SHALL不存在Ant Design的ConfigProvider
|
||||
- **AND** 应用中SHALL不存在ThemeProvider
|
||||
- **AND** 应用中SHALL不存在任何Ant Design全局配置
|
||||
|
||||
### Requirement: TDesign组件替换
|
||||
系统SHALL将所有Ant Design组件替换为对应的TDesign组件,保持功能完整性。
|
||||
|
||||
#### Scenario: 布局组件替换
|
||||
- **WHEN** 使用Layout组件
|
||||
- **THEN** SHALL使用TDesign的Layout组件
|
||||
- **AND** Layout.Sider SHALL替换为Layout.Aside
|
||||
- **AND** Layout.Header SHALL保持不变
|
||||
- **AND** Layout.Content SHALL保持不变
|
||||
|
||||
#### Scenario: 表单组件替换
|
||||
- **WHEN** 使用表单相关组件
|
||||
- **THEN** Modal SHALL替换为Dialog
|
||||
- **AND** Form.Item SHALL替换为Form.FormItem
|
||||
- **AND** Input、Select、Switch SHALL使用TDesign版本
|
||||
- **AND** 表单验证功能SHALL保持完整
|
||||
|
||||
#### Scenario: 数据展示组件替换
|
||||
- **WHEN** 使用数据展示组件
|
||||
- **THEN** Table、Card、Statistic、Tag SHALL使用TDesign版本
|
||||
- **AND** 组件功能SHALL保持不变
|
||||
- **AND** 组件API SHALL兼容原有使用方式
|
||||
|
||||
#### Scenario: 导航组件替换
|
||||
- **WHEN** 使用导航组件
|
||||
- **THEN** Menu SHALL使用TDesign版本
|
||||
- **AND** 菜单项配置SHALL正确映射
|
||||
- **AND** 路由导航功能SHALL保持正常
|
||||
|
||||
### Requirement: TDesign图标系统
|
||||
系统SHALL使用tdesign-icons-react作为图标库,替换@ant-design/icons。
|
||||
|
||||
#### Scenario: 图标导入替换
|
||||
- **WHEN** 使用图标组件
|
||||
- **THEN** SHALL从tdesign-icons-react导入
|
||||
- **AND** SHALL不从@ant-design/icons导入任何图标
|
||||
- **AND** 图标显示SHALL正常
|
||||
|
||||
#### Scenario: 图标语义匹配
|
||||
- **WHEN** 替换图标
|
||||
- **THEN** 新图标SHALL与原图标语义相近
|
||||
- **AND** CloudServerOutlined SHALL替换为语义相近的TDesign图标
|
||||
- **AND** BarChartOutlined SHALL替换为语义相近的TDesign图标
|
||||
- **AND** SettingOutlined SHALL替换为语义相近的TDesign图标
|
||||
|
||||
### Requirement: 栅格系统兼容
|
||||
系统SHALL保持Row/Col栅格系统的使用,确保响应式布局功能正常。
|
||||
|
||||
#### Scenario: 栅格组件替换
|
||||
- **WHEN** 使用Row和Col组件
|
||||
- **THEN** SHALL使用TDesign的Row和Col组件
|
||||
- **AND** 响应式配置(xs、sm、md、lg、xl)SHALL保持有效
|
||||
- **AND** gutter配置SHALL正常工作
|
||||
- **AND** 布局效果SHALL与迁移前一致
|
||||
|
||||
### Requirement: 主题系统移除
|
||||
系统SHALL完全移除多主题系统,使用TDesign默认主题。
|
||||
|
||||
#### Scenario: 删除主题相关代码
|
||||
- **WHEN** 迁移完成
|
||||
- **THEN** ThemeContext SHALL被删除
|
||||
- **AND** themes目录 SHALL被删除
|
||||
- **AND** 所有主题相关导入SHALL被移除
|
||||
|
||||
#### Scenario: 使用TDesign默认主题
|
||||
- **WHEN** 应用运行
|
||||
- **THEN** SHALL使用TDesign默认视觉风格
|
||||
- **AND** 不SHALL提供主题切换功能
|
||||
- **AND** UI风格SHALL统一一致
|
||||
|
||||
### Requirement: 依赖管理
|
||||
系统SHALL正确管理npm依赖,确保无冗余依赖。
|
||||
|
||||
#### Scenario: 移除Ant Design依赖
|
||||
- **WHEN** 依赖安装完成
|
||||
- **THEN** package.json中SHALL不存在antd
|
||||
- **AND** package.json中SHALL不存在@ant-design/icons
|
||||
- **AND** package.json中SHALL不存在@ant-design/charts
|
||||
- **AND** package.json中SHALL不存在antd-style
|
||||
|
||||
#### Scenario: 添加TDesign依赖
|
||||
- **WHEN** 依赖安装完成
|
||||
- **THEN** package.json中SHALL存在tdesign-react
|
||||
- **AND** package.json中SHALL存在tdesign-icons-react
|
||||
- **AND** 依赖版本SHALL为最新稳定版
|
||||
|
||||
### Requirement: Settings页面简化
|
||||
系统SHALL保留Settings页面路由,但简化页面内容为空状态。
|
||||
|
||||
#### Scenario: Settings页面访问
|
||||
- **WHEN** 用户访问/settings路由
|
||||
- **THEN** 页面SHALL正常加载
|
||||
- **AND** 页面SHALL显示空状态提示
|
||||
- **AND** 不SHALL显示主题设置相关内容
|
||||
|
||||
#### Scenario: Settings路由保留
|
||||
- **WHEN** 应用路由配置
|
||||
- **THEN** /settings路由SHALL存在
|
||||
- **AND** Settings页面组件SHALL存在
|
||||
@@ -1,6 +1,10 @@
|
||||
# Test Coverage
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
定义测试覆盖规范,建立后端和前端的单元测试、集成测试及 E2E 测试体系,确保核心业务逻辑的测试覆盖率达到目标水平。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 建立单元测试体系
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Unified Proxy Handler
|
||||
|
||||
## ADDED Requirements
|
||||
## Purpose
|
||||
|
||||
实现统一的代理处理器(ProxyHandler),替代独立的 OpenAI 和 Anthropic Handler,通过 ConversionEngine 和 RoutingService 支持多协议的请求转换与转发。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 实现统一代理 Handler
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Usage Statistics
|
||||
|
||||
## MODIFIED Requirements
|
||||
## Purpose
|
||||
|
||||
定义 API 使用统计规范,支持请求统计记录、按供应商/模型/日期范围查询统计、统计聚合以及并发安全的统计记录。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 记录请求统计
|
||||
|
||||
@@ -78,8 +82,6 @@
|
||||
|
||||
**变更说明:** 并发控制在 StatsRepository 中通过数据库事务实现。API 接口保持不变。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 使用 service 层处理业务逻辑
|
||||
|
||||
Handler SHALL 通过 StatsService 处理业务逻辑。
|
||||
|
||||
Reference in New Issue
Block a user