1
0

feat: 实现分层架构,包含 domain、service、repository 和 pkg 层

- 新增 domain 层:model、provider、route、stats 实体
- 新增 service 层:models、providers、routing、stats 业务逻辑
- 新增 repository 层:models、providers、stats 数据访问
- 新增 pkg 工具包:errors、logger、validator
- 新增中间件:CORS、logging、recovery、request ID
- 新增数据库迁移:初始 schema 和索引
- 新增单元测试和集成测试
- 新增规范文档:config-management、database-migration、error-handling、layered-architecture、middleware-system、request-validation、structured-logging、test-coverage
- 移除 config 子包和 model_router(已迁移至分层架构)
This commit is contained in:
2026-04-16 00:47:20 +08:00
parent 915b004924
commit f18904af1e
77 changed files with 5727 additions and 1257 deletions

View File

@@ -1,10 +1,6 @@
# Anthropic 协议代理
# Anthropic Protocol Proxy
## Purpose
TBD - 提供 Anthropic Messages API 的代理功能,通过协议转换实现与 OpenAI 兼容供应商的互操作
## Requirements
## MODIFIED Requirements
### Requirement: 支持 Anthropic Messages API 端点
@@ -26,6 +22,8 @@ TBD - 提供 Anthropic Messages API 的代理功能,通过协议转换实现
- **THEN** 网关 SHALL 将 OpenAI 流事件转换为 Anthropic 流事件
- **THEN** 网关 SHALL 使用 SSE 格式将转换后的事件流式返回给应用
**变更说明:** handler 通过 service 层调用,而非直接调用 config 和 provider 包。API 接口保持不变。
### Requirement: 将 Anthropic 请求转换为 OpenAI 格式
网关 SHALL 将 Anthropic Messages API 请求转换为 OpenAI Chat Completions API 格式。
@@ -41,138 +39,38 @@ TBD - 提供 Anthropic Messages API 的代理功能,通过协议转换实现
- **THEN** 网关 SHALL 在转换后的 OpenAI 请求中保留这些消息
- **THEN** 网关 SHALL 保留每条消息的 role 和 content
#### Scenario: Tools 转换
**变更说明:** 协议转换逻辑保持不变,仅调用方式改为通过 service 层。
- **WHEN** Anthropic 请求包含带有 `input_schema``tools`
- **THEN** 网关 SHALL 将每个工具转换为 OpenAI 格式,使用 `function.parameters` 替代 `input_schema`
- **THEN** 网关 SHALL 保留工具名称和描述
## ADDED Requirements
#### Scenario: Tool choice 转换
### Requirement: 使用 service 层处理请求
- **WHEN** Anthropic 请求包含 `type: "auto"``tool_choice`
- **THEN** 网关 SHALL 将其转换为 OpenAI 格式的 `"auto"`
- **WHEN** Anthropic 请求包含 `type: "any"``tool_choice`
- **THEN** 网关 SHALL 将其转换为 OpenAI 格式的 `"auto"`
- **WHEN** Anthropic 请求包含 `type: "tool"``name``tool_choice`
- **THEN** 网关 SHALL 将其转换为 OpenAI 格式的 `{"type": "function", "function": {"name": <name>}}`
Handler SHALL 通过 service 层处理业务逻辑。
#### Scenario: Tool result 转换
#### Scenario: 调用 routing service
- **WHEN** Anthropic 请求包含用户消息,其 `content` 数组包含 `type: "tool_result"`
- **THEN** 网关 SHALL 将每个工具结果转换为 `role: "tool"` 的消息
- **THEN** 网关 SHALL `tool_use_id` 设置 `tool_call_id`
- **THEN** 网关 SHALL 保留 content
- **WHEN** handler 收到请求并转换为 OpenAI 格式
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
- **THEN** SHALL 使用路由结果中的供应商信息
#### Scenario: Max tokens 处理
#### Scenario: 调用 stats service
- **WHEN** Anthropic 请求包含 `max_tokens`
- **THEN** 网关 SHALL 在 OpenAI 请求中包含它作为 `max_tokens`
- **WHEN** Anthropic 请求不包含 `max_tokens`
- **THEN** 网关 SHALL 设置默认值4096以满足 Anthropic 的要求
- **WHEN** 请求成功完成
- **THEN** SHALL 调用 StatsService.Record() 记录统计
- **THEN** SHALL 异步记录统计(不阻塞响应)
### Requirement: 将 OpenAI 响应转换为 Anthropic 格式
### Requirement: 使用结构化错误处理
网关 SHALL 将 OpenAI Chat Completions API 响应转换为 Anthropic Messages API 格式
Handler SHALL 使用结构化错误处理
#### Scenario: Content 转换
#### Scenario: 协议转换错误
- **WHEN** OpenAI 响应包含 `choices[0].message.content`
- **THEN** 网关 SHALL 将其转换为 Anthropic 格式的 `content: [{"type": "text", "text": <content>}]`
- **WHEN** 协议转换失败
- **THEN** SHALL 返回结构化错误响应
- **THEN** SHALL 包含详细的错误信息
#### Scenario: Tool calls 转换
#### Scenario: 路由错误处理
- **WHEN** OpenAI 响应包含 `choices[0].message.tool_calls`
- **THEN** 网关 SHALL 将每个工具调用转换为 `type: "tool_use"` 的内容块
- **THEN** 网关 SHALL `tool_calls[].id` 设置 `id`
- **THEN** 网关 SHALL 从 `tool_calls[].function.name` 设置 `name`
- **THEN** 网关 SHALL 解析 `arguments` JSON 字符串并将其设置为 `input` 对象
#### Scenario: Finish reason 转换
- **WHEN** OpenAI 响应的 `finish_reason``"stop"`
- **THEN** 网关 SHALL 在 Anthropic 响应中设置 `stop_reason: "end_turn"`
- **WHEN** OpenAI 响应的 `finish_reason``"tool_calls"`
- **THEN** 网关 SHALL 在 Anthropic 响应中设置 `stop_reason: "tool_use"`
#### Scenario: Usage 转换
- **WHEN** OpenAI 响应包含带有 `prompt_tokens``completion_tokens``usage`
- **THEN** 网关 SHALL 转换为 Anthropic 格式,使用 `input_tokens``output_tokens`
### Requirement: 转换流式事件
网关 SHALL 实时将 OpenAI 流事件转换为 Anthropic 流事件。
#### Scenario: Message start 事件
- **WHEN** 网关开始流式传输 Anthropic 响应
- **THEN** 网关 SHALL 发送带有消息元数据的 `message_start` 事件
#### Scenario: Content block start 事件
- **WHEN** OpenAI 流开始返回内容
- **THEN** 网关 SHALL 发送带有 `type: "text"``content_block_start` 事件
#### Scenario: Content delta 事件
- **WHEN** OpenAI 流发送带有内容的 delta
- **THEN** 网关 SHALL 发送带有 `type: "text_delta"``content_block_delta` 事件,包含文本
#### Scenario: Tool use 流式传输
- **WHEN** OpenAI 流发送工具调用 delta
- **THEN** 网关 SHALL 缓冲 `arguments`
- **THEN** 网关 SHALL 在工具调用开始时发送带有 `type: "tool_use"``content_block_start`
- **THEN** 网关 SHALL 发送带有部分 JSON 的 `input_delta` 事件
#### Scenario: Content block stop 事件
- **WHEN** 内容块完成
- **THEN** 网关 SHALL 发送 `content_block_stop` 事件
#### Scenario: Message stop 事件
- **WHEN** OpenAI 流完成
- **THEN** 网关 SHALL 发送 `message_stop` 事件
### Requirement: 支持 Anthropic 特有功能
网关 SHALL 支持映射到 OpenAI 能力的 Anthropic 特有功能。
#### Scenario: System prompt 作为独立字段
- **WHEN** Anthropic 请求包含 `system` 字段
- **THEN** 网关 SHALL 将其作为 OpenAI 格式的 system 消息处理
#### Scenario: 必需的 max_tokens
- **WHEN** 收到 Anthropic 请求
- **THEN** 网关 SHALL 确保 `max_tokens` 存在(如果未提供则使用默认值)
### Requirement: 处理纯文本内容
网关 SHALL 在 Anthropic 请求和响应中支持纯文本内容。
#### Scenario: 消息中的文本内容
- **WHEN** Anthropic 请求在消息中包含文本内容
- **THEN** 网关 SHALL 正确处理和转发文本内容
#### Scenario: 拒绝多模态内容
- **WHEN** Anthropic 请求包含多模态内容(图片、文档)
- **THEN** 网关 SHALL 返回错误,指示 MVP 不支持多模态内容
### Requirement: 保留请求元数据
网关 SHALL 在转换过程中保留请求元数据。
#### Scenario: 模型名称保留
- **WHEN** Anthropic 请求指定模型名称
- **THEN** 网关 SHALL 在转换后的 OpenAI 请求中保留模型名称
#### Scenario: 自定义参数
- **WHEN** Anthropic 请求包含自定义参数temperature, top_p 等)
- **THEN** 网关 SHALL 在转换后的请求中保留这些参数
- **WHEN** RoutingService 返回错误
- **THEN** SHALL 转换为对应的 AppError
- **THEN** SHALL 返回统一的错误响应

