1
0

Compare commits

...

3 Commits

Author SHA1 Message Date
f3a207fa16 docs: 添加 openspec 变更记录,归档已完成变更并添加 unified-model-id 提案 2026-04-21 00:45:39 +08:00
56ecc73d1b docs: 整合 openspec 规范,合并配置和前端相关独立 spec
将 cli-config、config-priority、env-config 合并入 config-management;
将 tdesign-integration、recharts-integration、frontend-config-ui、
frontend-testing、stats-dashboard 合并入新的 frontend/spec.md;
清理其余 spec 中的冗余标记,补充缺失场景。
2026-04-20 19:55:56 +08:00
1ae9336cbe docs: 更新后端文档细节和内容 2026-04-20 19:47:41 +08:00
41 changed files with 1298 additions and 1163 deletions

View File

@@ -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 @@ handlerHTTP 请求处理)
→ 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 用 CLIlog.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` 等精确分割,不使用索引切片

View 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/behaviorbasic → 删掉general → 删掉)
- 避免实现模式词crud、list、table而使用业务领域词
- 避免同一关键词在不同规范中重复出现导致歧义(如 layout 只出现在一个规范名中)
- 长度控制在 2-3 个词去掉不影响检索的冗余词info、data 等)
```
---
## 补充说明
### 审查时的判断边界
- **规范 vs 代码**:规范描述"应该是什么",不描述"代码怎么写"。如果规范中出现了具体文件路径(如 `src/data/adminData.js`),通常是实现细节而非规范,应该清理
- **规范 vs 变更记录**:规范用 SHALL/WHEN/THEN 格式描述功能需求。如果出现"移除以下列"、"保持现有样式"、"ADDED/MODIFIED Requirements"等措辞,说明混入了变更指令,需要改写
- **规范 vs 文档**:规范不替代 README 或开发文档,不需要描述项目背景、技术选型等宏观信息
### 建议的定期审查节奏
- 每完成一批功能变更后,对照新代码检查相关规范是否需要更新
- 规范数量超过 30 个时,建议做一次全面审查
- 新增规范前,先搜索现有规范名称和内容,确认是否有可复用/扩展的规范

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-20

View 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 → encodecanonical 的 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
无。所有关键决策已在探索阶段确认。

View 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 需要新增/更新测试
- **前端**: 本次变更不涉及前端适配,前端后续统一适配

View File

@@ -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

View File

@@ -0,0 +1,13 @@
## MODIFIED Requirements
### Requirement: models 表 schema 变更
系统 SHALL 通过迁移脚本重建 models 表结构(服务未上线,无需考虑数据迁移)。
#### Scenario: 迁移后 models 表结构
- **WHEN** 执行迁移
- **THEN** SHALL 先 DROP 已有的 models 表(无旧数据)
- **THEN** SHALL CREATE 新的 models 表包含字段idTEXT PRIMARY KEY、provider_idTEXT NOT NULL、model_nameTEXT NOT NULL、enabledINTEGER DEFAULT 1、created_atDATETIME
- **THEN** SHALL 存在 UNIQUE(provider_id, model_name) 约束
- **THEN** SHALL 存在 FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE

View 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 的供应商)

View File

@@ -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"`,其余字段原样保留

View File

@@ -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 字段位置进行改写

View File

@@ -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)

View File

@@ -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 错误

View File

@@ -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`

View File

@@ -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_nameOwnedBy 字段为 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_nameOwnedBy 字段为 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)`

View File

@@ -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 过滤

View File

@@ -0,0 +1,53 @@
## 1. 数据库迁移
- [ ] 1.1 新增迁移脚本DROP 旧 models 表 + CREATE 新 models 表id UUID PK, provider_id, model_name, enabled, created_atUNIQUE(provider_id, model_name)
- [ ] 1.2 更新 config/models.goModel 结构体适配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.goModel 结构体字段适配,新增 UnifiedModelID() 方法
- [ ] 3.2 修改 domain/route.goRouteResult 适配新字段
## 4. Repository 层适配
- [ ] 4.1 修改 repository/model_repo.go接口变更 — GetByModelName 改为 FindByProviderAndModelName新增 ListEnabled
- [ ] 4.2 修改 repository/model_repo_impl.go实现 FindByProviderAndModelNameWHERE provider_id=? AND model_name=?、ListEnabledJOIN providers WHERE enabled
- [ ] 4.3 编写 repository 层测试
## 5. Service 层适配
- [ ] 5.1 修改 service/routing_service.goRoute 接口改为 RouteByModelName(providerID, modelName string)
- [ ] 5.2 修改 service/routing_service_impl.go调用 FindByProviderAndModelName 替代 GetByModelName
- [ ] 5.3 修改 service/model_service.goCreate 生成 UUID、新增联合唯一校验方法
- [ ] 5.4 修改 service/model_service_impl.go实现联合唯一校验、UUID 生成
- [ ] 5.5 修改 service/provider_service_impl.goCreate 时调用 ValidateProviderID 校验 ID 字符集
- [ ] 5.6 编写 service 层测试
## 6. Conversion 层适配
- [ ] 6.1 修改 conversion/adapter.goProtocolAdapter 接口新增 ExtractUnifiedModelID、ExtractModelName、RewriteRequestModelName、RewriteResponseModelName 四个方法
- [ ] 6.2 修改 conversion/engine.goConvertHttpResponse 新增 modelOverride 参数(跨协议场景),各 convert*ResponseBody 中覆写 canonical ModelCreateStreamConverter 新增 modelOverride 参数
- [ ] 6.3 修改 conversion/openai/adapter.go实现 ExtractUnifiedModelID、ExtractModelName按 ifaceType 提取 model、RewriteRequestModelName 和 RewriteResponseModelNamejson.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.goHandleProxy 按接口类型分发 — Models/ModelInfo 本地聚合Chat/Embed/Rerank 用 adapter.ExtractModelName 提取统一 ID 路由,同协议走 Smart Passthroughadapter.RewriteRequestModelName 改写请求、adapter.RewriteResponseModelName 改写响应跨协议走全量转换modelOverride删除 forwardPassthrough 和硬编码的 extractModelName
- [ ] 7.2 修改 handler/model_handler.go请求体字段适配移除 id 输入、保留 provider_id 和 model_name响应新增 unified_idCreate 使用 UUID
- [ ] 7.3 修改 handler/provider_handler.goCreateProvider 校验 ID 字符集
- [ ] 7.4 编写 handler 层测试:统一模型 ID 路由、同协议 Smart Passthrough 保真性、跨协议 modelOverride、Models 聚合、ModelInfo 查询、流式场景 model 覆写、provider ID 校验
## 8. 路由注册适配
- [ ] 8.1 修改 cmd/server/main.gosetupRoutes 适配 handler 签名变更,传递新增依赖
## 9. 文档更新
- [ ] 9.1 按需更新 README.md同步 models 表结构、API 接口字段、统一模型 ID 格式、Smart Passthrough 策略等变更说明

