From 72aebef625f76391b4b15a73321ffa0160503bff Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 22 Apr 2026 16:05:23 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E4=B8=89=E4=BB=BD=20?= =?UTF-8?q?README=20=E6=96=87=E6=A1=A3=E4=BB=A5=E5=8F=8D=E6=98=A0=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E9=A1=B9=E7=9B=AE=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 55 +++++++++++++++---- backend/README.md | 131 ++++++++++++++++++++++++++++++++++++++++++--- frontend/README.md | 108 +++++++++++++++++++++++++++++++++++-- 3 files changed, 274 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 946e6e2..04db2f7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ nex/ │ │ ├── service/ # 业务逻辑层 │ │ ├── repository/ # 数据访问层 │ │ ├── domain/ # 领域模型 -│ │ ├── protocol/ # 协议适配器(OpenAI/Anthropic) +│ │ ├── conversion/ # 协议转换引擎(OpenAI/Anthropic 适配器) │ │ ├── provider/ # 供应商客户端 │ │ └── config/ # 配置管理 │ ├── pkg/ # 公共包(logger/errors/validator) @@ -38,10 +38,13 @@ nex/ ## 功能特性 - **双协议支持**:同时支持 OpenAI 和 Anthropic 协议 +- **跨协议转换**:Hub-and-Spoke 架构实现 OpenAI ↔ Anthropic 双向转换 - **统一模型 ID**:`provider_id/model_name` 格式全局唯一标识模型(如 `openai/gpt-4`) -- **透明代理**:对 OpenAI 兼容供应商 Smart Passthrough,最小化改写保持参数保真 -- **流式响应**:完整支持 SSE 流式传输 +- **Smart Passthrough**:同协议请求零序列化开销,仅改写 model 字段 +- **流式响应**:完整支持 SSE 流式传输,包括跨协议流式转换 - **Function Calling**:支持工具调用(Tools) +- **Thinking / Reasoning**:支持 OpenAI `reasoning_effort` 和 Anthropic `thinking` 配置 +- **扩展接口**:支持 Embeddings 和 Rerank 接口 - **多供应商管理**:配置和管理多个供应商(供应商 ID 仅限字母、数字、下划线) - **用量统计**:按供应商、模型、日期统计请求数量 - **Web 配置界面**:提供供应商和模型配置管理 @@ -54,7 +57,7 @@ nex/ - **ORM**: GORM - **数据库**: SQLite - **日志**: zap + lumberjack(结构化日志 + 日志轮转) -- **配置**: gopkg.in/yaml.v3 +- **配置**: Viper + pflag(多层配置:CLI > 环境变量 > 配置文件 > 默认值) - **验证**: go-playground/validator/v10 - **迁移**: goose @@ -100,12 +103,19 @@ bun dev ### 代理接口(对外部应用) -代理接口统一使用 `provider_id/model_name` 格式的模型 ID(如 `openai/gpt-4`)。同协议请求走 Smart Passthrough,最小化 JSON 改写保持参数保真;跨协议请求走完整 decode/encode 转换。 +代理接口统一使用 `/{protocol}/*path` 路由格式,模型 ID 使用 `provider_id/model_name` 格式(如 `openai/gpt-4`)。同协议请求走 Smart Passthrough,最小化 JSON 改写保持参数保真;跨协议请求走完整 decode/encode 转换。 -- `POST /v1/chat/completions` - OpenAI Chat Completions API -- `POST /v1/messages` - Anthropic Messages API -- `GET /v1/models` - 模型列表(本地数据库聚合,不请求上游) -- `GET /v1/models/{provider_id}/{model_name}` - 模型详情(本地数据库查询) +**OpenAI 协议**(`protocol=openai`): +- `POST /openai/chat/completions` - 对话补全 +- `GET /openai/models` - 模型列表(本地数据库聚合) +- `GET /openai/models/{provider_id}/{model_name}` - 模型详情(本地数据库查询) +- `POST /openai/embeddings` - 嵌入 +- `POST /openai/rerank` - 重排序 + +**Anthropic 协议**(`protocol=anthropic`): +- `POST /anthropic/v1/messages` - 消息对话 +- `GET /anthropic/v1/models` - 模型列表(本地数据库聚合) +- `GET /anthropic/v1/models/{provider_id}/{model_name}` - 模型详情(本地数据库查询) ### 管理接口(对前端) @@ -131,6 +141,10 @@ bun dev ## 配置 +配置支持多种方式,优先级为:**CLI 参数 > 环境变量 > 配置文件 > 默认值** + +### 配置文件 + 配置文件位于 `~/.nex/config.yaml`,首次启动自动生成: ```yaml @@ -154,7 +168,28 @@ log: compress: true ``` -数据文件: +### 环境变量 + +所有配置项支持环境变量,使用 `NEX_` 前缀: + +```bash +export NEX_SERVER_PORT=9000 +export NEX_DATABASE_PATH=/data/nex.db +export NEX_LOG_LEVEL=debug +``` + +命名规则:配置路径转大写 + 下划线(如 `server.port` → `NEX_SERVER_PORT`)。 + +### CLI 参数 + +```bash +./server --server-port 9000 --log-level debug --database-path /tmp/test.db +``` + +命名规则:配置路径转 kebab-case(如 `server.port` → `--server-port`)。 + +### 数据文件 + - `~/.nex/config.yaml` - 配置文件 - `~/.nex/config.db` - SQLite 数据库 - `~/.nex/log/` - 日志目录 diff --git a/backend/README.md b/backend/README.md index c143ed9..4041b3e 100644 --- a/backend/README.md +++ b/backend/README.md @@ -116,8 +116,11 @@ backend/ ├── migrations/ # 数据库迁移 │ └── 20260421000001_initial_schema.sql ├── tests/ # 集成测试 -│ ├── helpers.go -│ └── integration/ +│ ├── helpers.go # 测试辅助函数 +│ ├── config/ # 测试配置 +│ ├── integration/ # 集成测试 +│ │ └── e2e_conversion_test.go # E2E 协议转换测试 +│ └── mocks/ # Mock 实现 ├── Makefile ├── go.mod └── README.md @@ -146,6 +149,120 @@ Client Request (clientProtocol) 同协议时自动透传,跳过序列化开销。 +## 协议转换架构 + +### Canonical Model 中间表示 + +所有协议转换都经过 Canonical Model 中间表示层,实现 Hub-and-Spoke 架构: + +``` +OpenAI Request → Canonical Request → Anthropic Request + (中间表示) +OpenAI Response ← Canonical Response ← Anthropic Response +``` + +**CanonicalRequest 核心字段**: +- `Model` - 统一模型 ID +- `Messages` - 消息列表(支持 text、tool_use、tool_result、thinking 类型) +- `Tools` - 工具定义 +- `Thinking` - 推理配置(`budget_tokens`、`effort`) +- `Parameters` - 通用参数(`max_tokens`、`temperature`、`top_p` 等) + +### Smart Passthrough 机制 + +同协议请求走 Smart Passthrough 路径,**零序列化开销**: + +``` +1. 检测 clientProtocol == providerProtocol +2. 仅改写请求体中的 model 字段:unified_id → upstream_model_name +3. 直接转发请求到上游 +4. 响应中仅改写 model 字段:upstream_model_name → unified_id +``` + +### 流式转换器层次 + +``` +StreamConverter (接口) +├── PassthroughStreamConverter # 直接透传,无任何处理 +├── SmartPassthroughStreamConverter # 同协议 + 逐 chunk 改写 model +└── CanonicalStreamConverter # 跨协议完整转换(decode → encode) +``` + +### InterfaceType 枚举 + +| 类型 | 说明 | +|------|------| +| `CHAT` | 对话补全(chat/completions、messages) | +| `MODELS` | 模型列表 | +| `MODEL_INFO` | 模型详情 | +| `EMBEDDINGS` | 嵌入接口 | +| `RERANK` | 重排序接口 | +| `PASSTHROUGH` | 未知接口,直接透传 | + +## 协议适配器特性 + +### OpenAI 适配器 + +**特有字段支持**: +- `reasoning_effort` - 映射到 Canonical Thinking 配置(`none` → 禁用,其他 → `effort`) +- `reasoning_content` - 非标准字段,映射到 Canonical thinking 块 +- `max_completion_tokens` - 新字段,优先于 `max_tokens` +- `refusal` - 非标准字段,作为 text 块处理 + +**废弃字段兼容**: +- `functions` / `function_call` - 自动转换为 `tools` / `tool_choice` + +**消息处理**: +- 合并连续同角色消息(Anthropic 不支持连续同角色) +- 工具选择映射:`any` → `required` + +### Anthropic 适配器 + +**特有字段支持**: +- `thinking` - 推理配置(`type: enabled`、`budget_tokens`、`effort`) +- `output_config` - 结构化输出配置 +- `disable_parallel_tool_use` - 禁用并行工具调用 +- `container` - 工具容器字段 + +**不支持的功能**: +- Embeddings 接口(返回 `INTERFACE_NOT_SUPPORTED` 错误) + +### 跨协议转换注意事项 + +| 源协议 | 目标协议 | 转换说明 | +|--------|----------|----------| +| OpenAI | Anthropic | `reasoning_effort` → `thinking`,消息角色合并 | +| Anthropic | OpenAI | `thinking` 块 → `reasoning_content`,工具选择转换 | + +## 错误码 + +### ConversionError 错误码 + +| 错误码 | 说明 | +|--------|------| +| `INVALID_INPUT` | 输入数据无效 | +| `MISSING_REQUIRED_FIELD` | 缺少必填字段 | +| `INCOMPATIBLE_FEATURE` | 功能不兼容(如跨协议不支持某特性) | +| `FIELD_MAPPING_FAILURE` | 字段映射失败 | +| `TOOL_CALL_PARSE_ERROR` | 工具调用解析错误 | +| `JSON_PARSE_ERROR` | JSON 解析错误 | +| `STREAM_STATE_ERROR` | 流式状态错误 | +| `UTF8_DECODE_ERROR` | UTF-8 解码错误(流式 chunk 截断) | +| `PROTOCOL_CONSTRAINT_VIOLATION` | 协议约束违反 | +| `ENCODING_FAILURE` | 编码失败 | +| `INTERFACE_NOT_SUPPORTED` | 接口不支持(如 Anthropic Embeddings) | + +### AppError 预定义错误 + +| 错误 | HTTP 状态码 | 说明 | +|------|-------------|------| +| `ErrModelNotFound` | 404 | 模型未找到 | +| `ErrModelDisabled` | 404 | 模型已禁用 | +| `ErrProviderNotFound` | 404 | 供应商未找到 | +| `ErrInvalidProviderID` | 400 | 供应商 ID 格式无效 | +| `ErrDuplicateModel` | 409 | 同一供应商下模型名称重复 | +| `ErrImmutableField` | 400 | 不可修改字段(如供应商 ID) | + ## 运行方式 ### 安装依赖 @@ -278,10 +395,10 @@ goose -dir migrations sqlite3 ~/.nex/config.db up #### OpenAI 协议 ``` -POST /openai/v1/chat/completions -GET /openai/v1/models -POST /openai/v1/embeddings -POST /openai/v1/rerank +POST /openai/chat/completions +GET /openai/models +POST /openai/embeddings +POST /openai/rerank ``` #### Anthropic 协议 @@ -322,7 +439,7 @@ GET /anthropic/v1/models - Anthropic 协议:配置到域名,不包含版本路径,如 `https://api.anthropic.com` **对外 URL 格式**: -- OpenAI 协议:`/{protocol}/{endpoint}`,如 `/openai/chat/completions`、`/openai/models` +- OpenAI 协议:`/{protocol}/{endpoint}`,如 `/openai/chat/completions`、`/openai/models`、`/openai/embeddings` - Anthropic 协议:`/{protocol}/v1/{endpoint}`,如 `/anthropic/v1/messages`、`/anthropic/v1/models` #### 模型管理 diff --git a/frontend/README.md b/frontend/README.md index ba3fd40..4f2b6ee 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -14,6 +14,68 @@ AI 网关管理前端,提供供应商配置和用量统计界面。 - **样式**: SCSS Modules(禁止使用纯 CSS) - **测试**: Vitest + React Testing Library + Playwright +## API 层 + +### 字段转换机制 + +后端使用 `snake_case`,前端使用 `camelCase`,API 层自动转换: + +```typescript +// 发送请求时:camelCase → snake_case +toApi({ providerId: "openai" }) // → { provider_id: "openai" } + +// 接收响应时:snake_case → camelCase +fromApi({ provider_id: "openai" }) // → { providerId: "openai" } +``` + +### 统一请求函数 + +```typescript +export async function request(method: string, path: string, body?: unknown): Promise +``` + +- 自动处理字段转换 +- 自动处理 204 响应(无 body) +- 抛出 `ApiError` 包含 `status`、`code`、`message` + +### 错误处理 + +```typescript +class ApiError extends Error { + status: number; // HTTP 状态码 + code?: string; // 业务错误码 + message: string; // 错误消息 +} +``` + +## TanStack Query 模式 + +### Query Keys 定义 + +```typescript +// src/hooks/useProviders.ts +export const providerKeys = { + all: ['providers'] as const, +}; + +// src/hooks/useModels.ts +export const modelKeys = { + all: ['models'] as const, + byProvider: (providerId: string) => [...modelKeys.all, { providerId }] as const, +}; +``` + +### Mutation 使用 + +```typescript +const mutation = useMutation({ + mutationFn: createProvider, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: providerKeys.all }); + }, +}); +``` + ## 项目结构 ``` @@ -25,7 +87,7 @@ frontend/ │ │ ├── models.ts # Model CRUD │ │ └── stats.ts # Stats 查询 │ ├── components/ -│ │ └── AppLayout/ # 顶部导航布局 +│ │ └── AppLayout/ # 侧边栏导航布局 │ ├── hooks/ # TanStack Query hooks │ │ ├── useProviders.ts │ │ ├── useModels.ts @@ -33,6 +95,7 @@ frontend/ │ ├── pages/ │ │ ├── Providers/ # 供应商管理(含内嵌模型管理) │ │ ├── Stats/ # 用量统计 +│ │ ├── Settings/ # 设置(开发中) │ │ └── NotFound.tsx │ ├── routes/ │ │ └── index.tsx # 路由配置 @@ -125,11 +188,50 @@ bun run test:e2e - 按模型筛选 - 按日期范围筛选(DatePicker.RangePicker) +## 测试策略 + +### 目录结构 + +``` +__tests__/ +├── setup.ts # 测试配置(happy-dom) +├── api/ # API 层测试 +│ └── client.test.ts +├── hooks/ # TanStack Query Hook 测试 +│ ├── useProviders.test.ts +│ └── useModels.test.ts +└── components/ # 组件测试 + └── AppLayout.test.tsx +``` + +### E2E 测试 + +- 位于 `e2e/` 目录 +- 使用 Playwright +- 自动启动后端服务(临时端口 19026) +- 配置文件:`playwright.config.ts` + +### Mock 策略 + +- API 层测试使用 MSW(Mock Service Worker) +- Hook 测试使用 `@testing-library/react-hooks` +- 组件测试使用 `@testing-library/react` + +## 环境变量 + +| 变量 | 开发环境 | 生产环境 | 说明 | +|------|----------|----------|------| +| `VITE_API_BASE` | (空) | `/api` | API 基础路径,空则走 Vite proxy | + +**E2E 测试特有**: +- `NEX_BACKEND_PORT` - E2E 后端端口(默认 19026) +- `NEX_E2E_TEMP_DIR` - E2E 临时目录 + ## 开发规范 - 所有样式使用 SCSS,禁止使用纯 CSS 文件 - 组件级样式使用 SCSS Modules(*.module.scss) -- 图标优先使用 @ant-design/icons +- 图标优先使用 TDesign 图标(tdesign-icons-react) - TypeScript strict 模式,禁止 any 类型 - API 层自动处理 snake_case ↔ camelCase 字段转换 - 使用路径别名 `@/` 引用 src 目录 @@ -143,4 +245,4 @@ bun run test:e2e 1. 在 `src/pages/` 创建页面目录和组件 2. 在 `src/hooks/` 创建对应的 TanStack Query hook 3. 在 `src/routes/index.tsx` 添加路由 -4. 在 `src/components/AppLayout/index.tsx` 的 menuItems 添加导航项 +4. 在 `src/components/AppLayout/index.tsx` 的 Menu 中添加 MenuItem