View File

@@ -0,0 +1,123 @@
# Config Management
## ADDED Requirements
### Requirement: 使用 YAML 配置文件
系统 SHALL 使用 YAML 格式的配置文件。
#### Scenario: 配置文件路径
- **WHEN** 应用启动
- **THEN** SHALL 从 `~/.nex/config.yaml` 加载配置
- **THEN** SHALL 解析 YAML 格式
#### Scenario: 配置文件结构
- **WHEN** 加载配置文件
- **THEN** SHALL 包含 server、database、log 等配置节
- **THEN** SHALL 支持嵌套配置结构
### Requirement: 自动生成默认配置
系统 SHALL 在首次使用时自动生成默认配置。
#### Scenario: 配置文件不存在
- **WHEN** 应用启动且 `~/.nex/config.yaml` 不存在
- **THEN** SHALL 自动创建配置文件
- **THEN** SHALL 写入默认配置值
- **THEN** SHALL 记录日志提示已创建
#### Scenario: 配置文件已存在
- **WHEN** 应用启动且 `~/.nex/config.yaml` 已存在
- **THEN** SHALL 直接加载配置文件
- **THEN** SHALL NOT 覆盖现有配置
### Requirement: 配置验证
系统 SHALL 验证配置的有效性。
#### Scenario: 必需字段验证
- **WHEN** 加载配置
- **THEN** SHALL 验证必需字段存在
- **THEN** SHALL 在字段缺失时返回错误
#### Scenario: 字段值验证
- **WHEN** 加载配置
- **THEN** SHALL 验证端口号范围1-65535
- **THEN** SHALL 验证日志级别有效性
- **THEN** SHALL 验证路径有效性
#### Scenario: 配置错误处理
- **WHEN** 配置验证失败
- **THEN** SHALL 返回详细的错误信息
- **THEN** SHALL 指示哪些字段无效
- **THEN** SHALL 应用 SHALL NOT 启动
### Requirement: 配置结构定义
系统 SHALL 定义清晰的配置结构。
#### Scenario: Server 配置
- **WHEN** 加载 server 配置
- **THEN** SHALL 包含 port、read_timeout、write_timeout 字段
- **THEN** SHALL 使用合理的默认值
#### Scenario: Database 配置
- **WHEN** 加载 database 配置
- **THEN** SHALL 包含 path、max_idle_conns、max_open_conns、conn_max_lifetime 字段
- **THEN** SHALL 使用合理的默认值
#### Scenario: Log 配置
- **WHEN** 加载 log 配置
- **THEN** SHALL 包含 level、path、max_size、max_backups、max_age、compress 字段
- **THEN** SHALL 使用合理的默认值
### Requirement: 默认配置值
系统 SHALL 提供合理的默认配置值。
#### Scenario: Server 默认值
- **WHEN** 使用默认配置
- **THEN** server.port SHALL 为 9826
- **THEN** server.read_timeout SHALL 为 30s
- **THEN** server.write_timeout SHALL 为 30s
#### Scenario: Database 默认值
- **WHEN** 使用默认配置
- **THEN** database.path SHALL 为 `~/.nex/config.db`
- **THEN** database.max_idle_conns SHALL 为 10
- **THEN** database.max_open_conns SHALL 为 100
- **THEN** database.conn_max_lifetime SHALL 为 1h
#### Scenario: Log 默认值
- **WHEN** 使用默认配置
- **THEN** log.level SHALL 为 info
- **THEN** log.path SHALL 为 `~/.nex/log`
- **THEN** log.max_size SHALL 为 100 (MB)
- **THEN** log.max_backups SHALL 为 10
- **THEN** log.max_age SHALL 为 30 (days)
- **THEN** log.compress SHALL 为 true
### Requirement: 配置重载支持
系统 SHALL 支持配置重载(未来扩展)。
#### Scenario: 配置热重载
- **WHEN** 配置文件修改(未来功能)
- **THEN** SHALL 支持重新加载配置
- **THEN** SHALL 应用新配置到可动态调整的参数
注:当前版本不支持,仅为未来扩展预留接口。

View File

@@ -0,0 +1,156 @@
# Database Migration
## ADDED Requirements
### Requirement: 使用 goose 迁移工具
系统 SHALL 使用 goose 管理数据库迁移。
#### Scenario: goose 安装
- **WHEN** 开发环境设置
- **THEN** SHALL 安装 goose CLI 工具
- **THEN** SHALL 支持通过 CLI 执行迁移
#### Scenario: 迁移文件格式
- **WHEN** 创建迁移文件
- **THEN** SHALL 使用 SQL 格式(.sql 文件)
- **THEN** SHALL 包含 -- +goose Up 和 -- +goose Down 注释
- **THEN** SHALL 支持事务性迁移
### Requirement: 创建初始迁移
系统 SHALL 创建初始 schema 迁移。
#### Scenario: 初始迁移文件
- **WHEN** 创建初始迁移
- **THEN** SHALL 创建 001_initial_schema.sql
- **THEN** SHALL 包含 providers、models、usage_stats 表的创建语句
- **THEN** SHALL 包含外键约束
#### Scenario: Up 迁移
- **WHEN** 执行 up 迁移
- **THEN** SHALL 创建所有表
- **THEN** SHALL 创建索引
- **THEN** SHALL 创建外键约束
#### Scenario: Down 迁移
- **WHEN** 执行 down 迁移
- **THEN** SHALL 删除所有表
- **THEN** SHALL 按正确顺序删除(避免外键约束错误)
### Requirement: 添加索引迁移
系统 SHALL 创建索引迁移。
#### Scenario: 索引迁移文件
- **WHEN** 创建索引迁移
- **THEN** SHALL 创建 002_add_indexes.sql
- **THEN** SHALL 为常用查询字段添加索引
#### Scenario: 索引定义
- **WHEN** 添加索引
- **THEN** SHALL 为 models(provider_id) 添加索引
- **THEN** SHALL 为 models(model_name) 添加索引
- **THEN** SHALL 为 usage_stats(provider_id, model_name, date) 添加复合索引
### Requirement: 迁移命令集成
迁移 SHALL 集成到 Makefile。
#### Scenario: 迁移 up 命令
- **WHEN** 执行 `make migrate-up`
- **THEN** SHALL 执行所有待执行的迁移
- **THEN** SHALL 显示迁移进度
#### Scenario: 迁移 down 命令
- **WHEN** 执行 `make migrate-down`
- **THEN** SHALL 回滚最后一个迁移
- **THEN** SHALL 显示回滚进度
#### Scenario: 迁移状态命令
- **WHEN** 执行 `make migrate-status`
- **THEN** SHALL 显示当前迁移状态
- **THEN** SHALL 显示已执行和待执行的迁移
#### Scenario: 创建迁移命令
- **WHEN** 执行 `make migrate-create name=<name>`
- **THEN** SHALL 创建新的迁移文件模板
- **THEN** SHALL 使用递增的版本号
### Requirement: 应用启动时迁移
应用 SHALL 在启动时执行迁移。
#### Scenario: 自动迁移
- **WHEN** 应用启动
- **THEN** SHALL 自动执行待执行的迁移
- **THEN** SHALL 在迁移失败时拒绝启动
- **THEN** SHALL 记录迁移日志
#### Scenario: 迁移版本检查
- **WHEN** 应用启动
- **THEN** SHALL 检查数据库迁移版本
- **THEN** SHALL 在版本不匹配时执行迁移
### Requirement: 连接池配置
系统 SHALL 配置数据库连接池。
#### Scenario: 连接池参数
- **WHEN** 初始化数据库连接
- **THEN** SHALL 设置 MaxIdleConns默认 10
- **THEN** SHALL 设置 MaxOpenConns默认 100
- **THEN** SHALL 设置 ConnMaxLifetime默认 1h
#### Scenario: 连接池监控
- **WHEN** 应用运行
- **THEN** SHALL 定期记录连接池状态(可选)
- **THEN** SHALL 监控连接池使用情况
### Requirement: 迁移回滚支持
系统 SHALL 支持迁移回滚。
#### Scenario: 回滚到指定版本
- **WHEN** 执行 `goose down-to <version>`
- **THEN** SHALL 回滚到指定版本
- **THEN** SHALL 按顺序执行 down 迁移
#### Scenario: 完全回滚
- **WHEN** 执行 `goose reset`
- **THEN** SHALL 回滚所有迁移
- **THEN** SHALL 清空数据库
### Requirement: 迁移文件管理
迁移文件 SHALL 版本化管理。
#### Scenario: 迁移文件命名
- **WHEN** 创建迁移文件
- **THEN** SHALL 使用格式 `<version>_<name>.sql`
- **THEN** SHALL 版本号递增
- **THEN** SHALL 名称使用 snake_case
#### Scenario: 迁移文件存储
- **WHEN** 创建迁移文件
- **THEN** SHALL 存储在 migrations/ 目录
- **THEN** SHALL 提交到版本控制系统