View File

@@ -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 错误格式返回

View File

@@ -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 启动应用

View File

@@ -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 正常启动应用

View File

@@ -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 支持运行时添加新配置源
注:配置热重载为未来扩展功能,当前版本不支持。

View File

@@ -1,6 +1,8 @@
# Conversion Engine
## ADDED Requirements
## Purpose
定义协议无关的 Canonical Model 和 ConversionEngine 转换引擎,作为所有协议间请求/响应转换的统一枢纽。
### Requirement: 定义 CanonicalRequest 规范模型

View File

@@ -1,6 +1,8 @@
# Database Migration
## ADDED Requirements
## Purpose
使用 goose 管理数据库迁移,支持自动执行、回滚和版本化管理。
### Requirement: 使用 goose 迁移工具

View File

@@ -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 正常启动应用

View File

@@ -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 枚举。

View File

@@ -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 在测试失败时返回非零退出码

View File

@@ -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 提供导航。

View File

@@ -1,6 +1,8 @@
# Layered Architecture
## ADDED Requirements
## Purpose
实现 handler → service → repository 三层架构,并在 handler 和 provider 之间新增 conversion 层,通过依赖注入和清晰的接口边界提高代码可维护性和可测试性。
### Requirement: 实现三层架构

View File

@@ -1,6 +1,8 @@
# Middleware System
## ADDED Requirements
## Purpose
实现 HTTP 中间件体系,包括请求 ID、日志记录、错误恢复和 CORS 处理,确保请求的可追踪性、稳定性和跨域支持。
### Requirement: 实现请求 ID 中间件

View File

@@ -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 处理业务逻辑。

View File

@@ -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 错误格式返回

View File

@@ -1,6 +1,8 @@
# Protocol Adapter - Anthropic
## ADDED Requirements
## Purpose
实现 Anthropic 协议的完整 ProtocolAdapter支持请求/响应编解码、流式转换和错误处理,遵循 Anthropic Messages API 规范。
### Requirement: 实现 Anthropic ProtocolAdapter

View File

@@ -1,6 +1,8 @@
# Protocol Adapter - OpenAI
## ADDED Requirements
## Purpose
实现 OpenAI 协议的完整 ProtocolAdapter支持请求/响应编解码、流式转换和错误处理,遵循 OpenAI Chat Completions API 规范。
### Requirement: 实现 OpenAI ProtocolAdapter

View File

@@ -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 处理业务逻辑。

View File

@@ -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验证边界情况

View File

@@ -1,6 +1,10 @@
# Request Validation
## ADDED Requirements
## Purpose
定义请求验证规范,覆盖 AI 协议请求OpenAI、Anthropic和管理 API 请求的验证规则,确保输入数据的合法性和安全性。
## Requirements
### Requirement: 使用 validator 库

View File

@@ -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 共享同一份筛选后的数据

View File

@@ -1,6 +1,10 @@
# Structured Logging
## ADDED Requirements
## Purpose
定义结构化日志规范,使用 zap 日志库提供 JSON 格式日志输出、日志文件滚动、请求 ID 追踪和多级别日志控制。
## Requirements
### Requirement: 使用 zap 结构化日志

View File

@@ -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、xlSHALL保持有效
- **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存在

View File

@@ -1,6 +1,10 @@
# Test Coverage
## ADDED Requirements
## Purpose
定义测试覆盖规范,建立后端和前端的单元测试、集成测试及 E2E 测试体系,确保核心业务逻辑的测试覆盖率达到目标水平。
## Requirements
### Requirement: 建立单元测试体系

View File

@@ -1,6 +1,10 @@
# Unified Proxy Handler
## ADDED Requirements
## Purpose
实现统一的代理处理器ProxyHandler替代独立的 OpenAI 和 Anthropic Handler通过 ConversionEngine 和 RoutingService 支持多协议的请求转换与转发。
## Requirements
### Requirement: 实现统一代理 Handler

View File

@@ -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 处理业务逻辑。