feat: 实现ChatGPT Codex路由器的核心功能

- 添加完整的项目基础结构,包括配置、类型定义和常量
- 实现OAuth认证流程和令牌管理
- 开发请求转换和响应处理逻辑
- 添加SSE流处理和ChatCompletions API转换
- 实现模型映射和提示指令系统
- 包含Docker部署配置和快速启动文档
- 添加自动登录功能和测试脚本
This commit is contained in:
mars
2026-01-07 10:51:54 +08:00
commit 0dd6fe2c7d
45 changed files with 8286 additions and 0 deletions

774
plan.md Normal file
View File

@@ -0,0 +1,774 @@
# chatgpt-codex-router 项目计划
## 项目概览
**项目目标:** 创建一个独立的 OpenAI 兼容 API 服务器,通过 OAuth 认证将请求转发到 ChatGPT 后端,支持流式传输和详细的日志记录。
**支持的模型GPT-5.x 系列):**
- `gpt-5.1` (none/low/medium/high)
- `gpt-5.2` (none/low/medium/high/xhigh)
- `gpt-5.1-codex` (low/medium/high)
- `gpt-5.1-codex-max` (low/medium/high/xhigh)
- `gpt-5.1-codex-mini` (medium/high)
- `gpt-5.2-codex` (low/medium/high/xhigh)
---
## 项目结构
```
chatgpt-codex-router/
├── src/
│ ├── index.ts # 服务器入口
│ ├── server.ts # Hono 服务器配置
│ ├── router.ts # API 路由定义
│ ├── config.ts # 配置管理
│ ├── logger.ts # 日志系统
│ ├── auth/ # 认证模块
│ │ ├── oauth.ts # OAuth 流程逻辑
│ │ ├── token-storage.ts # Token 本地 JSON 存储
│ │ ├── token-refresh.ts # Token 刷新逻辑
│ │ ├── server.ts # 本地 OAuth 回调服务器
│ │ └── browser.ts # 浏览器打开工具
│ ├── request/ # 请求处理
│ │ ├── transformer.ts # 请求体转换
│ │ ├── headers.ts # Header 生成
│ │ ├── validator.ts # 请求验证
│ │ ├── model-map.ts # 模型映射
│ │ └── reasoning.ts # 推理参数配置
│ ├── response/ # 响应处理
│ │ ├── handler.ts # 响应处理器
│ │ ├── sse-parser.ts # SSE 流解析
│ │ ├── converter.ts # 响应格式转换
│ │ └── chat-completions.ts # Chat Completions 格式转换器
│ ├── prompts/ # 内置 Codex Prompts
│ │ ├── gpt-5-1.md # GPT-5.1 系统提示
│ │ ├── gpt-5-2.md # GPT-5.2 系统提示
│ │ ├── gpt-5-1-codex.md # GPT-5.1 Codex 系统提示
│ │ ├── gpt-5-1-codex-max.md # GPT-5.1 Codex Max 系统提示
│ │ ├── gpt-5-1-codex-mini.md # GPT-5.1 Codex Mini 系统提示
│ │ ├── gpt-5-2-codex.md # GPT-5.2 Codex 系统提示
│ │ └── index.ts # Prompt 加载器
│ ├── constants.ts # 常量定义
│ └── types.ts # TypeScript 类型定义
├── public/
│ └── oauth-success.html # OAuth 成功页面
├── docker/
│ ├── Dockerfile # Docker 镜像配置
│ ├── docker-compose.yml # Docker Compose 配置
│ └── .dockerignore # Docker 忽略文件
├── logs/ # 日志输出目录(.gitignore
├── data/ # 数据目录(.gitignore存储 tokens
├── package.json
├── tsconfig.json
├── .gitignore
└── README.md
```
---
## 核心功能模块详解
### 1. 认证模块 (`src/auth/`)
#### 1.1 OAuth 流程 (`oauth.ts`)
**功能:**
- PKCE challenge 生成
- 授权 URL 构建
- Authorization code 交换为 tokens
- JWT 解析获取 account_id
**关键常量:**
```typescript
CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
TOKEN_URL = "https://auth.openai.com/oauth/token"
REDIRECT_URI = "http://localhost:1455/auth/callback"
SCOPE = "openid profile email offline_access"
```
#### 1.2 Token 存储 (`token-storage.ts`)
**功能:**
- 读取/保存/删除 tokens
- 存储路径:`data/tokens.json`
- 存储格式:
```json
{
"access_token": "...",
"refresh_token": "...",
"expires_at": 1234567890,
"account_id": "...",
"updated_at": 1234567890
}
```
#### 1.3 Token 刷新 (`token-refresh.ts`)
**功能:**
- 检查 token 是否过期(提前 5 分钟刷新)
- 自动刷新过期的 token
- 更新本地存储
- 刷新失败时抛出错误
#### 1.4 本地 OAuth 服务器 (`server.ts`)
**功能:**
- 监听 `http://127.0.0.1:1455/auth/callback`
- 接收并验证 authorization code
- 返回 OAuth 成功页面
- Polling 机制(最多等待 60 秒)
#### 1.5 浏览器工具 (`browser.ts`)
**功能:**
- 跨平台浏览器打开macOS/Linux/Windows
- 静默失败(用户可手动复制 URL
---
### 2. 请求处理模块 (`src/request/`)
#### 2.1 请求体转换 (`transformer.ts`)
**核心转换逻辑:**
```typescript
// 原始请求
{
"model": "gpt-5.2-codex",
"messages": [...],
"stream": false,
"temperature": 0.7
}
// 转换后请求
{
"model": "gpt-5.2-codex",
"input": [...], // messages 转换为 input 格式
"stream": true, // 强制 stream=true
"store": false, // 添加 store=false
"instructions": "...", // 添加 Codex 系统提示
"reasoning": {
"effort": "high",
"summary": "auto"
},
"text": {
"verbosity": "medium"
},
"include": ["reasoning.encrypted_content"]
}
```
**转换步骤:**
1. 模型名称标准化(使用 model-map
2. `messages` → `input` 格式转换
3. 过滤 `item_reference` 和 IDs
4. 添加 `store: false`, `stream: true`
5. 添加 Codex 系统提示(从内置 prompts 加载)
6. 配置 reasoning 参数(根据模型类型)
7. 配置 text verbosity
8. 添加 include 参数
9. 移除不支持的参数(`max_output_tokens`, `max_completion_tokens`
#### 2.2 模型映射 (`model-map.ts`)
**支持的模型:**
```typescript
const MODEL_MAP: Record<string, string> = {
// GPT-5.1
"gpt-5.1": "gpt-5.1",
"gpt-5.1-none": "gpt-5.1",
"gpt-5.1-low": "gpt-5.1",
"gpt-5.1-medium": "gpt-5.1",
"gpt-5.1-high": "gpt-5.1",
// GPT-5.2
"gpt-5.2": "gpt-5.2",
"gpt-5.2-none": "gpt-5.2",
"gpt-5.2-low": "gpt-5.2",
"gpt-5.2-medium": "gpt-5.2",
"gpt-5.2-high": "gpt-5.2",
"gpt-5.2-xhigh": "gpt-5.2",
// GPT-5.1 Codex
"gpt-5.1-codex": "gpt-5.1-codex",
"gpt-5.1-codex-low": "gpt-5.1-codex",
"gpt-5.1-codex-medium": "gpt-5.1-codex",
"gpt-5.1-codex-high": "gpt-5.1-codex",
// GPT-5.1 Codex Max
"gpt-5.1-codex-max": "gpt-5.1-codex-max",
"gpt-5.1-codex-max-low": "gpt-5.1-codex-max",
"gpt-5.1-codex-max-medium": "gpt-5.1-codex-max",
"gpt-5.1-codex-max-high": "gpt-5.1-codex-max",
"gpt-5.1-codex-max-xhigh": "gpt-5.1-codex-max",
// GPT-5.1 Codex Mini
"gpt-5.1-codex-mini": "gpt-5.1-codex-mini",
"gpt-5.1-codex-mini-medium": "gpt-5.1-codex-mini",
"gpt-5.1-codex-mini-high": "gpt-5.1-codex-mini",
// GPT-5.2 Codex
"gpt-5.2-codex": "gpt-5.2-codex",
"gpt-5.2-codex-low": "gpt-5.2-codex",
"gpt-5.2-codex-medium": "gpt-5.2-codex",
"gpt-5.2-codex-high": "gpt-5.2-codex",
"gpt-5.2-codex-xhigh": "gpt-5.2-codex"
};
```
#### 2.3 Header 生成 (`headers.ts`)
**Headers 列表:**
```typescript
{
"Authorization": `Bearer ${accessToken}`,
"chatgpt-account-id": accountId,
"OpenAI-Beta": "responses=experimental",
"originator": "codex_cli_rs",
"session_id": promptCacheKey,
"conversation_id": promptCacheKey,
"accept": "text/event-stream",
"content-type": "application/json"
}
```
#### 2.4 Reasoning 配置 (`reasoning.ts`)
**Reasoning effort 配置:**
- `gpt-5.2`, `gpt-5.1`: 支持 `none/low/medium/high`(默认 `none`
- `gpt-5.2-codex`, `gpt-5.1-codex-max`: 支持 `low/medium/high/xhigh`(默认 `high`
- `gpt-5.1-codex`: 支持 `low/medium/high`(默认 `medium`
- `gpt-5.1-codex-mini`: 支持 `medium/high`(默认 `medium`
#### 2.5 请求验证 (`validator.ts`)
**验证内容:**
- `model` 参数是否存在且有效
- `messages` 或 `input` 是否存在
- 消息格式是否正确
- 必需字段是否存在
---
### 3. 响应处理模块 (`src/response/`)
#### 3.1 响应处理器 (`handler.ts`)
**处理流程:**
```typescript
async function handleResponse(response: Response, isStreaming: boolean): Promise<Response> {
if (isStreaming) {
// 流式响应:直接转发 SSE
return forwardStream(response);
} else {
// 非流式:解析 SSE 并转换为 JSON
return parseSseToJson(response);
}
}
```
#### 3.2 SSE 解析器 (`sse-parser.ts`)
**解析逻辑:**
```typescript
function parseSseStream(sseText: string): Response | null {
const lines = sseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.substring(6));
if (data.type === 'response.done' || data.type === 'response.completed') {
return data.response;
}
}
}
return null;
}
```
#### 3.3 响应格式转换 (`converter.ts` & `chat-completions.ts`)
**ChatGPT Responses API 格式 → OpenAI Chat Completions 格式:**
**原始格式ChatGPT**
```json
{
"type": "response.done",
"response": {
"id": "resp_...",
"status": "completed",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "Hello, world!"
}
]
}
],
"usage": {
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150
}
}
}
```
**转换后格式OpenAI**
```json
{
"id": "resp_...",
"object": "chat.completion",
"created": 1736153600,
"model": "gpt-5.2-codex",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello, world!"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150
}
}
```
**流式响应格式转换:**
**原始 SSE 事件ChatGPT**
```
data: {"type": "response.output_item.add.delta", "delta": {"content": [{"type": "output_text", "text": "Hello"}]}}
data: {"type": "response.output_item.add.delta", "delta": {"content": [{"type": "output_text", "text": ", world!"}]}}
data: {"type": "response.done", "response": {...}}
```
**转换后 SSE 事件OpenAI**
```
data: {"id": "...", "object": "chat.completion.chunk", "created": 1736153600, "model": "gpt-5.2-codex", "choices": [{"index": 0, "delta": {"content": "Hello"}, "finish_reason": null}]}
data: {"id": "...", "object": "chat.completion.chunk", "created": 1736153600, "model": "gpt-5.2-codex", "choices": [{"index": 0, "delta": {"content": ", world!"}, "finish_reason": null}]}
data: {"id": "...", "object": "chat.completion.chunk", "created": 1736153600, "model": "gpt-5.2-codex", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}
```
---
### 4. 内置 Prompts (`src/prompts/`)
**文件结构:**
- `gpt-5-1.md` - GPT-5.1 通用模型系统提示
- `gpt-5-2.md` - GPT-5.2 通用模型系统提示
- `gpt-5-1-codex.md` - GPT-5.1 Codex 系统提示
- `gpt-5-1-codex-max.md` - GPT-5.1 Codex Max 系统提示
- `gpt-5-1-codex-mini.md` - GPT-5.1 Codex Mini 系统提示
- `gpt-5-2-codex.md` - GPT-5.2 Codex 系统提示
**Prompt 加载器 (`index.ts`)**
```typescript
export function getPrompt(modelFamily: ModelFamily): string {
const promptFile = PROMPT_FILES[modelFamily];
return readFileSync(join(__dirname, promptFile), 'utf-8');
}
```
**Prompts 内容:** 需要从 Codex CLI GitHub 仓库下载最新的 prompts 并嵌入到项目中。
---
### 5. 日志系统 (`src/logger.ts`)
**日志级别:**
- `ERROR` - 错误日志(始终记录)
- `WARN` - 警告日志(始终记录)
- `INFO` - 信息日志(通过 `LOG_LEVEL=info` 启用)
- `DEBUG` - 调试日志(通过 `LOG_LEVEL=debug` 启用)
**日志配置:**
- 日志目录:`logs/`
- 日志文件格式:`{date}-{level}.log`
- 日志格式:`[timestamp] [level] [request-id] message`
- 控制台输出:带颜色和结构化数据
**日志内容:**
- 请求开始/结束
- Token 刷新
- 请求转换前后
- 响应状态
- 错误详情(请求/响应体)
**示例日志:**
```
[2025-01-06 10:30:00] [INFO] [req-001] POST /v1/chat/completions
[2025-01-06 10:30:00] [DEBUG] [req-001] Before transform: {"model": "gpt-5.2-codex", "messages": [...]}
[2025-01-06 10:30:00] [DEBUG] [req-001] After transform: {"model": "gpt-5.2-codex", "input": [...], "stream": true, "store": false}
[2025-01-06 10:30:01] [INFO] [req-001] Response: 200 OK, 150 tokens
[2025-01-06 10:30:01] [INFO] [req-001] Request completed in 1234ms
```
---
### 6. API 端点
#### 6.1 `POST /v1/chat/completions`
**请求:** OpenAI Chat Completions 格式
**响应:** OpenAI Chat Completions 格式(转换后)
**流程:**
1. 验证请求体
2. 检查 token自动刷新
3. 转换请求体messages → input
4. 生成 Headers
5. 转发到 `https://chatgpt.com/backend-api/codex/responses`
6. 处理响应(流式/非流式)
7. 转换响应格式ChatGPT → OpenAI
#### 6.2 `POST /v1/responses`
**请求:** OpenAI Responses API 格式
**响应:** OpenAI Responses API 格式(部分转换)
**流程:**
1. 验证请求体
2. 检查并刷新 token
3. 转换请求体(添加必需字段)
4. 生成 Headers
5. 转发到 `https://chatgpt.com/backend-api/codex/responses`
6. 处理响应(流式/非流式)
7. 返回转换后的 Responses API 格式
#### 6.3 `POST /auth/login`
**请求:** 无需参数
**响应:** 授权 URL 和说明
**流程:**
1. 生成 PKCE challenge 和 state
2. 构建授权 URL
3. 启动本地 OAuth 服务器
4. 尝试打开浏览器
5. 返回授权信息
#### 6.4 `POST /auth/callback`
**内部端点:** 仅用于本地 OAuth 服务器
- 接收 authorization code
- 验证 state
- 交换 tokens
- 保存到 `data/tokens.json`
- 返回成功页面
---
### 7. 配置管理 (`src/config.ts`)
**配置文件:** `~/.chatgpt-codex-router/config.json`
**默认配置:**
```json
{
"server": {
"port": 3000,
"host": "0.0.0.0"
},
"oauth": {
"clientId": "app_EMoamEEZ73f0CkXaXp7hrann",
"redirectUri": "http://localhost:1455/auth/callback",
"localServerPort": 1455
},
"backend": {
"url": "https://chatgpt.com/backend-api",
"timeout": 120000
},
"logging": {
"level": "info",
"dir": "logs",
"enableRequestLogging": false
},
"codex": {
"mode": true,
"defaultReasoningEffort": "medium",
"defaultTextVerbosity": "medium"
}
}
```
**环境变量:**
- `PORT` - 服务器端口(默认 3000
- `CONFIG_PATH` - 配置文件路径
- `LOG_LEVEL` - 日志级别error/warn/info/debug
- `ENABLE_REQUEST_LOGGING` - 启用请求日志true/false
---
### 8. Docker 支持 (`docker/`)
#### 8.1 Dockerfile
```dockerfile
FROM node:20-alpine
WORKDIR /app
# 安装依赖
COPY package.json package-lock.json ./
RUN npm ci --only=production
# 复制源代码
COPY src/ ./src/
COPY public/ ./public/
COPY tsconfig.json ./
# 构建项目
RUN npm run build
# 创建数据和日志目录
RUN mkdir -p /app/data /app/logs
# 暴露端口
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# 启动服务
CMD ["npm", "start"]
```
#### 8.2 docker-compose.yml
```yaml
version: '3.8'
services:
chatgpt-codex-router:
build: .
container_name: chatgpt-codex-router
ports:
- "3000:3000"
- "1455:1455"
volumes:
- ./data:/app/data
- ./logs:/app/logs
- ./config.json:/app/.chatgpt-codex-router/config.json:ro
environment:
- PORT=3000
- LOG_LEVEL=info
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 5s
```
#### 8.3 .dockerignore
```
node_modules
npm-debug.log
.git
.gitignore
README.md
.dockerignore
Dockerfile
docker-compose.yml
test
vitest.config.ts
logs/*
data/*
!data/.gitkeep
```
---
## 开发步骤
### 阶段 1项目初始化第 1-2 天)
- [ ] 创建项目结构
- [ ] 配置 TypeScript
- [ ] 配置 package.json依赖、脚本
- [ ] 创建 .gitignore
- [ ] 设置开发环境ESLint、Prettier
### 阶段 2基础设施第 3-4 天)
- [ ] 实现日志系统 (`logger.ts`)
- [ ] 实现配置管理 (`config.ts`)
- [ ] 实现常量定义 (`constants.ts`)
- [ ] 实现类型定义 (`types.ts`)
- [ ] 创建基础服务器框架 (`server.ts`, `index.ts`)
### 阶段 3认证模块第 5-7 天)
- [ ] 实现 OAuth 流程 (`oauth.ts`)
- [ ] 实现 Token 存储 (`token-storage.ts`)
- [ ] 实现 Token 刷新 (`token-refresh.ts`)
- [ ] 实现本地 OAuth 服务器 (`server.ts`)
- [ ] 实现浏览器工具 (`browser.ts`)
- [ ] 创建 OAuth 成功页面 (`public/oauth-success.html`)
### 阶段 4请求处理第 8-10 天)
- [ ] 实现模型映射 (`model-map.ts`)
- [ ] 实现 Reasoning 配置 (`reasoning.ts`)
- [ ] 实现请求体转换 (`transformer.ts`)
- [ ] 实现 Header 生成 (`headers.ts`)
- [ ] 实现请求验证 (`validator.ts`)
- [ ] 从 GitHub 下载并内置 Codex prompts
### 阶段 5响应处理第 11-13 天)
- [ ] 实现 SSE 解析器 (`sse-parser.ts`)
- [ ] 实现响应格式转换 (`converter.ts`)
- [ ] 实现 Chat Completions 格式转换器 (`chat-completions.ts`)
- [ ] 实现响应处理器 (`handler.ts`)
- [ ] 实现流式/非流式响应处理
### 阶段 6API 端点(第 14-16 天)
- [ ] 实现 `/v1/chat/completions`
- [ ] 实现 `/v1/responses`
- [ ] 实现 `/auth/login`
- [ ] 实现 `/health` 健康检查端点
- [ ] 测试所有端点
### 阶段 7Docker 支持(第 17-18 天)
- [ ] 创建 Dockerfile
- [ ] 创建 docker-compose.yml
- [ ] 创建 .dockerignore
- [ ] 测试 Docker 构建
- [ ] 编写 Docker 使用文档
### 阶段 8测试和优化第 19-21 天)
- [ ] 单元测试
- [ ] 集成测试
- [ ] 性能测试
- [ ] 日志优化
- [ ] 错误处理优化
### 阶段 9文档和发布第 22-23 天)
- [ ] 编写 README
- [ ] 编写 API 文档
- [ ] 编写 Docker 文档
- [ ] 准备 NPM 发布
---
## 依赖项
### 生产依赖
```json
{
"hono": "^4.10.4",
"@openauthjs/openauth": "^0.4.3",
"dotenv": "^16.4.5"
}
```
### 开发依赖
```json
{
"@types/node": "^24.6.2",
"typescript": "^5.9.3",
"vitest": "^3.2.4",
"eslint": "^9.15.0",
"prettier": "^3.4.2",
"@typescript-eslint/eslint-plugin": "^8.15.0"
}
```
---
## 关键实现细节
### 1. Messages → Input 转换
**OpenAI Chat Completions 格式:**
```json
{
"messages": [
{"role": "user", "content": "Hello"}
]
}
```
**ChatGPT Responses API 格式:**
```json
{
"input": [
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Hello"}]}
]
}
```
**转换逻辑:**
```typescript
function messagesToInput(messages: Message[]): InputItem[] {
return messages.map(msg => ({
type: "message",
role: msg.role,
content: Array.isArray(msg.content)
? msg.content.map(c => ({ type: "input_text", text: c.text }))
: [{ type: "input_text", text: msg.content }]
}));
}
```
### 2. 流式 SSE 转换
**转换逻辑:**
```typescript
async function transformSseStream(
reader: ReadableStreamDefaultReader,
model: string
): ReadableStream {
const encoder = new TextEncoder();
return new ReadableStream({
async start(controller) {
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.substring(6));
const transformed = transformChunk(data, model);
controller.enqueue(encoder.encode(`data: ${JSON.stringify(transformed)}\n\n`));
}
}
}
controller.close();
}
});
}
```
### 3. Token 刷新时机
**刷新策略:**
- 提前 5 分钟刷新(`expires_at - 300000 < Date.now()`
- 每次请求前检查
- 刷新失败时返回 401
- 刷新成功后更新本地存储
---
## 测试计划
### 单元测试
- [ ] OAuth 流程测试
- [ ] Token 存储测试
- [ ] 请求转换测试
- [ ] 响应转换测试
- [ ] SSE 解析测试
### 集成测试
- [ ] 完整 OAuth 流程测试
- [ ] Chat Completions 端点测试(流式/非流式)
- [ ] Responses API 端点测试(流式/非流式)
- [ ] Token 自动刷新测试
- [ ] 错误处理测试
### 手动测试
- [ ] 使用 curl 测试所有端点
- [ ] 使用 OpenAI SDK 测试兼容性
- [ ] 使用不同模型测试
- [ ] Docker 部署测试