View File

@@ -0,0 +1,122 @@
# Error Handling
## ADDED Requirements
### Requirement: 定义结构化错误类型
系统 SHALL 定义 AppError 结构体。
#### Scenario: AppError 结构
- **WHEN** 定义错误
- **THEN** SHALL 包含 Code错误码、Message错误消息、HTTPStatusHTTP 状态码)字段
- **THEN** SHALL 可选包含 Cause原始错误、Context上下文信息字段
#### Scenario: 错误码定义
- **WHEN** 定义错误码
- **THEN** SHALL 使用 kebab-case 格式(如 model_not_found
- **THEN** SHALL 定义清晰的错误码语义
- **THEN** SHALL 为常见错误预定义错误码
### Requirement: 预定义常见错误
系统 SHALL 预定义常见错误。
#### Scenario: 资源不存在错误
- **WHEN** 资源不存在
- **THEN** SHALL 使用 ErrModelNotFound、ErrProviderNotFound 等预定义错误
- **THEN** SHALL 设置 HTTP 状态码为 404
#### Scenario: 验证错误
- **WHEN** 请求验证失败
- **THEN** SHALL 使用 ErrInvalidRequest 等预定义错误
- **THEN** SHALL 设置 HTTP 状态码为 400
#### Scenario: 内部错误
- **WHEN** 发生内部错误
- **THEN** SHALL 使用 ErrInternal 等预定义错误
- **THEN** SHALL 设置 HTTP 状态码为 500
### Requirement: 支持错误包装
系统 SHALL 支持错误包装。
#### Scenario: 包装原始错误
- **WHEN** 发生错误
- **THEN** SHALL 能够包装原始错误(设置 Cause 字段)
- **THEN** SHALL 能够添加上下文信息(设置 Context 字段)
- **THEN** SHALL 保留错误链
#### Scenario: 错误链追踪
- **WHEN** 记录错误日志
- **THEN** SHALL 记录完整的错误链
- **THEN** SHALL 包含每一层错误的信息
### Requirement: 统一错误响应
系统 SHALL 统一错误响应格式。
#### Scenario: OpenAI 协议错误响应
- **WHEN** OpenAI 协议发生错误
- **THEN** SHALL 返回标准 OpenAI 错误响应格式
- **THEN** SHALL 包含 error.message、error.type、error.code 字段
#### Scenario: Anthropic 协议错误响应
- **WHEN** Anthropic 协议发生错误
- **THEN** SHALL 返回标准 Anthropic 错误响应格式
- **THEN** SHALL 包含 type、error.type、error.message 字段
#### Scenario: 管理 API 错误响应
- **WHEN** 管理 API 发生错误
- **THEN** SHALL 返回统一的错误响应格式
- **THEN** SHALL 包含 code、message 字段
- **THEN** SHALL 可选包含 details 字段(验证错误详情)
### Requirement: 错误处理中间件
系统 SHALL 提供错误处理中间件。
#### Scenario: 捕获 panic
- **WHEN** handler 发生 panic
- **THEN** 中间件 SHALL 捕获 panic
- **THEN** SHALL 记录堆栈信息
- **THEN** SHALL 返回 500 错误响应
#### Scenario: 统一错误响应
- **WHEN** handler 返回 AppError
- **THEN** 中间件 SHALL 转换为对应的 HTTP 响应
- **THEN** SHALL 设置正确的 HTTP 状态码
- **THEN** SHALL 设置正确的响应体
### Requirement: 替换现有错误处理
系统 SHALL 替换所有 errors.New 为结构化错误。
#### Scenario: config 包错误
- **WHEN** config 包发生错误
- **THEN** SHALL 使用结构化错误
- **THEN** SHALL 设置适当的错误码和 HTTP 状态码
#### Scenario: service 层错误
- **WHEN** service 层发生错误
- **THEN** SHALL 使用结构化错误
- **THEN** SHALL 包含业务上下文信息
#### Scenario: repository 层错误
- **WHEN** repository 层发生错误
- **THEN** SHALL 包装数据库错误
- **THEN** SHALL 转换为应用错误

View File

@@ -0,0 +1,115 @@
# Layered Architecture
## ADDED Requirements
### Requirement: 实现三层架构
系统 SHALL 实现 handler → service → repository 三层架构。
#### Scenario: Handler 层职责
- **WHEN** 处理 HTTP 请求
- **THEN** handler 层 SHALL 仅负责 HTTP 请求解析和响应
- **THEN** handler 层 SHALL 调用 service 层处理业务逻辑
- **THEN** handler 层 SHALL NOT 直接访问数据库
#### Scenario: Service 层职责
- **WHEN** 处理业务逻辑
- **THEN** service 层 SHALL 包含业务规则和验证
- **THEN** service 层 SHALL 调用 repository 层访问数据
- **THEN** service 层 SHALL 协调多个 repository 的操作
#### Scenario: Repository 层职责
- **WHEN** 访问数据
- **THEN** repository 层 SHALL 仅负责数据访问
- **THEN** repository 层 SHALL 封装数据库操作
- **THEN** repository 层 SHALL NOT 包含业务逻辑
### Requirement: 定义核心接口
系统 SHALL 定义清晰的接口边界。
#### Scenario: Service 接口定义
- **WHEN** 定义 service 接口
- **THEN** SHALL 定义 ProviderService、ModelService、RoutingService、StatsService 接口
- **THEN** SHALL 定义清晰的业务方法签名
- **THEN** SHALL 使用 domain 类型作为参数和返回值
#### Scenario: Repository 接口定义
- **WHEN** 定义 repository 接口
- **THEN** SHALL 定义 ProviderRepository、ModelRepository、StatsRepository 接口
- **THEN** SHALL 定义清晰的数据访问方法签名
- **THEN** SHALL 使用 domain 类型作为参数和返回值
#### Scenario: Provider Client 接口定义
- **WHEN** 定义 provider client 接口
- **THEN** SHALL 定义 ProviderClient 接口
- **THEN** SHALL 包含 SendRequest 和 SendStreamRequest 方法
- **THEN** SHALL 支持接口 Mock
### Requirement: 实现依赖注入
系统 SHALL 使用手动依赖注入。
#### Scenario: Repository 注入
- **WHEN** 初始化 service
- **THEN** SHALL 通过构造函数注入 repository 依赖
- **THEN** SHALL 使用接口类型而非具体类型
#### Scenario: Service 注入
- **WHEN** 初始化 handler
- **THEN** SHALL 通过构造函数注入 service 依赖
- **THEN** SHALL 使用接口类型而非具体类型
#### Scenario: 主函数组装
- **WHEN** 应用启动
- **THEN** main.go SHALL 按顺序构造所有依赖
- **THEN** SHALL 先构造基础设施logger、database
- **THEN** SHALL 再构造 repository、service、handler
### Requirement: 定义 Domain 模型
系统 SHALL 定义独立的 domain 模型。
#### Scenario: Domain 模型定义
- **WHEN** 定义领域模型
- **THEN** SHALL 在 internal/domain/ 包中定义
- **THEN** SHALL 包含 Provider、Model、UsageStats 等模型
- **THEN** SHALL 与数据库模型分离
#### Scenario: Domain 模型使用
- **WHEN** service 和 repository 处理数据
- **THEN** SHALL 使用 domain 模型
- **THEN** SHALL NOT 使用数据库模型GORM 模型)
### Requirement: 提高可测试性
架构 SHALL 提高代码可测试性。
#### Scenario: Service 层测试
- **WHEN** 测试 service 层
- **THEN** SHALL 能够 Mock repository 依赖
- **THEN** SHALL 能够独立测试业务逻辑
#### Scenario: Handler 层测试
- **WHEN** 测试 handler 层
- **THEN** SHALL 能够 Mock service 依赖
- **THEN** SHALL 能够独立测试 HTTP 处理逻辑
#### Scenario: Repository 层测试
- **WHEN** 测试 repository 层
- **THEN** SHALL 使用测试数据库
- **THEN** SHALL 能够独立测试数据访问逻辑

