docs: 更新三份 README 文档以反映实际项目情况
This commit is contained in:
55
README.md
55
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/` - 日志目录
|
||||
|
||||
@@ -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`
|
||||
|
||||
#### 模型管理
|
||||
|
||||
@@ -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<T>(method: string, path: string, body?: unknown): Promise<T>
|
||||
```
|
||||
|
||||
- 自动处理字段转换
|
||||
- 自动处理 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
|
||||
|
||||
Reference in New Issue
Block a user