View File

@@ -0,0 +1,136 @@
# Middleware System
## ADDED Requirements
### Requirement: 实现请求 ID 中间件
系统 SHALL 实现请求 ID 中间件。
#### Scenario: 生成请求 ID
- **WHEN** 收到 HTTP 请求且 header 中无 X-Request-ID
- **THEN** SHALL 生成新的 UUID 作为请求 ID
- **THEN** SHALL 设置到响应 header 的 X-Request-ID
- **THEN** SHALL 设置到 gin.Context 中
#### Scenario: 复用请求 ID
- **WHEN** 收到 HTTP 请求且 header 中已有 X-Request-ID
- **THEN** SHALL 复用该请求 ID
- **THEN** SHALL 设置到响应 header
- **THEN** SHALL 设置到 gin.Context 中
### Requirement: 实现日志中间件
系统 SHALL 实现日志中间件。
#### Scenario: 记录请求开始
- **WHEN** 收到 HTTP 请求
- **THEN** SHALL 记录请求开始日志
- **THEN** SHALL 包含请求方法、路径、客户端 IP、请求 ID
#### Scenario: 记录请求结束
- **WHEN** HTTP 请求处理完成
- **THEN** SHALL 记录请求结束日志
- **THEN** SHALL 包含响应状态码、响应大小、请求耗时、请求 ID
#### Scenario: 记录错误
- **WHEN** 请求处理过程中发生错误
- **THEN** SHALL 记录错误日志
- **THEN** SHALL 包含错误详情和请求 ID
### Requirement: 实现错误恢复中间件
系统 SHALL 实现错误恢复中间件。
#### Scenario: 捕获 panic
- **WHEN** handler 发生 panic
- **THEN** SHALL 捕获 panic
- **THEN** SHALL 记录堆栈信息
- **THEN** SHALL 返回 500 错误响应
#### Scenario: 记录堆栈
- **WHEN** 发生 panic
- **THEN** SHALL 记录完整的堆栈信息
- **THEN** SHALL 包含 panic 原因和请求 ID
#### Scenario: 防止服务崩溃
- **WHEN** handler panic
- **THEN** SHALL 恢复并继续处理其他请求
- **THEN** SHALL NOT 导致服务崩溃
### Requirement: 实现 CORS 中间件
系统 SHALL 实现 CORS 中间件。
#### Scenario: 允许所有来源
- **WHEN** 收到 CORS 预检请求
- **THEN** SHALL 设置 Access-Control-Allow-Origin 为 *
- **THEN** SHALL 设置 Access-Control-Allow-Methods
- **THEN** SHALL 设置 Access-Control-Allow-Headers
#### Scenario: 处理预检请求
- **WHEN** 收到 OPTIONS 请求
- **THEN** SHALL 返回 204 状态码
- **THEN** SHALL 设置 CORS headers
注:当前配置允许所有来源,适合个人使用。
### Requirement: 中间件注册顺序
系统 SHALL 按正确顺序注册中间件。
#### Scenario: 全局中间件顺序
- **WHEN** 注册全局中间件
- **THEN** SHALL 按以下顺序注册:
1. RequestID生成请求 ID
2. Recovery错误恢复
3. Logging日志记录
4. CORS跨域处理
#### Scenario: 中间件执行顺序
- **WHEN** 处理请求
- **THEN** SHALL 按注册顺序执行中间件
- **THEN** SHALL 确保请求 ID 在其他中间件之前生成
### Requirement: 中间件配置
中间件 SHALL 支持配置。
#### Scenario: 日志中间件配置
- **WHEN** 初始化日志中间件
- **THEN** SHALL 注入 logger 实例
- **THEN** SHALL 使用配置的日志级别
#### Scenario: Recovery 中间件配置
- **WHEN** 初始化 recovery 中间件
- **THEN** SHALL 注入 logger 实例
- **THEN** SHALL 配置堆栈打印深度
### Requirement: 中间件上下文传递
中间件 SHALL 支持上下文传递。
#### Scenario: 请求 ID 传递
- **WHEN** 中间件设置请求 ID
- **THEN** SHALL 通过 gin.Context 传递
- **THEN** SHALL 在后续中间件和 handler 中可访问
#### Scenario: 日志上下文传递
- **WHEN** 日志中间件记录日志
- **THEN** SHALL 包含请求 ID
- **THEN** SHALL 支持添加其他上下文信息

View File

@@ -1,10 +1,6 @@
# 模型管理
# Model Management
## Purpose
TBD - 提供模型配置的管理功能,模型关联到供应商
## Requirements
## MODIFIED Requirements
### Requirement: 创建模型配置
@@ -23,16 +19,7 @@ TBD - 提供模型配置的管理功能,模型关联到供应商
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
- **THEN** 错误 SHALL 指示供应商不存在
#### Scenario: 使用重复 ID 创建模型
- **WHEN** 向 `/api/models` 发送 POST 请求,携带已存在的 ID
- **THEN** 网关 SHALL 返回错误,状态码为 409 (Conflict)
#### Scenario: 创建模型时缺少必需字段
- **WHEN** 向 `/api/models` 发送 POST 请求缺少必需字段id, provider_id 或 model_name
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
- **THEN** 错误 SHALL 指示缺少哪些字段
**变更说明:** handler 通过 ModelService 调用,数据访问通过 ModelRepository 和 ProviderRepository。API 接口保持不变。
### Requirement: 列出所有模型
@@ -44,10 +31,7 @@ TBD - 提供模型配置的管理功能,模型关联到供应商
- **THEN** 网关 SHALL 返回所有模型的列表
- **THEN** 每个模型 SHALL 包含 id, provider_id, model_name, enabled, created_at
#### Scenario: 列出模型时为空
- **WHEN** 向 `/api/models` 发送 GET 请求,且不存在模型
- **THEN** 网关 SHALL 返回空列表
**变更说明:** 数据访问从 config 包迁移到 ModelRepository。API 接口保持不变。
### Requirement: 按供应商列出模型
@@ -58,24 +42,7 @@ TBD - 提供模型配置的管理功能,模型关联到供应商
- **WHEN** 向 `/api/models?provider_id=<provider_id>` 发送 GET 请求
- **THEN** 网关 SHALL 返回指定供应商的模型列表
#### Scenario: 列出不存在供应商的模型
- **WHEN** 向 `/api/models?provider_id=<non_existent_id>` 发送 GET 请求
- **THEN** 网关 SHALL 返回空列表
### Requirement: 获取特定模型
网关 SHALL 允许通过 ID 获取特定模型。
#### Scenario: 获取存在的模型
- **WHEN** 向 `/api/models/:id` 发送 GET 请求,携带有效的模型 ID
- **THEN** 网关 SHALL 返回模型详情
#### Scenario: 获取不存在的模型
- **WHEN** 向 `/api/models/:id` 发送 GET 请求,携带不存在的 ID
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
**变更说明:** 通过 ModelService 和 ModelRepository 实现。API 接口保持不变。
### Requirement: 更新模型配置
@@ -87,22 +54,13 @@ TBD - 提供模型配置的管理功能,模型关联到供应商
- **THEN** 网关 SHALL 更新数据库中的模型记录
- **THEN** 网关 SHALL 返回更新后的模型
#### Scenario: 更新不存在的模型
- **WHEN** 向 `/api/models/:id` 发送 PUT 请求,携带不存在的 ID
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
#### Scenario: 更新模型供应商
- **WHEN** 向 `/api/models/:id` 发送 PUT 请求,携带新的 provider_id
- **THEN** 网关 SHALL 验证新供应商是否存在
- **THEN** 网关 SHALL 更新模型的供应商关联
#### Scenario: 部分更新
- **WHEN** 向 `/api/models/:id` 发送 PUT 请求,仅包含部分字段
- **THEN** 网关 SHALL 仅更新提供的字段
- **THEN** 网关 SHALL 保留未更改的字段
**变更说明:** 通过 ModelService、ModelRepository 和 ProviderRepository 实现。API 接口保持不变。
### Requirement: 删除模型配置
@@ -114,61 +72,38 @@ TBD - 提供模型配置的管理功能,模型关联到供应商
- **THEN** 网关 SHALL 删除模型记录
- **THEN** 网关 SHALL 返回状态码 204 (No Content)
#### Scenario: 删除不存在的模型
**变更说明:** 通过 ModelService 和 ModelRepository 实现。API 接口保持不变。
- **WHEN** 向 `/api/models/:id` 发送 DELETE 请求,携带不存在的 ID
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
## ADDED Requirements
### Requirement: 启用和禁用模型
### Requirement: 使用 service 层处理业务逻辑
网关 SHALL 支持启用和禁用模型
Handler SHALL 通过 ModelService 处理业务逻辑
#### Scenario: 禁用模型
#### Scenario: 调用 service 方法
- **WHEN** 模型的 `enabled` 字段设置为 false
- **THEN** 网关 SHALL 不向该模型路由请求
- **THEN** 模型 SHALL 保留在数据库中
- **WHEN** handler 收到请求
- **THEN** SHALL 调用对应的 ModelService 方法Create、Get、List、Update、Delete
- **THEN** SHALL 使用 domain.Model 类型
#### Scenario: 启用模型
- **WHEN** 已禁用模型的 `enabled` 字段设置为 true
- **THEN** 网关 SHALL 恢复向该模型路由请求
### Requirement: 验证模型配置
网关 SHALL 验证模型配置数据。
#### Scenario: 验证供应商存在
- **WHEN** 创建或更新模型时携带 provider_id
- **THEN** 网关 SHALL 验证供应商存在于数据库中
#### Scenario: 验证必需字段
#### Scenario: 供应商验证
- **WHEN** 创建或更新模型
- **THEN** 网关 SHALL 验证 id, provider_id 和 model_name 存在且非空
- **THEN** SHALL 在 service 层验证供应商存在
- **THEN** SHALL 通过 ProviderRepository 查询供应商
### Requirement: 支持透明的模型名称
### Requirement: 使用 repository 层访问数据
网关 SHALL 使用模型名称透明传输,不做转换
Service SHALL 通过 ModelRepository 访问数据
#### Scenario: 模型名称保留
#### Scenario: 调用 repository 方法
- **WHEN** 模型配置了 model_name
- **THEN** 网关 SHALL 在路由请求时使用该确切名称
- **THEN** 网关 SHALL 不修改或转换模型名称
- **WHEN** service 处理业务逻辑
- **THEN** SHALL 调用对应的 ModelRepository 方法
- **THEN** SHALL 使用 domain.Model 类型
#### Scenario: 不同供应商的同名模型
#### Scenario: 数据验证
- **WHEN** 多个供应商拥有相同 model_name 的模型
- **THEN** 每个模型 SHALL 通过其唯一 ID 和 provider_id 区分
- **THEN** 网关 SHALL 基于模型名称和供应商关联的组合进行路由
### Requirement: 随供应商级联删除
网关 SHALL 在删除关联供应商时删除模型。
#### Scenario: 供应商删除级联到模型
- **WHEN** 供应商被删除
- **THEN** 该供应商关联的所有模型 SHALL 自动删除
- **WHEN** 创建或更新模型
- **THEN** SHALL 在 service 层验证业务规则
- **THEN** SHALL 在 repository 层执行数据库操作

View File

@@ -1,10 +1,6 @@
# OpenAI 协议代理
# OpenAI Protocol Proxy
## Purpose
TBD - 提供 OpenAI Chat Completions API 的代理功能
## Requirements
## MODIFIED Requirements
### Requirement: 支持 OpenAI Chat Completions API 端点
@@ -23,27 +19,7 @@ TBD - 提供 OpenAI Chat Completions API 的代理功能
- **THEN** 网关 SHALL 使用 SSE 格式将响应流式返回给应用
- **THEN** 网关 SHALL 在流完成时发送 `data: [DONE]`
### Requirement: 支持 Function Calling
网关 SHALL 在非流式和流式模式下都支持 OpenAI Function Calling。
#### Scenario: 非流式函数调用
- **WHEN** 应用发送包含 `tools` 定义的请求
- **AND** 供应商返回包含 `tool_calls` 的响应
- **THEN** 网关 SHALL 在响应中原样转发 `tool_calls`
#### Scenario: 流式函数调用
- **WHEN** 应用发送包含 `tools` 定义的流式请求
- **AND** 供应商在 delta 块中流式返回 `tool_calls`
- **THEN** 网关 SHALL 将 `tool_calls` 块流式发送给应用
- **THEN** 网关 SHALL 在完成时设置 `finish_reason: "tool_calls"`
#### Scenario: 工具结果提交
- **WHEN** 应用发送包含 `role: "tool"` 消息的后续请求,携带函数结果
- **THEN** 网关 SHALL 将工具结果原样转发给供应商
**变更说明:** handler 通过 service 层调用,而非直接调用 config 和 provider 包。API 接口保持不变。
### Requirement: 根据模型名称路由请求
@@ -65,6 +41,8 @@ TBD - 提供 OpenAI Chat Completions API 的代理功能
- **WHEN** 请求包含已禁用模型的 `model` 字段
- **THEN** 网关 SHALL 返回错误响应,指示模型不可用
**变更说明:** 路由逻辑从 router 包迁移到 RoutingService通过 service 层调用。API 接口保持不变。
### Requirement: 对 OpenAI 兼容供应商透明代理
网关 SHALL 对 OpenAI 兼容供应商的请求和响应进行透明转发,不做修改。
@@ -83,47 +61,38 @@ TBD - 提供 OpenAI Chat Completions API 的代理功能
- **THEN** 网关 SHALL 将响应体原样返回给应用
- **THEN** 网关 SHALL 保留所有响应头和状态码
### Requirement: 处理供应商错误
**变更说明:** provider client 通过接口注入到 handler便于测试和替换实现。API 接口保持不变。
网关 SHALL 将供应商错误透明返回给应用。
## ADDED Requirements
#### Scenario: 供应商返回错误
### Requirement: 使用 service 层处理请求
- **WHEN** 供应商返回错误响应4xx 或 5xx
- **THEN** 网关 SHALL 将相同的错误响应返回给应用
- **THEN** 网关 SHALL 保留错误消息和状态码
Handler SHALL 通过 service 层处理业务逻辑。
#### Scenario: 供应商超时
#### Scenario: 调用 routing service
- **WHEN** 供应商在超时时间内未响应
- **THEN** 网关 SHALL 向应用返回超时错误
- **WHEN** handler 收到请求
- **THEN** SHALL 调用 RoutingService.Route() 获取路由结果
- **THEN** SHALL 使用路由结果中的供应商信息
#### Scenario: 供应商连接失败
#### Scenario: 调用 stats service
- **WHEN** 网关无法连接到供应商
- **THEN** 网关 SHALL 向应用返回连接错误
- **WHEN** 请求成功完成
- **THEN** SHALL 调用 StatsService.Record() 记录统计
- **THEN** SHALL 异步记录统计(不阻塞响应)
### Requirement: 支持标准 OpenAI 请求字段
### Requirement: 使用结构化错误处理
网关 SHALL 支持所有标准 OpenAI Chat Completions API 请求字段
Handler SHALL 使用结构化错误处理
#### Scenario: 支持标准字段
#### Scenario: 路由错误处理
- **WHEN** 请求包含标准字段model, messages, temperature, max_tokens, top_p, frequency_penalty, presence_penalty, stop, n, stream, tools, tool_choice, user
- **THEN** 网关 SHALL 接受并将所有字段转发给供应商
- **WHEN** RoutingService 返回错误
- **THEN** SHALL 转换为对应的 AppError
- **THEN** SHALL 返回统一的错误响应
### Requirement: 维护流式连接稳定性
#### Scenario: 供应商错误处理
网关 SHALL 维护稳定的流式连接并优雅处理中断。
#### Scenario: 流中断
- **WHEN** 供应商流在传输过程中中断
- **THEN** 网关 SHALL 优雅关闭客户端连接
- **THEN** 网关 SHALL 记录中断日志以便调试
#### Scenario: 客户端提前断开
- **WHEN** 客户端在流完成前断开连接
- **THEN** 网关 SHALL 取消供应商请求
- **THEN** 网关 SHALL 释放相关资源
- **WHEN** ProviderClient 返回错误
- **THEN** SHALL 包装为 AppError
- **THEN** SHALL 包含请求上下文信息

View File

@@ -1,10 +1,6 @@
# 供应商管理
# Provider Management
## Purpose
TBD - 提供供应商配置的管理功能(创建、查询、更新、删除)
## Requirements
## MODIFIED Requirements
### Requirement: 创建供应商配置
@@ -28,6 +24,8 @@ TBD - 提供供应商配置的管理功能(创建、查询、更新、删除
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
- **THEN** 错误 SHALL 指示缺少哪些字段
**变更说明:** handler 通过 ProviderService 调用,数据访问通过 ProviderRepository。API 接口保持不变。
### Requirement: 列出所有供应商
网关 SHALL 允许获取所有供应商配置。
@@ -39,10 +37,7 @@ TBD - 提供供应商配置的管理功能(创建、查询、更新、删除
- **THEN** 每个供应商 SHALL 包含 id, name, api_key已掩码, base_url, enabled, created_at, updated_at
- **THEN** api_key SHALL 被掩码(仅显示最后 4 个字符)
#### Scenario: 列出供应商时为空
- **WHEN** 向 `/api/providers` 发送 GET 请求,且不存在供应商
- **THEN** 网关 SHALL 返回空列表
**变更说明:** 数据访问从 config 包迁移到 ProviderRepository。API 接口保持不变。
### Requirement: 获取特定供应商
@@ -59,6 +54,8 @@ TBD - 提供供应商配置的管理功能(创建、查询、更新、删除
- **WHEN** 向 `/api/providers/:id` 发送 GET 请求,携带不存在的 ID
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。
### Requirement: 更新供应商配置
网关 SHALL 允许更新现有供应商配置。
@@ -70,16 +67,7 @@ TBD - 提供供应商配置的管理功能(创建、查询、更新、删除
- **THEN** 网关 SHALL 返回更新后的供应商
- **THEN** updated_at 时间戳 SHALL 被更新
#### Scenario: 更新不存在的供应商
- **WHEN** 向 `/api/providers/:id` 发送 PUT 请求,携带不存在的 ID
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
#### Scenario: 部分更新
- **WHEN** 向 `/api/providers/:id` 发送 PUT 请求,仅包含部分字段
- **THEN** 网关 SHALL 仅更新提供的字段
- **THEN** 网关 SHALL 保留未更改的字段
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。
### Requirement: 删除供应商配置
@@ -92,60 +80,38 @@ TBD - 提供供应商配置的管理功能(创建、查询、更新、删除
- **THEN** 网关 SHALL 删除所有关联的模型CASCADE
- **THEN** 网关 SHALL 返回状态码 204 (No Content)
#### Scenario: 删除不存在的供应商
**变更说明:** 通过 ProviderService 和 ProviderRepository 实现。API 接口保持不变。
- **WHEN** 向 `/api/providers/:id` 发送 DELETE 请求,携带不存在的 ID
- **THEN** 网关 SHALL 返回错误,状态码为 404 (Not Found)
## ADDED Requirements
### Requirement: 启用和禁用供应商
### Requirement: 使用 service 层处理业务逻辑
网关 SHALL 支持启用和禁用供应商
Handler SHALL 通过 ProviderService 处理业务逻辑
#### Scenario: 禁用供应商
#### Scenario: 调用 service 方法
- **WHEN** 供应商的 `enabled` 字段设置为 false
- **THEN** 网关 SHALL 不向该供应商路由请求
- **THEN** 供应商 SHALL 保留在数据库中
- **WHEN** handler 收到请求
- **THEN** SHALL 调用对应的 ProviderService 方法Create、Get、List、Update、Delete
- **THEN** SHALL 使用 domain.Provider 类型
#### Scenario: 启用供应商
#### Scenario: 错误处理
- **WHEN** 已禁用供应商的 `enabled` 字段设置为 true
- **THEN** 网关 SHALL 恢复向该供应商路由请求
- **WHEN** service 返回错误
- **THEN** SHALL 转换为 HTTP 错误响应
- **THEN** SHALL 使用结构化错误处理
### Requirement: 验证供应商配置
### Requirement: 使用 repository 层访问数据
网关 SHALL 验证供应商配置数据。
Service SHALL 通过 ProviderRepository 访问数据。
#### Scenario: 验证 base_url 格式
#### Scenario: 调用 repository 方法
- **WHEN** 创建或更新供应商时使用无效的 base_url 格式
- **THEN** 网关 SHALL 返回错误,状态码为 400 (Bad Request)
- **WHEN** service 处理业务逻辑
- **THEN** SHALL 调用对应的 ProviderRepository 方法
- **THEN** SHALL 使用 domain.Provider 类型
#### Scenario: 验证必需字段
#### Scenario: 数据验证
- **WHEN** 创建或更新供应商
- **THEN** 网关 SHALL 验证 id, name, api_key 和 base_url 存在且非空
### Requirement: 安全存储供应商配置
网关 SHALL 安全存储供应商 API Key。
#### Scenario: 存储 API Key
- **WHEN** 创建或更新供应商时携带 API Key
- **THEN** 网关 SHALL 将 API Key 存储在数据库中
#### Scenario: 在响应中掩码 API Key
- **WHEN** 在 API 响应中返回供应商数据
- **THEN** API Key SHALL 被掩码(仅显示最后 4 个字符)
### Requirement: 仅支持 OpenAI 兼容供应商
网关 SHALL 在 MVP 中仅支持 OpenAI 兼容供应商。
#### Scenario: 供应商类型验证
- **WHEN** 创建供应商
- **THEN** 供应商类型 SHALL 隐式设置为 "openai-compatible"
- **THEN** MVP 中 SHALL 不支持其他供应商类型
- **THEN** SHALL 在 service 层验证业务规则
- **THEN** SHALL 在 repository 层执行数据库操作

View File

@@ -0,0 +1,132 @@
# Request Validation
## ADDED Requirements
### Requirement: 使用 validator 库
系统 SHALL 使用 go-playground/validator 进行请求验证。
#### Scenario: 验证器初始化
- **WHEN** 应用启动
- **THEN** SHALL 初始化 validator 实例
- **THEN** SHALL 注册自定义验证规则
#### Scenario: 验证规则定义
- **WHEN** 定义请求结构体
- **THEN** SHALL 使用 struct tag 定义验证规则
- **THEN** SHALL 支持必需字段、范围、格式等验证
### Requirement: 验证 OpenAI 请求
系统 SHALL 验证 OpenAI ChatCompletionRequest。
#### Scenario: 必需字段验证
- **WHEN** 收到 OpenAI 请求
- **THEN** SHALL 验证 model 字段不为空
- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息
#### Scenario: 参数范围验证
- **WHEN** 收到 OpenAI 请求
- **THEN** SHALL 验证 temperature 范围在 [0, 2]
- **THEN** SHALL 验证 max_tokens 大于 0
- **THEN** SHALL 验证 top_p 范围在 (0, 1]
- **THEN** SHALL 验证 frequency_penalty 范围在 [-2, 2]
- **THEN** SHALL 验证 presence_penalty 范围在 [-2, 2]
#### Scenario: 消息内容验证
- **WHEN** 验证 messages 字段
- **THEN** SHALL 验证每条消息的 role 有效system、user、assistant、tool
- **THEN** SHALL 验证 content 不为空
### Requirement: 验证 Anthropic 请求
系统 SHALL 验证 Anthropic MessagesRequest。
#### Scenario: 必需字段验证
- **WHEN** 收到 Anthropic 请求
- **THEN** SHALL 验证 model 字段不为空
- **THEN** SHALL 验证 messages 字段不为空且至少有一条消息
- **THEN** SHALL 验证 max_tokens 大于 0或使用默认值
#### Scenario: 参数范围验证
- **WHEN** 收到 Anthropic 请求
- **THEN** SHALL 验证 temperature 范围在 [0, 1]
- **THEN** SHALL 验证 top_p 范围在 (0, 1]
#### Scenario: 消息内容验证
- **WHEN** 验证 messages 字段
- **THEN** SHALL 验证每条消息的 role 有效user、assistant
- **THEN** SHALL 验证 content 数组不为空
### Requirement: 验证管理 API 请求
系统 SHALL 验证管理 API 请求。
#### Scenario: Provider 创建验证
- **WHEN** 创建 Provider
- **THEN** SHALL 验证 id、name、api_key、base_url 字段不为空
- **THEN** SHALL 验证 base_url 格式有效URL 格式)
- **THEN** SHALL 验证 id 格式(字母、数字、下划线、连字符)
#### Scenario: Model 创建验证
- **WHEN** 创建 Model
- **THEN** SHALL 验证 id、provider_id、model_name 字段不为空
- **THEN** SHALL 验证 provider_id 存在
#### Scenario: 更新请求验证
- **WHEN** 更新资源
- **THEN** SHALL 验证至少提供一个可更新字段
- **THEN** SHALL 验证字段值有效性
### Requirement: 返回友好的验证错误
系统 SHALL 返回友好的验证错误响应。
#### Scenario: 错误消息格式
- **WHEN** 验证失败
- **THEN** SHALL 返回 400 状态码
- **THEN** SHALL 返回详细的错误消息
- **THEN** SHALL 指示哪些字段验证失败
#### Scenario: 多字段错误
- **WHEN** 多个字段验证失败
- **THEN** SHALL 返回所有验证错误
- **THEN** SHALL 使用结构化格式(字段名 → 错误消息)
#### Scenario: 国际化支持
- **WHEN** 返回验证错误(未来)
- **THEN** SHALL 支持错误消息国际化
- **THEN** SHALL 使用错误码作为国际化 key
注:当前版本使用中文错误消息。
### Requirement: 在 handler 中应用验证
系统 SHALL 在 handler 中应用验证。
#### Scenario: 验证中间件
- **WHEN** 使用验证中间件
- **THEN** SHALL 在请求解析后立即验证
- **THEN** SHALL 在验证失败时提前返回错误
- **THEN** SHALL 避免执行后续处理逻辑
#### Scenario: 验证时机
- **WHEN** 处理请求
- **THEN** SHALL 在 handler 函数开始时验证
- **THEN** SHALL 在验证通过后才执行业务逻辑

View File

@@ -0,0 +1,124 @@
# Structured Logging
## ADDED Requirements
### Requirement: 使用 zap 结构化日志
系统 SHALL 使用 zap 作为结构化日志库。
#### Scenario: 日志初始化
- **WHEN** 应用启动
- **THEN** SHALL 初始化 zap logger
- **THEN** SHALL 根据配置设置日志级别
- **THEN** SHALL 配置日志输出格式为 JSON
#### Scenario: 日志字段
- **WHEN** 记录日志
- **THEN** SHALL 支持结构化字段key-value
- **THEN** SHALL 支持嵌套字段
- **THEN** SHALL 自动包含时间戳和日志级别
### Requirement: 支持日志滚动
系统 SHALL 支持日志文件滚动,使用 lumberjack。
#### Scenario: 按大小滚动
- **WHEN** 日志文件大小达到配置的最大值(默认 100 MB
- **THEN** SHALL 创建新的日志文件
- **THEN** SHALL 重命名旧文件(添加序号)
#### Scenario: 按数量清理
- **WHEN** 日志文件数量超过配置的最大备份数(默认 10 个)
- **THEN** SHALL 删除最旧的日志文件
#### Scenario: 按时间清理
- **WHEN** 日志文件超过配置的最大保留天数(默认 30 天)
- **THEN** SHALL 自动删除过期文件
#### Scenario: 压缩旧文件
- **WHEN** 配置启用压缩(默认启用)
- **THEN** SHALL 压缩旧的日志文件为 .gz 格式
### Requirement: 支持请求 ID 追踪
系统 SHALL 支持请求 ID 追踪。
#### Scenario: 生成请求 ID
- **WHEN** 收到 HTTP 请求
- **THEN** SHALL 生成唯一的请求 IDUUID
- **THEN** SHALL 设置到响应 header 中X-Request-ID
- **THEN** SHALL 添加到日志上下文中
#### Scenario: 复用请求 ID
- **WHEN** 请求 header 中已包含 X-Request-ID
- **THEN** SHALL 复用该请求 ID
- **THEN** SHALL 在整个请求生命周期中使用该 ID
#### Scenario: 日志关联请求 ID
- **WHEN** 记录请求相关的日志
- **THEN** SHALL 自动包含请求 ID 字段
- **THEN** SHALL 支持通过请求 ID 检索日志
### Requirement: 记录请求日志
系统 SHALL 记录 HTTP 请求日志。
#### Scenario: 请求开始日志
- **WHEN** 收到 HTTP 请求
- **THEN** SHALL 记录请求方法、路径、客户端 IP
- **THEN** SHALL 包含请求 ID
#### Scenario: 请求结束日志
- **WHEN** HTTP 请求处理完成
- **THEN** SHALL 记录响应状态码、响应大小
- **THEN** SHALL 记录请求耗时
- **THEN** SHALL 包含请求 ID
### Requirement: 支持日志级别
系统 SHALL 支持日志级别控制。
#### Scenario: 日志级别配置
- **WHEN** 配置日志级别
- **THEN** SHALL 支持 debug、info、warn、error 级别
- **THEN** SHALL 只记录大于等于配置级别的日志
#### Scenario: 开发环境日志
- **WHEN** 配置为开发模式
- **THEN** SHALL 使用 debug 级别
- **THEN** SHALL 输出到控制台和文件
#### Scenario: 生产环境日志
- **WHEN** 配置为生产模式
- **THEN** SHALL 使用 info 级别
- **THEN** SHALL 仅输出到文件
### Requirement: 日志存储位置
日志 SHALL 存储在 `~/.nex/log/` 目录。
#### Scenario: 日志文件路径
- **WHEN** 初始化日志系统
- **THEN** SHALL 使用 `~/.nex/log/` 作为日志目录
- **THEN** SHALL 自动创建目录(如果不存在)
#### Scenario: 日志文件命名
- **WHEN** 创建日志文件
- **THEN** SHALL 使用 `nex-YYYY-MM-DD.log` 格式命名
- **THEN** SHALL 按日期创建新文件

View File

@@ -0,0 +1,106 @@
# Test Coverage
## ADDED Requirements
### Requirement: 建立单元测试体系
系统 SHALL 建立完整的单元测试体系,覆盖核心业务逻辑。
#### Scenario: config 包测试覆盖
- **WHEN** 运行 config 包的单元测试
- **THEN** SHALL 覆盖 Provider、Model、Stats 的 CRUD 操作
- **THEN** SHALL 测试正常场景和错误场景
- **THEN** SHALL 验证数据库操作的准确性
#### Scenario: router 包测试覆盖
- **WHEN** 运行 router 包的单元测试
- **THEN** SHALL 覆盖模型路由逻辑
- **THEN** SHALL 测试模型不存在、模型禁用、供应商禁用等场景
- **THEN** SHALL 验证路由结果的正确性
#### Scenario: protocol 包测试覆盖
- **WHEN** 运行 protocol 包的单元测试
- **THEN** SHALL 覆盖 OpenAI 和 Anthropic 协议转换逻辑
- **THEN** SHALL 测试请求转换、响应转换、流式转换
- **THEN** SHALL 验证转换的准确性和完整性
### Requirement: 建立集成测试体系
系统 SHALL 建立集成测试体系,覆盖 API 端到端流程。
#### Scenario: OpenAI 协议集成测试
- **WHEN** 运行 OpenAI 协议的集成测试
- **THEN** SHALL 测试完整的请求-响应流程
- **THEN** SHALL 测试流式响应流程
- **THEN** SHALL 测试错误处理流程
#### Scenario: Anthropic 协议集成测试
- **WHEN** 运行 Anthropic 协议的集成测试
- **THEN** SHALL 测试完整的请求-响应流程
- **THEN** SHALL 测试流式响应流程
- **THEN** SHALL 测试协议转换的准确性
#### Scenario: 管理接口集成测试
- **WHEN** 运行管理接口的集成测试
- **THEN** SHALL 测试 Provider、Model、Stats 的 CRUD 操作
- **THEN** SHALL 验证 API 响应格式
- **THEN** SHALL 测试错误场景
### Requirement: 提供测试工具函数
系统 SHALL 提供测试工具函数,简化测试编写。
#### Scenario: 测试数据库初始化
- **WHEN** 编写需要数据库的测试
- **THEN** SHALL 提供测试数据库初始化函数
- **THEN** SHALL 使用临时数据库文件
- **THEN** SHALL 在测试结束后自动清理
#### Scenario: Mock 工具
- **WHEN** 编写需要 Mock 的测试
- **THEN** SHALL 提供 Mock 接口实现
- **THEN** SHALL 支持常见 Mock 场景
- **THEN** SHALL 易于使用和扩展
### Requirement: 达到测试覆盖率目标
系统 SHALL 达到 > 80% 的测试覆盖率。
#### Scenario: 总体覆盖率
- **WHEN** 运行所有测试并生成覆盖率报告
- **THEN** 总体覆盖率 SHALL 大于 80%
- **THEN** 核心包覆盖率 SHALL 大于 85%
#### Scenario: 覆盖率报告生成
- **WHEN** 运行测试覆盖率命令
- **THEN** SHALL 生成覆盖率报告文件
- **THEN** SHALL 支持生成 HTML 格式报告
- **THEN** SHALL 显示每个文件的覆盖率
### Requirement: 集成到构建流程
测试 SHALL 集成到构建流程中。
#### Scenario: 运行测试命令
- **WHEN** 执行 `make test` 命令
- **THEN** SHALL 运行所有单元测试和集成测试
- **THEN** SHALL 显示测试结果
- **THEN** SHALL 在测试失败时返回非零退出码
#### Scenario: 覆盖率检查命令
- **WHEN** 执行 `make test-coverage` 命令
- **THEN** SHALL 运行测试并生成覆盖率报告
- **THEN** SHALL 检查覆盖率是否达标
- **THEN** SHALL 在覆盖率不足时返回非零退出码

View File

@@ -1,10 +1,6 @@
# 用量统计
# Usage Statistics
## Purpose
TBD - 提供请求用量统计的记录和查询功能
## Requirements
## MODIFIED Requirements
### Requirement: 记录请求统计
@@ -22,15 +18,7 @@ TBD - 提供请求用量统计的记录和查询功能
- **THEN** 网关 SHALL 增加该供应商和模型的请求计数
- **THEN** 网关 SHALL 在流结束后记录统计
#### Scenario: 不记录失败请求
- **WHEN** 请求在到达供应商前失败(路由错误、验证错误)
- **THEN** 网关 SHALL NOT 增加请求计数
#### Scenario: 记录供应商错误
- **WHEN** 请求到达供应商但供应商返回错误
- **THEN** 网关 SHALL 仍然增加请求计数(请求已被处理)
**变更说明:** 统计记录通过 StatsService 调用,数据访问通过 StatsRepository。API 接口保持不变。
### Requirement: 按供应商查询统计
@@ -41,10 +29,7 @@ TBD - 提供请求用量统计的记录和查询功能
- **WHEN** 向 `/api/stats?provider_id=<provider_id>` 发送 GET 请求
- **THEN** 网关 SHALL 仅返回指定供应商的统计
#### Scenario: 查询不存在供应商的统计
- **WHEN** 向 `/api/stats?provider_id=<non_existent_id>` 发送 GET 请求
- **THEN** 网关 SHALL 返回空结果或零计数
**变更说明:** 通过 StatsService 和 StatsRepository 实现。API 接口保持不变。
### Requirement: 按模型查询统计
@@ -55,10 +40,7 @@ TBD - 提供请求用量统计的记录和查询功能
- **WHEN** 向 `/api/stats?model_name=<model_name>` 发送 GET 请求
- **THEN** 网关 SHALL 仅返回指定模型的统计
#### Scenario: 查询不存在模型的统计
- **WHEN** 向 `/api/stats?model_name=<non_existent_name>` 发送 GET 请求
- **THEN** 网关 SHALL 返回空结果或零计数
**变更说明:** 通过 StatsService 和 StatsRepository 实现。API 接口保持不变。
### Requirement: 按日期范围查询统计
@@ -70,20 +52,7 @@ TBD - 提供请求用量统计的记录和查询功能
- **THEN** 网关 SHALL 仅返回指定范围内的日期统计
- **THEN** 日期格式 SHALL 为 YYYY-MM-DD
#### Scenario: 不使用日期范围查询统计
- **WHEN** 向 `/api/stats` 发送 GET 请求,不带 start 和 end 参数
- **THEN** 网关 SHALL 返回所有可用日期的统计
#### Scenario: 仅使用开始日期查询统计
- **WHEN** 向 `/api/stats?start=<start_date>` 发送 GET 请求
- **THEN** 网关 SHALL 返回从开始日期到当前日期的统计
#### Scenario: 仅使用结束日期查询统计
- **WHEN** 向 `/api/stats?end=<end_date>` 发送 GET 请求
- **THEN** 网关 SHALL 返回从最早可用日期到结束日期的统计
**变更说明:** 通过 StatsService 和 StatsRepository 实现。API 接口保持不变。
### Requirement: 聚合统计
@@ -95,25 +64,7 @@ TBD - 提供请求用量统计的记录和查询功能
- **THEN** 网关 SHALL 为该天维护单条统计记录
- **THEN** 请求计数 SHALL 为所有请求的总和
#### Scenario: 跨多天请求
- **WHEN** 跨不同天发起请求
- **THEN** 网关 SHALL 为每一天维护独立的统计记录
### Requirement: 以结构化格式返回统计
网关 SHALL 以结构化 JSON 格式返回统计。
#### Scenario: 统计响应格式
- **WHEN** 查询统计
- **THEN** 响应 SHALL 为统计对象数组
- **THEN** 每个对象 SHALL 包含 provider_id, model_name, request_count 和 date
#### Scenario: 空统计
- **WHEN** 没有统计匹配查询条件
- **THEN** 网关 SHALL 返回空数组
**变更说明:** 聚合逻辑在 StatsRepository 中实现。API 接口保持不变。
### Requirement: 支持并发统计记录
@@ -125,31 +76,48 @@ TBD - 提供请求用量统计的记录和查询功能
- **THEN** 网关 SHALL 正确为每个请求增加请求计数
- **THEN** 不 SHALL 因并发写入而丢失统计
### Requirement: 仅将统计限制为请求计数
**变更说明:** 并发控制在 StatsRepository 中通过数据库事务实现。API 接口保持不变。
网关 SHALL 在 MVP 中仅记录请求计数,不记录其他指标。
## ADDED Requirements
#### Scenario: 仅请求计数
### Requirement: 使用 service 层处理业务逻辑
Handler SHALL 通过 StatsService 处理业务逻辑。
#### Scenario: 调用 service 方法
- **WHEN** handler 收到统计查询请求
- **THEN** SHALL 调用对应的 StatsService 方法Get、Aggregate
- **THEN** SHALL 使用 domain.UsageStats 类型
#### Scenario: 异步记录统计
- **WHEN** 请求完成需要记录统计
- **THEN** SHALL 异步调用 StatsService.Record()
- **THEN** SHALL 不阻塞响应返回
### Requirement: 使用 repository 层访问数据
Service SHALL 通过 StatsRepository 访问数据。
#### Scenario: 调用 repository 方法
- **WHEN** service 处理业务逻辑
- **THEN** SHALL 调用对应的 StatsRepository 方法
- **THEN** SHALL 使用 domain.UsageStats 类型
#### Scenario: 事务处理
- **WHEN** 记录统计
- **THEN** 网关 SHALL 仅跟踪请求数量
- **THEN** 网关 SHALL NOT 在 MVP 中跟踪 token 使用、成本、延迟或其他指标
- **THEN** SHALL 在 repository 层使用数据库事务
- **THEN** SHALL 确保并发安全
### Requirement: 为新组合初始化统计
### Requirement: 统计查询优化
网关 SHALL 为新的供应商-模型-日期组合自动创建统计记录
统计查询 SHALL 使用索引优化性能
#### Scenario: 组合的首次请求
#### Scenario: 使用索引
- **WHEN** 在新日期首次对供应商-模型组合发起请求
- **THEN** 网关 SHALL 创建新的统计记录request_count = 1
### Requirement: 查询所有统计
网关 SHALL 允许不带过滤条件查询所有统计。
#### Scenario: 查询所有统计
- **WHEN** 向 `/api/stats` 发送 GET 请求,不带任何查询参数
- **THEN** 网关 SHALL 返回所有可用统计
- **THEN** 结果 SHALL 按日期排序(最近的在前)
- **WHEN** 查询统计
- **THEN** SHALL 使用 (provider_id, model_name, date) 复合索引
- **THEN** SHALL 优化查询性能