mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
refactor: align backend names
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -125,7 +125,7 @@ async fn ensure_default_admin_exists(
|
||||
### 1. 管理员登录
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8080/admin/login \
|
||||
curl -X POST http://127.0.0.1:8080/api/admin/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"password":"admin"}'
|
||||
```
|
||||
@@ -151,7 +151,7 @@ cpr_admin_session=sess_a27875537483420ab7ba6ce4d1f91b5e; Path=/; HttpOnly; SameS
|
||||
### 2. 导入测试账号
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8080/admin/accounts/import \
|
||||
curl -X POST http://127.0.0.1:8080/api/admin/accounts/import \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: cpr_admin_session=sess_a27875537483420ab7ba6ce4d1f91b5e" \
|
||||
-d @accounts-export.json
|
||||
@@ -177,7 +177,7 @@ curl -X POST http://127.0.0.1:8080/admin/accounts/import \
|
||||
#### 3.1 创建 API 密钥
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8080/admin/api-keys \
|
||||
curl -X POST http://127.0.0.1:8080/api/admin/api-keys \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: cpr_admin_session=sess_..." \
|
||||
-d '{"name":"test-key","label":"测试密钥"}'
|
||||
@@ -197,7 +197,7 @@ curl -X POST http://127.0.0.1:8080/admin/api-keys \
|
||||
#### 3.2 刷新模型列表
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8080/admin/refresh-models \
|
||||
curl -X POST http://127.0.0.1:8080/api/admin/refresh-models \
|
||||
-H "Cookie: cpr_admin_session=sess_..."
|
||||
```
|
||||
|
||||
@@ -262,7 +262,7 @@ curl -X POST http://127.0.0.1:8080/v1/chat/completions \
|
||||
由于 `AdminConfig` 添加了新字段,需要修复所有测试文件中的初始化代码。
|
||||
|
||||
**影响的测试文件:**
|
||||
- `tests/admin/*.rs` (6 个文件)
|
||||
- `tests/api/admin/*.rs` (6 个文件)
|
||||
- `tests/codex_serving/*.rs` (2 个文件)
|
||||
- `tests/runtime/startup.rs`
|
||||
- `tests/support/*.rs` (2 个文件)
|
||||
@@ -325,7 +325,7 @@ test result: ok. 49 passed; 0 failed
|
||||
- `tests/**/*.rs` - 修复测试中的 AdminConfig 初始化
|
||||
|
||||
### 不变的模块
|
||||
- `src/admin/auth/service.rs` - 认证逻辑无需变更
|
||||
- `src/api/admin/auth/service.rs` - 认证逻辑无需变更
|
||||
- `src/platform/identity/admin_session.rs` - 密码哈希逻辑无需变更
|
||||
- `src/platform/storage/schema.sql` - 数据库表结构无需变更
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ pub fn capture_set_cookie(&mut self, account_id: &str, raw: &str) {
|
||||
- 添加所有浏览器特征头
|
||||
- 修正 `x-openai-internal-codex-residency` 为 `"us"`
|
||||
|
||||
4. **src/codex/gateway/transport/client.rs**
|
||||
4. **src/codex/gateway/transport/http_client.rs**
|
||||
- 更新导入:`build_codex_headers` → `build_ordered_codex_headers`
|
||||
- 重构 `request_headers()` 使用排序后的头
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
### 2. 核心传输层更新
|
||||
|
||||
#### `src/codex/gateway/transport/client.rs`
|
||||
#### `src/codex/gateway/transport/http_client.rs`
|
||||
- `CodexRequestContext` 添加两个新字段:
|
||||
- `installation_id: Option<&'a str>`
|
||||
- `session_id: Option<&'a str>`
|
||||
@@ -209,10 +209,10 @@ Dispatch Service (src/codex/serving/dispatch/mod.rs)
|
||||
|
||||
### 修改文件 (9)
|
||||
- `src/codex/gateway/mod.rs` (+2 行)
|
||||
- `src/codex/gateway/transport/client.rs` (+4 行)
|
||||
- `src/codex/gateway/transport/http_client.rs` (+4 行)
|
||||
- `src/codex/serving/dispatch/mod.rs` (+15 行)
|
||||
- `src/codex/accounts/service/health.rs` (+2 行)
|
||||
- `src/codex/accounts/models/service.rs` (+2 行)
|
||||
- `src/codex/models/service.rs` (+2 行)
|
||||
- `src/codex/serving/diagnostics.rs` (+2 行)
|
||||
- `Cargo.toml` (+1 行)
|
||||
- `tests/codex_gateway/client.rs` (+2 行)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
Rust 状态:
|
||||
- 到位:`src/codex/gateway/transport/headers.rs` 构造账号、residency、request id、UA、sec-ch、默认 header 和顺序。
|
||||
- 到位:`src/codex/gateway/transport/client.rs` 将 installation/window/turn/parent/subagent 同步到 header 和 `client_metadata`。
|
||||
- 到位:`src/codex/gateway/transport/http_client.rs` 将 installation/window/turn/parent/subagent 同步到 header 和 `client_metadata`。
|
||||
- 到位:`src/codex/gateway/installation.rs` 兼容读取 `~/.codex/installation_id`,否则落库生成。
|
||||
- 到位:`src/runtime/bootstrap.rs` 从数据库加载自动更新指纹,避免实际请求继续使用硬编码默认值。
|
||||
- 边界:Rust 使用 `reqwest + rustls` 传输,未声称复制原版 native transport 的完整 Chrome TLS 指纹。IP 代理/VPN 相关能力不属于移植范围。
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
- 使用 SHA256 哈希生成确定性标识符
|
||||
- 格式:`cp_{hash[..32]}` (conversation) 或 `cw_{hash[..32]}` (window)
|
||||
|
||||
- 更新 `src/codex/gateway/transport/client.rs`
|
||||
- 更新 `src/codex/gateway/transport/http_client.rs`
|
||||
- 在 `CodexRequestContext` 添加 `installation_id` 和 `session_id` 字段
|
||||
- 在 `request_headers()` 方法中添加这两个 header
|
||||
|
||||
@@ -132,10 +132,10 @@ test codex::gateway::installation::tests::test_generate_and_persist ... ok
|
||||
|
||||
### 修改文件
|
||||
1. `src/codex/gateway/mod.rs` - 导出新模块
|
||||
2. `src/codex/gateway/transport/client.rs` - 添加新 header 字段
|
||||
2. `src/codex/gateway/transport/http_client.rs` - 添加新 header 字段
|
||||
3. `src/codex/serving/dispatch/mod.rs` - 集成身份派生逻辑
|
||||
4. `src/codex/accounts/service/health.rs` - 更新 context 构造
|
||||
5. `src/codex/accounts/models/service.rs` - 更新 context 构造
|
||||
5. `src/codex/models/service.rs` - 更新 context 构造
|
||||
6. `src/codex/serving/diagnostics.rs` - 更新 context 构造
|
||||
7. `Cargo.toml` - 添加 `dirs = "5.0.1"` 依赖
|
||||
|
||||
|
||||
+27
-25
@@ -1,10 +1,12 @@
|
||||
# API Contract
|
||||
|
||||
`codex-proxy-rs` exposes two API families.
|
||||
`codex-proxy-rs` exposes two API families. The `/admin/*` path is reserved for
|
||||
the future human Web console; machine-readable management endpoints live under
|
||||
`/api/admin/*`.
|
||||
|
||||
## `/v1/*`
|
||||
|
||||
These endpoints are OpenAI-compatible and are authenticated only by enabled local client API keys created through `/admin/api-keys` and sent as `Authorization: Bearer cpr_...`.
|
||||
These endpoints are OpenAI-compatible and are authenticated only by enabled local client API keys created through `/api/admin/api-keys` and sent as `Authorization: Bearer cpr_...`.
|
||||
Responses include an `X-Request-Id` header for tracing, but the body stays OpenAI-compatible and does not include a custom `requestId` field.
|
||||
|
||||
Error body:
|
||||
@@ -159,14 +161,14 @@ Refresh failures are recorded as non-secret `account.refresh` event logs with th
|
||||
|
||||
`previous_response_id` and explicit WebSocket-only requests are rejected until the WebSocket transport is implemented and verified.
|
||||
|
||||
## `/admin/*`
|
||||
## `/api/admin/*`
|
||||
|
||||
Admin endpoints are authenticated only by HttpOnly admin session cookies.
|
||||
Admin JSON uses lower camelCase field names. Every admin response includes an `X-Request-Id` header, and every JSON body includes `requestId`.
|
||||
|
||||
Use real HTTP status codes and body-level frontend codes together. Do not return HTTP `200` for failed requests. The body `code` exists for frontend branching; the HTTP status remains the transport truth.
|
||||
|
||||
### `GET /admin/diagnostics`
|
||||
### `GET /api/admin/diagnostics`
|
||||
|
||||
Returns the same non-secret runtime diagnostics summary as `/debug/diagnostics`, wrapped in the admin envelope and gated by the admin session cookie.
|
||||
|
||||
@@ -193,7 +195,7 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /admin/login`
|
||||
### `POST /api/admin/login`
|
||||
|
||||
Authenticates only with the configured admin password stored in `admin_users`. Client API keys (`Bearer cpr_...`) are ignored by the admin login flow and cannot create admin sessions.
|
||||
|
||||
@@ -226,7 +228,7 @@ Set-Cookie: cpr_admin_session=...; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600
|
||||
|
||||
Invalid password response uses HTTP `401` and body code `40102`.
|
||||
|
||||
### `GET /admin/settings`
|
||||
### `GET /api/admin/settings`
|
||||
|
||||
Returns the in-scope runtime settings visible to the admin UI. This endpoint uses admin sessions, not client API keys.
|
||||
|
||||
@@ -263,7 +265,7 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `PATCH /admin/settings`
|
||||
### `PATCH /api/admin/settings`
|
||||
|
||||
Updates retained settings and writes them to root `local.yaml` as a config overlay. The route requires the admin session cookie. Client API keys and removed TypeScript-era fields are not accepted.
|
||||
|
||||
@@ -297,7 +299,7 @@ Request:
|
||||
}
|
||||
```
|
||||
|
||||
Success response uses the same `data` shape as `GET /admin/settings`.
|
||||
Success response uses the same `data` shape as `GET /api/admin/settings`.
|
||||
|
||||
Validation:
|
||||
|
||||
@@ -306,7 +308,7 @@ Validation:
|
||||
- Quota warning thresholds must be between `0` and `100`.
|
||||
- Removed settings such as `proxyUrl`, `openaiApiKey`, `ollama`, provider settings, proxy API key settings, and Electron/self-update settings return HTTP `400` with body code `40001`.
|
||||
|
||||
### `GET /admin/logs`
|
||||
### `GET /api/admin/logs`
|
||||
|
||||
Returns cursor-paginated event logs for admin troubleshooting. The list uses the standard admin page envelope and lower camelCase fields.
|
||||
|
||||
@@ -348,7 +350,7 @@ Example item:
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /admin/logs/state`
|
||||
### `GET /api/admin/logs/state`
|
||||
|
||||
Returns the current retained logging configuration plus the number of stored SQLite event logs.
|
||||
|
||||
@@ -368,9 +370,9 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `PATCH /admin/logs/state`
|
||||
### `PATCH /api/admin/logs/state`
|
||||
|
||||
Updates the in-process logging state used by admin log controls. This is runtime-only; persistent configuration changes still go through `PATCH /admin/settings`.
|
||||
Updates the in-process logging state used by admin log controls. This is runtime-only; persistent configuration changes still go through `PATCH /api/admin/settings`.
|
||||
|
||||
Request:
|
||||
|
||||
@@ -382,13 +384,13 @@ Request:
|
||||
}
|
||||
```
|
||||
|
||||
Success response uses the same data shape as `GET /admin/logs/state`.
|
||||
Success response uses the same data shape as `GET /api/admin/logs/state`.
|
||||
|
||||
### `GET /admin/logs/{id}`
|
||||
### `GET /api/admin/logs/{id}`
|
||||
|
||||
Returns a single stored event log by id using the same event shape as list items. Missing ids return HTTP `404` with body code `40401`.
|
||||
|
||||
### `DELETE /admin/logs`
|
||||
### `DELETE /api/admin/logs`
|
||||
|
||||
Clears stored SQLite event logs.
|
||||
|
||||
@@ -405,7 +407,7 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /admin/usage-stats`
|
||||
### `GET /api/admin/usage-stats`
|
||||
|
||||
Returns cursor-paginated account usage counters recorded after `/v1/*` calls. The endpoint never returns account access tokens, refresh tokens, Cookie values, or client API keys.
|
||||
|
||||
@@ -425,7 +427,7 @@ Example item:
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /admin/usage-stats/summary`
|
||||
### `GET /api/admin/usage-stats/summary`
|
||||
|
||||
Returns global usage totals across accounts.
|
||||
|
||||
@@ -446,11 +448,11 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /admin/api-keys`
|
||||
### `GET /api/admin/api-keys`
|
||||
|
||||
Returns local client API keys with cursor pagination. The plaintext key and hash are never returned by list endpoints.
|
||||
|
||||
### `POST /admin/api-keys`
|
||||
### `POST /api/admin/api-keys`
|
||||
|
||||
Creates a local client API key for `/v1/*`. The plaintext value is returned only in this response; store it client-side and use it as `Authorization: Bearer <plaintext>`.
|
||||
|
||||
@@ -481,14 +483,14 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /admin/api-keys/export`
|
||||
### `GET /api/admin/api-keys/export`
|
||||
|
||||
Exports Rust-local client API key metadata for backup or migration. The export never includes plaintext keys, key hashes, or the raw auth pepper. Because `cpr_` keys are stored only as peppered HMAC hashes, imports rotate keys instead of preserving the original plaintext.
|
||||
|
||||
Optional query:
|
||||
|
||||
```http
|
||||
GET /admin/api-keys/export?ids=key_01,key_02
|
||||
GET /api/admin/api-keys/export?ids=key_01,key_02
|
||||
```
|
||||
|
||||
Success response:
|
||||
@@ -516,7 +518,7 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /admin/api-keys/import`
|
||||
### `POST /api/admin/api-keys/import`
|
||||
|
||||
Imports Rust-local client API key metadata exported by this service. Import creates new local `cpr_` keys using the target instance's pepper and returns the new plaintext values only in this response. Clients must replace the old keys with these rotated plaintext values.
|
||||
|
||||
@@ -567,11 +569,11 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /admin/accounts`
|
||||
### `GET /api/admin/accounts`
|
||||
|
||||
Returns stored Codex accounts with cursor pagination. Access tokens and refresh tokens are never decrypted or returned.
|
||||
|
||||
### `GET /admin/accounts/quota-warnings`
|
||||
### `GET /api/admin/accounts/quota-warnings`
|
||||
|
||||
Returns active quota warnings computed from cached account quota snapshots in SQLite. The route requires an admin session cookie and does not call the upstream Codex backend.
|
||||
|
||||
@@ -598,7 +600,7 @@ Success response:
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /admin/accounts/import`
|
||||
### `POST /api/admin/accounts/import`
|
||||
|
||||
Imports accounts into encrypted SQLite storage. The request body uses the Rust import format, which accepts the exported account object fields needed by this service and ignores unrelated fields.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
src/
|
||||
├── codex/ # OpenAI/Codex 对接层(核心业务逻辑)
|
||||
├── service/ # 业务服务层(Chat, Responses, Diagnostics)
|
||||
├── http/ # HTTP 路由层(/v1/* 和 /admin/*)
|
||||
├── http/ # HTTP 路由层(/v1/* 和 /api/admin/*)
|
||||
├── auth/ # 认证模块(API keys, sessions)
|
||||
├── scheduler/ # 后台调度器(refresh, quota, model)
|
||||
├── storage/ # 数据持久化(SQLite)
|
||||
@@ -30,7 +30,7 @@ src/
|
||||
|
||||
**三层认证边界设计:**
|
||||
```
|
||||
Admin Auth (密码 + Session) → /admin/*
|
||||
Admin Auth (密码 + Session) → /api/admin/*
|
||||
Client API Keys (cpr_ 前缀) → /v1/*
|
||||
Upstream Tokens (内部加密) → ChatGPT Backend
|
||||
```
|
||||
@@ -151,7 +151,7 @@ src/codex/accounts/service/
|
||||
├── cookies.rs # Cookie 操作
|
||||
├── health.rs # 健康检查
|
||||
├── import.rs # 导入逻辑
|
||||
├── mutation.rs # CRUD 操作
|
||||
├── lifecycle.rs # CRUD 操作
|
||||
├── quota.rs # 配额管理
|
||||
├── refresh.rs # 令牌刷新
|
||||
└── runtime_pool.rs # 运行时池同步
|
||||
@@ -224,7 +224,7 @@ pub enum AccountServiceError {
|
||||
### 5. 🟢 配置热重载未完全实现
|
||||
|
||||
**当前状态:**
|
||||
- `/admin/settings PATCH` 写入 `local.yaml`
|
||||
- `/api/admin/settings PATCH` 写入 `local.yaml`
|
||||
- 更新 `AppState` 中的 config
|
||||
- ⚠️ 但已构造的服务(如调度器)仍使用旧配置
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ src/
|
||||
│ │ │ ├── cookies.rs
|
||||
│ │ │ ├── health.rs
|
||||
│ │ │ ├── import.rs # 手动/CLI/OAuth 导入
|
||||
│ │ │ ├── mutation.rs
|
||||
│ │ │ ├── lifecycle.rs
|
||||
│ │ │ ├── quota.rs
|
||||
│ │ │ ├── refresh.rs
|
||||
│ │ │ └── runtime_pool.rs
|
||||
@@ -136,18 +136,18 @@ src/
|
||||
│
|
||||
├── admin/ # 🔵 后台管理域(控制面)
|
||||
│ ├── mod.rs
|
||||
│ ├── http/ # /admin/* ← http/admin/
|
||||
│ │ ├── mod.rs # ← http/admin/mod.rs
|
||||
│ │ ├── router.rs # ← http/admin/router.rs
|
||||
│ │ ├── response.rs # ← http/admin/response.rs(AdminError envelope)
|
||||
│ │ ├── auth.rs # ← http/admin/auth.rs(login/status/logout)
|
||||
│ │ ├── api_keys.rs # ← http/admin/api_keys.rs
|
||||
│ │ ├── logs.rs # ← http/admin/logs.rs
|
||||
│ │ ├── models.rs # ← http/admin/models.rs(refresh-models)
|
||||
│ │ ├── settings.rs # ← http/admin/settings.rs
|
||||
│ │ ├── usage.rs # ← http/admin/usage.rs
|
||||
│ │ ├── diagnostics.rs # ← http/admin/diagnostics.rs
|
||||
│ │ └── accounts/ # ⭐ 拆分 http/admin/accounts.rs(1697 行)
|
||||
│ ├── http/ # /api/admin/* ← http/api/admin/
|
||||
│ │ ├── mod.rs # ← http/api/admin/mod.rs
|
||||
│ │ ├── router.rs # ← http/api/admin/router.rs
|
||||
│ │ ├── response.rs # ← http/api/admin/response.rs(AdminError envelope)
|
||||
│ │ ├── auth.rs # ← http/api/admin/auth.rs(login/status/logout)
|
||||
│ │ ├── api_keys.rs # ← http/api/admin/api_keys.rs
|
||||
│ │ ├── logs.rs # ← http/api/admin/logs.rs
|
||||
│ │ ├── models.rs # ← http/api/admin/models.rs(refresh-models)
|
||||
│ │ ├── settings.rs # ← http/api/admin/settings.rs
|
||||
│ │ ├── usage.rs # ← http/api/admin/usage.rs
|
||||
│ │ ├── diagnostics.rs # ← http/api/admin/diagnostics.rs
|
||||
│ │ └── accounts/ # ⭐ 拆分 http/api/admin/accounts.rs(1697 行)
|
||||
│ │ ├── mod.rs # 路由注册 + 共享 DTO
|
||||
│ │ ├── list.rs # accounts / export_accounts
|
||||
│ │ ├── create.rs # create_account / import_accounts / import_cli_auth
|
||||
@@ -156,7 +156,7 @@ src/
|
||||
│ │ ├── health.rs # health_check_accounts
|
||||
│ │ ├── quota.rs # quota_warnings / account_quota
|
||||
│ │ ├── cookies.rs # get / set / delete cookies
|
||||
│ │ └── oauth.rs # ← http/admin/auth.rs 中 OAuth 流程
|
||||
│ │ └── oauth.rs # ← http/api/admin/auth.rs 中 OAuth 流程
|
||||
│ │ # device-login / poll / login-start / code-relay / callback
|
||||
│ ├── auth/ # 管理员认证 + key 管理策略
|
||||
│ │ ├── mod.rs
|
||||
@@ -200,7 +200,7 @@ src/
|
||||
|
||||
大部分是目录搬迁,但有 3 个文件需要真正动刀:
|
||||
|
||||
1. **`http/admin/accounts.rs`(1697 行)→ `admin/http/accounts/` 9 文件**。按上面 16 个 handler 归类(list/create/mutate/delete/health/quota/cookies/oauth + mod 路由)。
|
||||
1. **`http/api/admin/accounts.rs`(1697 行)→ `admin/http/accounts/` 9 文件**。按上面 16 个 handler 归类(list/create/mutate/delete/health/quota/cookies/oauth + mod 路由)。
|
||||
|
||||
2. **`service/admin_auth.rs` 拆责**。密码登录/会话校验留 `admin/auth/service.rs`;OAuth token 交换 + 账号导入的编排移到 `admin/http/accounts/oauth.rs`,由它调用 `codex::gateway::oauth`(换 token)+ `codex::accounts::service::import`(落库)。
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ The cleanup started from these pressure points:
|
||||
| `tests/admin_accounts_route_test.rs` | 2588 | Admin account scenarios are too large to scan or move safely. |
|
||||
| `tests/v1_upstream_route_test.rs` | 2115 | Responses HTTP SSE, WebSocket, fallback, error mapping, usage, and logging side effects are mixed. |
|
||||
| `tests/chat_completions_route_test.rs` | 710 | Manageable, but should eventually align with the v1 split. |
|
||||
| `tests/admin_api_keys_route_test.rs` | 659 | Manageable, but can share common admin test setup. |
|
||||
| `tests/admin_client_keys_route_test.rs` | 659 | Manageable, but can share common admin test setup. |
|
||||
|
||||
The main coupling pattern is:
|
||||
|
||||
@@ -51,8 +51,8 @@ touch fallback, account state, usage logs, and protocol conversion at the same t
|
||||
|
||||
As of this cleanup pass:
|
||||
|
||||
- `src/http/admin/mod.rs` is the admin module entry, backed by resource-specific files
|
||||
under `src/http/admin/` and a dedicated admin router.
|
||||
- `src/http/api/admin/mod.rs` is the admin module entry, backed by resource-specific files
|
||||
under `src/http/api/admin/` and a dedicated admin router.
|
||||
- `src/http/v1/mod.rs` is the v1 module entry, backed by thin route handlers for auth,
|
||||
chat, responses, models, errors, and router mounting under `src/http/v1/`.
|
||||
- `src/service/` now contains backend-facing HTTP use-case services:
|
||||
@@ -78,7 +78,7 @@ As of this cleanup pass:
|
||||
- admin model catalog refresh orchestration lives in `codex::models::service::ModelService`.
|
||||
- `tests/common/` now contains shared integration-test helpers, including generic
|
||||
response/session helpers plus admin account and v1 upstream route fixtures.
|
||||
- `tests/admin_accounts_route_test.rs` has been split into list, mutation,
|
||||
- `tests/admin_accounts_route_test.rs` has been split into list, lifecycle,
|
||||
import/export, OAuth/auth, and cookies/quota/refresh/health scenario files.
|
||||
- `tests/v1_upstream_route_test.rs` has been split into Responses HTTP/SSE,
|
||||
Responses WebSocket, upstream fallback/refresh, and upstream error scenario files.
|
||||
@@ -100,8 +100,8 @@ As of this cleanup pass:
|
||||
- `codex/upstream/dispatch.rs`, `fallback.rs`, `refresh.rs`, `stream.rs`, and
|
||||
`usage.rs` now own the obvious helper groups for dispatch response helpers, retry
|
||||
classification, token refresh retry, SSE response collection, and usage recording.
|
||||
- `codex/accounts/service/import.rs`, `mutation.rs`, `cookies.rs`, `quota.rs`, `health.rs`,
|
||||
`refresh.rs`, and `runtime_pool.rs` now own account import, mutation, cookie, quota,
|
||||
- `codex/accounts/service/import.rs`, `lifecycle.rs`, `cookies.rs`, `quota.rs`, `health.rs`,
|
||||
`refresh.rs`, and `runtime_pool.rs` now own account import, lifecycle, cookie, quota,
|
||||
health-check, single-account refresh, and runtime-pool workflows instead of leaving
|
||||
them in `service/account/mod.rs`.
|
||||
- `ChatService` and `ResponsesService` own a cloned `CodexUpstreamService` instance injected from
|
||||
@@ -128,7 +128,7 @@ As of this cleanup pass:
|
||||
moved out of route tests into `tests/fixtures/*.sse`.
|
||||
- v1 Responses WebSocket mocked response payloads have been moved into
|
||||
`tests/fixtures/*.json`.
|
||||
- `http/admin/response.rs` now provides `AdminError`, and the admin route modules use
|
||||
- `http/api/admin/response.rs` now provides `AdminError`, and the admin route modules use
|
||||
`Result<impl IntoResponse, AdminError>` for standard admin-envelope failures.
|
||||
- admin auth handlers now propagate session, login, OAuth, and PKCE failures through
|
||||
`AdminError`; success paths that must set cookies or redirect still build explicit
|
||||
@@ -210,7 +210,7 @@ and the Rust best-practice rules below.
|
||||
### Async, Sharing, And Trait Objects
|
||||
|
||||
- Shared runtime state should use `Arc` and explicit synchronization primitives such as
|
||||
`tokio::sync::Mutex` only where mutation is required.
|
||||
`tokio::sync::Mutex` only where lifecycle is required.
|
||||
- Keep lock scopes short. Do not hold account-pool or session locks while performing
|
||||
network requests, database calls, or expensive serialization.
|
||||
- Prefer generics/static dispatch for local helper abstractions. Use `Arc<dyn Trait>`
|
||||
@@ -336,7 +336,7 @@ src/
|
||||
service/
|
||||
mod.rs
|
||||
import.rs
|
||||
mutation.rs
|
||||
lifecycle.rs
|
||||
cookies.rs
|
||||
quota.rs
|
||||
health.rs
|
||||
@@ -505,7 +505,7 @@ This phase should be mostly mechanical:
|
||||
Create:
|
||||
|
||||
```text
|
||||
src/http/admin/
|
||||
src/http/api/admin/
|
||||
mod.rs
|
||||
router.rs
|
||||
response.rs
|
||||
@@ -526,10 +526,10 @@ Suggested ownership:
|
||||
| `auth.rs` | `login`, `auth_status`, `auth_logout`, OAuth PKCE/device routes, admin session cookie helpers. |
|
||||
| `accounts.rs` | account list/create/import/export/delete/status/label, health check, refresh, reset usage, quota, cookies, account import parsing helpers. |
|
||||
| `api_keys.rs` | local client key list/create/import/export/delete/status/label/batch helpers. |
|
||||
| `logs.rs` | `/admin/logs` handler and log DTOs. |
|
||||
| `models.rs` | `/admin/refresh-models` handler and model refresh DTOs. |
|
||||
| `settings.rs` | `/admin/settings` handler and settings DTOs. |
|
||||
| `usage.rs` | `/admin/usage-stats` and `/admin/usage-stats/summary`. |
|
||||
| `logs.rs` | `/api/admin/logs` handler and log DTOs. |
|
||||
| `models.rs` | `/api/admin/refresh-models` handler and model refresh DTOs. |
|
||||
| `settings.rs` | `/api/admin/settings` handler and settings DTOs. |
|
||||
| `usage.rs` | `/api/admin/usage-stats` and `/api/admin/usage-stats/summary`. |
|
||||
| `router.rs` | admin route mounting only. |
|
||||
|
||||
Keep cross-resource helpers private when possible. If a helper is needed by multiple
|
||||
@@ -540,10 +540,10 @@ The admin router should own admin paths:
|
||||
```rust
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/admin/login", post(auth::login))
|
||||
.route("/admin/auth/status", get(auth::auth_status))
|
||||
.route("/admin/accounts", get(accounts::accounts).post(accounts::create_account))
|
||||
.route("/admin/api-keys", get(api_keys::api_keys).post(api_keys::create_api_key))
|
||||
.route("/api/admin/login", post(auth::login))
|
||||
.route("/api/admin/auth/status", get(auth::auth_status))
|
||||
.route("/api/admin/accounts", get(accounts::accounts).post(accounts::create_account))
|
||||
.route("/api/admin/api-keys", get(api_keys::api_keys).post(api_keys::create_api_key))
|
||||
}
|
||||
```
|
||||
|
||||
@@ -600,7 +600,7 @@ cargo test --test admin_accounts_mutation_test
|
||||
cargo test --test admin_accounts_import_export_test
|
||||
cargo test --test admin_accounts_oauth_test
|
||||
cargo test --test admin_accounts_cookies_quota_test
|
||||
cargo test --test admin_api_keys_route_test
|
||||
cargo test --test admin_client_keys_route_test
|
||||
cargo test --test v1_responses_http_sse_test
|
||||
cargo test --test v1_responses_websocket_test
|
||||
cargo test --test v1_upstream_fallback_test
|
||||
@@ -758,7 +758,7 @@ Targets:
|
||||
```text
|
||||
src/codex/protocol/error.rs
|
||||
src/http/response.rs
|
||||
src/http/admin/response.rs
|
||||
src/http/api/admin/response.rs
|
||||
src/http/v1/errors.rs
|
||||
```
|
||||
|
||||
@@ -990,7 +990,7 @@ cargo test --test v1_upstream_errors_test
|
||||
|
||||
### Step 4: Split Former `src/http/admin.rs`
|
||||
|
||||
Move admin code into `src/http/admin/*.rs`, with `src/http/admin/mod.rs` as the module
|
||||
Move admin code into `src/http/api/admin/*.rs`, with `src/http/api/admin/mod.rs` as the module
|
||||
entry. Preserve behavior.
|
||||
|
||||
Verification:
|
||||
@@ -1002,7 +1002,7 @@ cargo test --test admin_accounts_mutation_test
|
||||
cargo test --test admin_accounts_import_export_test
|
||||
cargo test --test admin_accounts_oauth_test
|
||||
cargo test --test admin_accounts_cookies_quota_test
|
||||
cargo test --test admin_api_keys_route_test
|
||||
cargo test --test admin_client_keys_route_test
|
||||
cargo test --test admin_logs_route_test
|
||||
cargo test --test admin_models_route_test
|
||||
cargo test --test admin_settings_route_test
|
||||
@@ -1090,7 +1090,7 @@ cargo test --test admin_accounts_mutation_test
|
||||
cargo test --test admin_accounts_import_export_test
|
||||
cargo test --test admin_accounts_oauth_test
|
||||
cargo test --test admin_accounts_cookies_quota_test
|
||||
cargo test --test admin_api_keys_route_test
|
||||
cargo test --test admin_client_keys_route_test
|
||||
cargo test --test v1_responses_http_sse_test
|
||||
cargo test --test v1_responses_websocket_test
|
||||
cargo test --test v1_upstream_fallback_test
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
# Backend Naming Audit
|
||||
|
||||
## Verdict
|
||||
|
||||
当前后端命名可以作为第一版完整架构定稿。主边界已经清楚:`runtime` 负责装配,`web` 负责未来 Web 控制台承载,`admin` 负责管理域,`codex` 负责 Codex 业务内核,`platform` 负责跨领域基础设施。
|
||||
|
||||
命名的艺术不在于词汇华丽,而在于每个名字都能回答三个问题:
|
||||
|
||||
- 这个目录属于哪个领域。
|
||||
- 这个文件对外暴露什么责任。
|
||||
- 这个名字是否会让后来的人误以为它还承担了别的职责。
|
||||
|
||||
本轮审计结论:源码目录已经达到“可长期演进”的层级。剩余问题主要是少数兼容性术语和偏大的聚合文件,不影响当前架构定稿。
|
||||
|
||||
## Naming Principles
|
||||
|
||||
- 路由表面用 `api`、`http`、`web` 表达协议和承载面。
|
||||
- 业务能力用领域名表达,例如 `accounts`、`models`、`events`、`gateway`。
|
||||
- 基础设施只放在 `platform`,避免业务目录承担系统能力。
|
||||
- 测试目录跟随生产目录命名,测试文件表达场景而不是实现细节。
|
||||
- 对外兼容词可以保留在 route 和 DTO 中,内部模块优先使用更准确的领域词。
|
||||
|
||||
## Complete Architecture Tree
|
||||
|
||||
以下目录排除了生成物和本地状态:`.git`、`target`、`data`、`logs`、`designs`。
|
||||
|
||||
```text
|
||||
.
|
||||
├── docs
|
||||
│ ├── superpowers
|
||||
│ │ ├── plans
|
||||
│ │ │ ├── 2026-06-11-codex-proxy-rs.md
|
||||
│ │ │ ├── 2026-06-12-openai-gpt-codex-parity.md
|
||||
│ │ │ ├── 2026-06-13-utils-module-migration.md
|
||||
│ │ │ ├── 2026-06-14-backend-naming-architecture.md
|
||||
│ │ │ └── 2026-06-14-logging-refactor.md
|
||||
│ │ └── specs
|
||||
│ │ ├── 2026-06-12-openai-gpt-codex-parity-design.md
|
||||
│ │ ├── 2026-06-13-architecture-refactor-design.md
|
||||
│ │ ├── 2026-06-14-backend-naming-architecture-design.md
|
||||
│ │ └── 2026-06-14-logging-refactor-design.md
|
||||
│ ├── ADMIN_INITIALIZATION_FIX.md
|
||||
│ ├── FINGERPRINT_AUTO_UPDATE.md
|
||||
│ ├── FINGERPRINT_FIX.md
|
||||
│ ├── FINGERPRINT_PROPAGATION_FIX.md
|
||||
│ ├── FINGERPRINT_UPDATE_TEST_REPORT.md
|
||||
│ ├── IMPLEMENTATION_COMPARISON.md
|
||||
│ ├── IMPLEMENTATION_COMPLETE.md
|
||||
│ ├── OPENAI_CODEX_PARITY_AUDIT.md
|
||||
│ ├── OPENAI_REQUEST_ALIGNMENT.md
|
||||
│ ├── RELIABILITY_ENHANCEMENTS.md
|
||||
│ ├── RESPONSE_CHAIN_ALIGNMENT.md
|
||||
│ ├── api.md
|
||||
│ ├── architecture-audit.md
|
||||
│ ├── architecture-capability-plan.md
|
||||
│ ├── architecture-reorganization.md
|
||||
│ ├── backend-naming-audit.md
|
||||
│ ├── code-quality-audit.md
|
||||
│ ├── database-storage-audit.md
|
||||
│ ├── dependency-policy.md
|
||||
│ ├── implementation-status.md
|
||||
│ ├── optimization-roadmap.md
|
||||
│ ├── rust-community-review.md
|
||||
│ ├── scheduler-completion-report.md
|
||||
│ ├── scheduler-implementation.md
|
||||
│ ├── scheduler-usage.md
|
||||
│ └── status-codes.md
|
||||
├── src
|
||||
│ ├── admin
|
||||
│ │ ├── api
|
||||
│ │ │ ├── accounts
|
||||
│ │ │ │ ├── cookies.rs
|
||||
│ │ │ │ ├── create.rs
|
||||
│ │ │ │ ├── delete.rs
|
||||
│ │ │ │ ├── export.rs
|
||||
│ │ │ │ ├── health.rs
|
||||
│ │ │ │ ├── import.rs
|
||||
│ │ │ │ ├── lifecycle.rs
|
||||
│ │ │ │ ├── list.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── oauth.rs
|
||||
│ │ │ │ └── quota.rs
|
||||
│ │ │ ├── client_keys
|
||||
│ │ │ │ ├── create.rs
|
||||
│ │ │ │ ├── export.rs
|
||||
│ │ │ │ ├── import.rs
|
||||
│ │ │ │ ├── lifecycle.rs
|
||||
│ │ │ │ ├── list.rs
|
||||
│ │ │ │ └── mod.rs
|
||||
│ │ │ ├── logs
|
||||
│ │ │ │ ├── detail.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── query.rs
|
||||
│ │ │ │ └── state.rs
|
||||
│ │ │ ├── diagnostics.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── models.rs
|
||||
│ │ │ ├── response.rs
|
||||
│ │ │ ├── router.rs
|
||||
│ │ │ ├── session.rs
|
||||
│ │ │ ├── settings.rs
|
||||
│ │ │ └── usage.rs
|
||||
│ │ ├── client_keys
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── service.rs
|
||||
│ │ ├── session
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── repository.rs
|
||||
│ │ │ └── service.rs
|
||||
│ │ ├── tasks
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── session_cleanup.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── settings.rs
|
||||
│ ├── codex
|
||||
│ │ ├── accounts
|
||||
│ │ │ ├── cookies
|
||||
│ │ │ │ ├── jar.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ └── repository.rs
|
||||
│ │ │ ├── repository
|
||||
│ │ │ │ ├── accounts.rs
|
||||
│ │ │ │ ├── leases.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── quotas.rs
|
||||
│ │ │ │ ├── tokens.rs
|
||||
│ │ │ │ └── usage.rs
|
||||
│ │ │ ├── service
|
||||
│ │ │ │ ├── cookies.rs
|
||||
│ │ │ │ ├── health.rs
|
||||
│ │ │ │ ├── import.rs
|
||||
│ │ │ │ ├── lifecycle.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── pool_sync.rs
|
||||
│ │ │ │ ├── quota.rs
|
||||
│ │ │ │ └── refresh.rs
|
||||
│ │ │ ├── cloudflare_challenge.rs
|
||||
│ │ │ ├── jwt.rs
|
||||
│ │ │ ├── lifecycle.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── model.rs
|
||||
│ │ │ ├── pool.rs
|
||||
│ │ │ └── usage_snapshots.rs
|
||||
│ │ ├── events
|
||||
│ │ │ ├── event.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── repository.rs
|
||||
│ │ │ └── service.rs
|
||||
│ │ ├── gateway
|
||||
│ │ │ ├── fingerprint
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── model.rs
|
||||
│ │ │ │ ├── repository.rs
|
||||
│ │ │ │ ├── update_checker.rs
|
||||
│ │ │ │ └── updater.rs
|
||||
│ │ │ ├── oauth
|
||||
│ │ │ │ ├── client.rs
|
||||
│ │ │ │ ├── codex_cli.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── refresh.rs
|
||||
│ │ │ │ └── token.rs
|
||||
│ │ │ ├── protocol
|
||||
│ │ │ │ ├── codex_to_openai.rs
|
||||
│ │ │ │ ├── error.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── openai_to_codex.rs
|
||||
│ │ │ │ └── schema.rs
|
||||
│ │ │ ├── transport
|
||||
│ │ │ │ ├── websocket
|
||||
│ │ │ │ │ ├── codec.rs
|
||||
│ │ │ │ │ ├── mod.rs
|
||||
│ │ │ │ │ └── pool.rs
|
||||
│ │ │ │ ├── headers.rs
|
||||
│ │ │ │ ├── http_client.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── rate_limits.rs
|
||||
│ │ │ │ ├── sse.rs
|
||||
│ │ │ │ ├── types.rs
|
||||
│ │ │ │ └── usage_events.rs
|
||||
│ │ │ ├── conversation_identity.rs
|
||||
│ │ │ ├── installation_id.rs
|
||||
│ │ │ └── mod.rs
|
||||
│ │ ├── models
|
||||
│ │ │ ├── catalog.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── repository.rs
|
||||
│ │ │ └── service.rs
|
||||
│ │ ├── serving
|
||||
│ │ │ ├── dispatch
|
||||
│ │ │ │ ├── account_refresh.rs
|
||||
│ │ │ │ ├── affinity.rs
|
||||
│ │ │ │ ├── fallback.rs
|
||||
│ │ │ │ ├── limits.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── routing.rs
|
||||
│ │ │ │ ├── stream.rs
|
||||
│ │ │ │ ├── stream_audit.rs
|
||||
│ │ │ │ └── usage.rs
|
||||
│ │ │ ├── http
|
||||
│ │ │ │ ├── auth.rs
|
||||
│ │ │ │ ├── chat.rs
|
||||
│ │ │ │ ├── diagnostics.rs
|
||||
│ │ │ │ ├── errors.rs
|
||||
│ │ │ │ ├── mod.rs
|
||||
│ │ │ │ ├── models.rs
|
||||
│ │ │ │ ├── responses.rs
|
||||
│ │ │ │ └── router.rs
|
||||
│ │ │ ├── chat.rs
|
||||
│ │ │ ├── diagnostics.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── responses.rs
|
||||
│ │ ├── tasks
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── model_refresh.rs
|
||||
│ │ │ ├── quota_refresh.rs
|
||||
│ │ │ └── token_refresh.rs
|
||||
│ │ ├── usage
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── service.rs
|
||||
│ │ └── mod.rs
|
||||
│ ├── config
|
||||
│ │ ├── loader.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── types.rs
|
||||
│ ├── platform
|
||||
│ │ ├── crypto
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── secret_box.rs
|
||||
│ │ ├── http
|
||||
│ │ │ ├── auth.rs
|
||||
│ │ │ ├── health.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── request_id.rs
|
||||
│ │ ├── identity
|
||||
│ │ │ ├── admin_session.rs
|
||||
│ │ │ ├── client_key.rs
|
||||
│ │ │ ├── client_key_repository.rs
|
||||
│ │ │ ├── error.rs
|
||||
│ │ │ └── mod.rs
|
||||
│ │ ├── logging
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── rotation.rs
|
||||
│ │ ├── storage
|
||||
│ │ │ ├── db.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── paths.rs
|
||||
│ │ │ └── schema.sql
|
||||
│ │ └── mod.rs
|
||||
│ ├── runtime
|
||||
│ │ ├── tasks
|
||||
│ │ │ ├── coordinator.rs
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── types.rs
|
||||
│ │ ├── bootstrap.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── router.rs
|
||||
│ │ └── state.rs
|
||||
│ ├── utils
|
||||
│ │ ├── json.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── pagination.rs
|
||||
│ ├── web
|
||||
│ │ ├── assets.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── router.rs
|
||||
│ │ ├── security.rs
|
||||
│ │ └── shell.rs
|
||||
│ ├── lib.rs
|
||||
│ └── main.rs
|
||||
├── tests
|
||||
│ ├── admin
|
||||
│ │ ├── accounts
|
||||
│ │ │ ├── cookies_quota.rs
|
||||
│ │ │ ├── import_export.rs
|
||||
│ │ │ ├── lifecycle.rs
|
||||
│ │ │ ├── list.rs
|
||||
│ │ │ └── oauth.rs
|
||||
│ │ ├── api_contract.rs
|
||||
│ │ ├── client_keys_route.rs
|
||||
│ │ ├── logs_route.rs
|
||||
│ │ ├── models_route.rs
|
||||
│ │ ├── session.rs
|
||||
│ │ ├── session_login_route.rs
|
||||
│ │ ├── session_repository.rs
|
||||
│ │ ├── settings_route.rs
|
||||
│ │ └── usage_stats_route.rs
|
||||
│ ├── architecture
|
||||
│ │ ├── accounts_boundary.rs
|
||||
│ │ ├── admin_accounts_split.rs
|
||||
│ │ ├── admin_boundary.rs
|
||||
│ │ ├── codex_activity_boundary.rs
|
||||
│ │ ├── gateway_boundary.rs
|
||||
│ │ ├── platform_boundary.rs
|
||||
│ │ ├── runtime_tasks_boundary.rs
|
||||
│ │ └── serving_boundary.rs
|
||||
│ ├── codex_accounts
|
||||
│ │ ├── cookie_store.rs
|
||||
│ │ ├── pool_scheduling.rs
|
||||
│ │ ├── refresh.rs
|
||||
│ │ ├── refresh_scheduler.rs
|
||||
│ │ ├── repository.rs
|
||||
│ │ └── service_refresh.rs
|
||||
│ ├── codex_events
|
||||
│ │ └── logs_pagination.rs
|
||||
│ ├── codex_gateway
|
||||
│ │ ├── websocket
|
||||
│ │ │ └── pool.rs
|
||||
│ │ ├── cli_auth_import.rs
|
||||
│ │ ├── fingerprint_update.rs
|
||||
│ │ ├── headers.rs
|
||||
│ │ ├── http_client.rs
|
||||
│ │ ├── oauth_refresh.rs
|
||||
│ │ ├── usage_events.rs
|
||||
│ │ └── websocket.rs
|
||||
│ ├── codex_models
|
||||
│ │ └── catalog.rs
|
||||
│ ├── codex_serving
|
||||
│ │ ├── chat_completions.rs
|
||||
│ │ ├── diagnostics_route.rs
|
||||
│ │ ├── responses_http_sse.rs
|
||||
│ │ ├── responses_websocket.rs
|
||||
│ │ ├── routes_chat.rs
|
||||
│ │ ├── routes_responses.rs
|
||||
│ │ ├── upstream_errors.rs
|
||||
│ │ └── upstream_fallback.rs
|
||||
│ ├── fixtures
|
||||
│ │ ├── chat
|
||||
│ │ │ ├── non_stream_text.sse
|
||||
│ │ │ ├── parallel_tools_success.sse
|
||||
│ │ │ ├── retry_after_success.sse
|
||||
│ │ │ ├── stream_text.sse
|
||||
│ │ │ ├── tool_reasoning_complete.sse
|
||||
│ │ │ └── tool_stream.sse
|
||||
│ │ └── responses
|
||||
│ │ ├── http_sse
|
||||
│ │ │ ├── after_402.sse
|
||||
│ │ │ ├── after_403.sse
|
||||
│ │ │ ├── after_429.sse
|
||||
│ │ │ ├── after_cloudflare_403.sse
|
||||
│ │ │ ├── after_refresh.sse
|
||||
│ │ │ ├── completed_fields.sse
|
||||
│ │ │ ├── completed_reasoning_include.sse
|
||||
│ │ │ ├── completed_usage.sse
|
||||
│ │ │ ├── default_stream.sse
|
||||
│ │ │ ├── done_item_completed.sse
|
||||
│ │ │ ├── error_event.sse
|
||||
│ │ │ ├── failed_event.sse
|
||||
│ │ │ ├── stream_after_429.sse
|
||||
│ │ │ ├── stream_after_refresh.sse
|
||||
│ │ │ ├── stream_error_event.sse
|
||||
│ │ │ ├── stream_failed_event.sse
|
||||
│ │ │ ├── stream_usage.sse
|
||||
│ │ │ └── text_deltas_completed.sse
|
||||
│ │ └── websocket
|
||||
│ │ ├── completed.json
|
||||
│ │ ├── first_account_limited.json
|
||||
│ │ ├── history_rate_limited.json
|
||||
│ │ ├── rate_limited.json
|
||||
│ │ ├── second_account_limited.json
|
||||
│ │ └── token_revoked.json
|
||||
│ ├── platform
|
||||
│ │ ├── client_key_auth.rs
|
||||
│ │ ├── crypto.rs
|
||||
│ │ ├── http_auth.rs
|
||||
│ │ ├── log_rotation.rs
|
||||
│ │ └── storage_schema.rs
|
||||
│ ├── runtime
|
||||
│ │ ├── http_trace.rs
|
||||
│ │ └── startup.rs
|
||||
│ ├── support
|
||||
│ │ ├── admin_accounts.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── upstream.rs
|
||||
│ ├── admin.rs
|
||||
│ ├── architecture.rs
|
||||
│ ├── codex_accounts.rs
|
||||
│ ├── codex_events.rs
|
||||
│ ├── codex_gateway.rs
|
||||
│ ├── codex_models.rs
|
||||
│ ├── codex_serving.rs
|
||||
│ ├── config.rs
|
||||
│ ├── platform.rs
|
||||
│ └── runtime.rs
|
||||
├── .gitignore
|
||||
├── Cargo.lock
|
||||
├── Cargo.toml
|
||||
├── Dockerfile
|
||||
├── README.md
|
||||
├── config.yaml
|
||||
├── docker-compose.yml
|
||||
└── rust-toolchain.toml
|
||||
```
|
||||
|
||||
## Domain Audit
|
||||
|
||||
| Area | Verdict | Notes |
|
||||
| --- | --- | --- |
|
||||
| `src/runtime` | Good | `bootstrap`、`router`、`state`、`tasks` 表达装配层职责,适合单 crate 起步阶段。 |
|
||||
| `src/web` | Good | 只作为未来 Admin Web 控制台的后端挂载面,没有前端历史包袱。 |
|
||||
| `src/admin/api` | Good | 管理 JSON API 从 `http` 收束为 `api`,比协议名更能表达契约层。 |
|
||||
| `src/admin/session` | Good | 管理员登录态与 `client_keys` 拆开,避免 `auth` 泛化。 |
|
||||
| `src/admin/client_keys` | Good | 内部语义是本地客户端凭据,不是上游 provider key。 |
|
||||
| `src/codex/accounts` | Good | 账号、cookies、仓储、服务、调度池都在账号域内。 |
|
||||
| `src/codex/models` | Good | model catalog 从账号域剥离,边界正确。 |
|
||||
| `src/codex/events` | Good | 业务事件从进程日志中独立出来,命名比 `logs` 更准确。 |
|
||||
| `src/codex/gateway` | Good | `protocol`、`transport`、`oauth`、`fingerprint` 是上游链路的自然分层。 |
|
||||
| `src/codex/serving` | Good | 对外 OpenAI-compatible 服务面与上游 gateway 分离。 |
|
||||
| `src/platform` | Good | `crypto`、`http`、`identity`、`logging`、`storage` 都是跨领域能力。 |
|
||||
| `src/config` | Good | 配置加载与类型独立,暂不需要进一步艺术化命名。 |
|
||||
| `src/utils` | Acceptable | 当前只有 `json` 和 `pagination`,还没变成杂物间;后续如果扩张,应优先迁回领域目录。 |
|
||||
|
||||
## File Naming Audit
|
||||
|
||||
### Names To Keep
|
||||
|
||||
- `admin/api/accounts/lifecycle.rs`:覆盖状态、标签、删除、批量变更,比 `mutate.rs` 更准确。
|
||||
- `admin/api/client_keys/*`:内部使用 `client_keys` 是正确选择,路由保留 `/api-keys` 是对外兼容。
|
||||
- `codex/accounts/cloudflare_challenge.rs`:比 `cf_path_block.rs` 明确,表达真实安全链路问题。
|
||||
- `codex/accounts/service/pool_sync.rs`:表达账号仓储与运行时池同步,比 `runtime_pool.rs` 更有边界。
|
||||
- `codex/gateway/conversation_identity.rs`:比泛化 `identity.rs` 更准确。
|
||||
- `codex/gateway/installation_id.rs`:表达 ChatGPT/Codex 安装标识,不混入一般 installation 概念。
|
||||
- `codex/gateway/transport/http_client.rs`:比 `client.rs` 清楚,因为 transport 下还有 WebSocket。
|
||||
- `codex/gateway/transport/usage_events.rs`:表达从上游事件流中提取 usage,而不是通用 usage 领域。
|
||||
- `codex/tasks/{model_refresh,quota_refresh,token_refresh}.rs`:任务以动作命名,职责清晰。
|
||||
- `platform/http/request_id.rs`:比 `middleware.rs` 精确。
|
||||
- `platform/crypto/secret_box.rs`:表达加密封装能力,适合稳定复用。
|
||||
|
||||
### Names That Are Acceptable But Can Be Refined Later
|
||||
|
||||
- `src/codex/accounts/model.rs`:目前承载 `Account` 和 `AccountStatus`,与 `codex/models` 有轻微视觉冲突。若继续追求极致,可改成 `account.rs`,让 `accounts/account.rs` 与 `models/catalog.rs` 完全错开。
|
||||
- `src/admin/api/client_keys/*` 内部 handler 名仍使用 `api_key`,例如 `create_api_key`。这是对外路由 `/api/admin/api-keys` 的兼容词,不是架构错误。若后续精修,可把 handler 改为 `create_client_key`,DTO 保留 `ClientApiKey*`。
|
||||
- `tests/codex_events/logs_pagination.rs`:目录已经叫 `codex_events`,文件还叫 `logs_pagination`。如果按领域纯度,可改为 `event_log_pagination.rs`。
|
||||
- `tests/admin/*_route.rs`:测试文件显式带 `route`,表达 HTTP 路由测试,可以保留。若未来测试分层更细,可迁到 `tests/admin/api/*`。
|
||||
- `src/codex/gateway/transport/types.rs`:`types` 是弱名,但当前范围在 `transport` 下仍可接受;如果继续增长,应按 request/response/error 拆出更具体文件。
|
||||
|
||||
### Names To Avoid Reintroducing
|
||||
|
||||
- `admin/http`:会把管理 API 误导为协议工具层。
|
||||
- `admin/auth`:同时包含 session 和 client key 时语义过宽。
|
||||
- `codex/accounts/models`:会让 model catalog 看起来像账号私有数据。
|
||||
- `codex/logs`:会混淆业务事件和进程日志。
|
||||
- `platform/http/middleware.rs`:middleware 是技术位置,不是责任名。
|
||||
- `gateway/identity.rs`:identity 太泛,会掩盖 conversation identity 的具体含义。
|
||||
- `transport/client.rs`:在有 HTTP、SSE、WebSocket 多传输时过于笼统。
|
||||
|
||||
## Size And Organization Audit
|
||||
|
||||
命名已经过关,但少数文件仍偏大,后续可在不改变逻辑的前提下继续整理:
|
||||
|
||||
- `src/codex/serving/dispatch/mod.rs`:约 1000 行,是下一轮最值得拆的聚合点。建议按 request context、response assembly、fallback decision、event recording 继续切分。
|
||||
- `src/codex/gateway/transport/http_client.rs`:约 780 行。可拆为 request building、response parsing、model fetching、probe 四块。
|
||||
- `src/codex/accounts/repository/accounts.rs`:约 700 行。可按 load/list/write/status 或 token metadata 继续切分。
|
||||
- `src/codex/accounts/pool.rs`、`src/codex/serving/dispatch/affinity.rs`、`src/codex/gateway/transport/websocket/*` 当前偏大但仍有领域凝聚力,暂不强拆。
|
||||
|
||||
这些不是命名阻断项。继续优化时应遵守“只拆大文件,不改逻辑”,每次拆分后跑 `cargo fmt --check`、`cargo test --locked`、`cargo clippy --all-targets --all-features --locked -- -D warnings`。
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
当前架构可以定为:
|
||||
|
||||
```text
|
||||
runtime -> web/admin/codex/platform/config/utils
|
||||
admin -> api/session/client_keys/tasks/settings
|
||||
codex -> accounts/models/gateway/serving/events/usage/tasks
|
||||
platform-> crypto/http/identity/logging/storage
|
||||
```
|
||||
|
||||
这是一个适合项目起步阶段的完整体:没有过度抽象,没有强行 workspace 化,也没有把 Web 前端塞进后端。后续前端可以独立演进,后端只保留 `src/web` 作为运行时挂载点。
|
||||
@@ -64,7 +64,7 @@ Claude 审计中“窗口字段未持久化”的结论成立,但“配额控
|
||||
|
||||
- v1 response 和 401 后账户刷新失败事件通过 `LogService::record` 写入。
|
||||
- 在一个入口执行 `enabled`、`capacity`、`capture_body` 和 metadata 规范。
|
||||
- 管理端 `/admin/logs/state` 和请求链路共享同一个 `LogService` 实例,运行时开关立即生效。
|
||||
- 管理端 `/api/admin/logs/state` 和请求链路共享同一个 `LogService` 实例,运行时开关立即生效。
|
||||
|
||||
### P2:收敛 schema 约束(已落地)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@
|
||||
|
||||
**问题:**
|
||||
```
|
||||
1697 行 - src/http/admin/accounts.rs ⚠️ 太大!
|
||||
1697 行 - src/http/api/admin/accounts.rs ⚠️ 太大!
|
||||
895 行 - src/codex/upstream/mod.rs
|
||||
784 行 - src/codex/accounts/repository.rs
|
||||
```
|
||||
@@ -46,9 +46,9 @@
|
||||
|
||||
**改进方案:**
|
||||
```rust
|
||||
// 当前:src/http/admin/accounts.rs (1697 行)
|
||||
// 当前:src/http/api/admin/accounts.rs (1697 行)
|
||||
// 建议拆分为:
|
||||
src/http/admin/accounts/
|
||||
src/http/api/admin/accounts/
|
||||
├── mod.rs // 路由注册
|
||||
├── list.rs // GET /accounts
|
||||
├── create.rs // POST /accounts
|
||||
@@ -138,7 +138,7 @@ src/codex/accounts/service/
|
||||
├── cookies.rs // Cookie 操作
|
||||
├── health.rs // 健康检查
|
||||
├── import.rs // 导入逻辑
|
||||
├── mutation.rs // CRUD
|
||||
├── lifecycle.rs // CRUD
|
||||
├── quota.rs // 配额
|
||||
├── refresh.rs // 刷新
|
||||
└── runtime_pool.rs // 池同步
|
||||
@@ -397,7 +397,7 @@ pub account_pool: Arc<RwLock<AccountPool>>,
|
||||
- 风险:低
|
||||
|
||||
2. **🟡 拆分大文件**
|
||||
- 先拆 `http/admin/accounts.rs` (1697 行)
|
||||
- 先拆 `http/api/admin/accounts.rs` (1697 行)
|
||||
- 按功能模块拆分(list/create/update/delete/oauth)
|
||||
- 成本:中等
|
||||
- 收益:提升可维护性
|
||||
|
||||
@@ -12,9 +12,9 @@ HTTP status codes are not replaced by body codes. Admin/frontend APIs return bot
|
||||
|
||||
| HTTP status | Body code range | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `200` | `200` | Successful read, login, or mutation with response body. |
|
||||
| `200` | `200` | Successful read, login, or lifecycle with response body. |
|
||||
| `201` | `201` | Resource created. |
|
||||
| `204` | `204` | Successful mutation with no response body. No JSON envelope is sent. |
|
||||
| `204` | `204` | Successful lifecycle with no response body. No JSON envelope is sent. |
|
||||
| `400` | `40000`-`40099` | Malformed JSON, invalid parameters, or invalid cursor. |
|
||||
| `401` | `40100`-`40199` | Missing/expired admin session or bad admin password. |
|
||||
| `403` | `40300`-`40399` | Authenticated admin lacks permission for a local-only/bootstrap action. |
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# Backend Naming Architecture Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Rename backend modules so every directory and file has one clear domain meaning while reserving `/admin` for the future Web console.
|
||||
|
||||
**Architecture:** Move the current management JSON handlers from `admin/http` to `admin/api`, keep `/v1/*` under `codex/serving`, add a `web` backend shell for future static assets, and split ambiguous support modules into domain-specific names. The work is mechanical and must not change upstream Codex behavior.
|
||||
|
||||
**Tech Stack:** Rust 2021, Axum, SQLx, Tokio, tracing, existing integration tests.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
Create:
|
||||
- `src/web/mod.rs`
|
||||
- `src/web/router.rs`
|
||||
- `src/web/assets.rs`
|
||||
- `src/web/shell.rs`
|
||||
- `src/web/security.rs`
|
||||
- `src/platform/crypto/mod.rs`
|
||||
- `src/platform/crypto/secret_box.rs`
|
||||
- `src/platform/logging/mod.rs`
|
||||
- `src/admin/api/client_keys/mod.rs`
|
||||
- `src/admin/api/logs/mod.rs`
|
||||
- `src/admin/session/mod.rs`
|
||||
- `src/admin/client_keys/mod.rs`
|
||||
- `src/codex/events/mod.rs`
|
||||
- `src/codex/models/mod.rs`
|
||||
|
||||
Move or rename:
|
||||
- `src/admin/http` to `src/admin/api`
|
||||
- `src/admin/auth/repository.rs` to `src/admin/session/repository.rs`
|
||||
- `src/admin/auth/service.rs` to `src/admin/session/service.rs`
|
||||
- `src/admin/auth/api_key.rs` to `src/admin/client_keys/service.rs`
|
||||
- `src/admin/http/api_keys.rs` to `src/admin/api/client_keys/mod.rs`
|
||||
- `src/admin/http/auth.rs` to `src/admin/api/session.rs`
|
||||
- `src/admin/http/logs.rs` to `src/admin/api/logs/mod.rs`
|
||||
- `src/admin/http/accounts/mutate.rs` to `src/admin/api/accounts/lifecycle.rs`
|
||||
- `src/codex/accounts/cf_path_block.rs` to `src/codex/accounts/cloudflare_challenge.rs`
|
||||
- `src/codex/accounts/models` to `src/codex/models`
|
||||
- `src/codex/accounts/repository/lease.rs` to `src/codex/accounts/repository/leases.rs`
|
||||
- `src/codex/accounts/repository/quota.rs` to `src/codex/accounts/repository/quotas.rs`
|
||||
- `src/codex/accounts/repository/token.rs` to `src/codex/accounts/repository/tokens.rs`
|
||||
- `src/codex/accounts/service/mutation.rs` to `src/codex/accounts/service/lifecycle.rs`
|
||||
- `src/codex/accounts/service/runtime_pool.rs` to `src/codex/accounts/service/pool_sync.rs`
|
||||
- `src/codex/gateway/identity.rs` to `src/codex/gateway/conversation_identity.rs`
|
||||
- `src/codex/gateway/installation.rs` to `src/codex/gateway/installation_id.rs`
|
||||
- `src/codex/gateway/oauth/cli_import.rs` to `src/codex/gateway/oauth/codex_cli.rs`
|
||||
- `src/codex/gateway/transport/client.rs` to `src/codex/gateway/transport/http_client.rs`
|
||||
- `src/codex/gateway/transport/usage.rs` to `src/codex/gateway/transport/usage_events.rs`
|
||||
- `src/codex/logs` to `src/codex/events`
|
||||
- `src/codex/logs/rotation.rs` to `src/platform/logging/rotation.rs`
|
||||
- `src/codex/tasks/model.rs` to `src/codex/tasks/model_refresh.rs`
|
||||
- `src/codex/tasks/quota.rs` to `src/codex/tasks/quota_refresh.rs`
|
||||
- `src/codex/tasks/refresh.rs` to `src/codex/tasks/token_refresh.rs`
|
||||
- `src/codex/serving/dispatch/audit.rs` to `src/codex/serving/dispatch/stream_audit.rs`
|
||||
- `src/codex/serving/dispatch/refresh.rs` to `src/codex/serving/dispatch/account_refresh.rs`
|
||||
- `src/platform/crypto.rs` to `src/platform/crypto/secret_box.rs`
|
||||
- `src/platform/http/middleware.rs` to `src/platform/http/request_id.rs`
|
||||
- `src/platform/identity/api_key.rs` to `src/platform/identity/client_key.rs`
|
||||
- `src/platform/identity/api_key_repository.rs` to `src/platform/identity/client_key_repository.rs`
|
||||
|
||||
## Task 1: Freeze the naming contract
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/superpowers/specs/2026-06-14-backend-naming-architecture-design.md`
|
||||
- Create: `docs/superpowers/plans/2026-06-14-backend-naming-architecture.md`
|
||||
|
||||
- [x] **Step 1: Write the design spec**
|
||||
|
||||
Record route boundaries, target source tree, naming decisions, and migration policy.
|
||||
|
||||
- [x] **Step 2: Write this implementation plan**
|
||||
|
||||
List every file move and the verification gates.
|
||||
|
||||
## Task 2: Move management API and backend Web shell
|
||||
|
||||
**Files:**
|
||||
- Move: `src/admin/http/**` to `src/admin/api/**`
|
||||
- Create: `src/web/{mod.rs,router.rs,assets.rs,shell.rs,security.rs}`
|
||||
- Modify: `src/admin/mod.rs`
|
||||
- Modify: `src/runtime/router.rs`
|
||||
- Test: `tests/architecture/admin_boundary.rs`
|
||||
|
||||
- [x] **Step 1: Move the management API directory**
|
||||
|
||||
Run file moves so handlers live under `admin::api`.
|
||||
|
||||
- [x] **Step 2: Update admin module exports**
|
||||
|
||||
Expose `pub mod api;`, `pub mod session;`, and `pub mod client_keys;`.
|
||||
|
||||
- [x] **Step 3: Add empty Web shell modules**
|
||||
|
||||
Add `src/web` modules with module-level documentation only. Do not serve frontend assets yet.
|
||||
|
||||
- [x] **Step 4: Update route mounting**
|
||||
|
||||
Import `crate::admin::api as admin_api` in `runtime/router.rs` and merge `admin_api::router()`.
|
||||
|
||||
## Task 3: Rename session and client key domains
|
||||
|
||||
**Files:**
|
||||
- Move: `src/admin/auth/service.rs` to `src/admin/session/service.rs`
|
||||
- Move: `src/admin/auth/repository.rs` to `src/admin/session/repository.rs`
|
||||
- Move: `src/admin/auth/api_key.rs` to `src/admin/client_keys/service.rs`
|
||||
- Move: `src/admin/api/api_keys.rs` to `src/admin/api/client_keys/mod.rs`
|
||||
- Move: `src/admin/api/auth.rs` to `src/admin/api/session.rs`
|
||||
- Modify: `src/runtime/state.rs`
|
||||
- Modify: `src/runtime/bootstrap.rs`
|
||||
- Test: `tests/architecture/admin_boundary.rs`
|
||||
|
||||
- [x] **Step 1: Move files into the new domains**
|
||||
|
||||
Keep type names stable unless a rename is required by compiler errors.
|
||||
|
||||
- [x] **Step 2: Replace imports**
|
||||
|
||||
Replace `admin::auth::service` with `admin::session::service`, `admin::auth::repository` with `admin::session::repository`, and `admin::auth::api_key` with `admin::client_keys::service`.
|
||||
|
||||
- [x] **Step 3: Update API router imports**
|
||||
|
||||
Import the API key handlers from `admin::api::client_keys` and session handlers from `admin::api::session`.
|
||||
|
||||
## Task 4: Rename Codex catalog, events, gateway, transport, and task modules
|
||||
|
||||
**Files:**
|
||||
- Move the Codex files listed in the File Structure section.
|
||||
- Modify: `src/codex/mod.rs`
|
||||
- Modify: `src/codex/accounts/mod.rs`
|
||||
- Modify: `src/codex/accounts/repository.rs`
|
||||
- Modify: `src/codex/accounts/service/mod.rs`
|
||||
- Modify: `src/codex/gateway/mod.rs`
|
||||
- Modify: `src/codex/gateway/oauth/mod.rs`
|
||||
- Modify: `src/codex/gateway/transport/mod.rs`
|
||||
- Modify: `src/codex/tasks/mod.rs`
|
||||
- Modify: `src/codex/serving/dispatch/mod.rs`
|
||||
- Test: `tests/architecture/{accounts_boundary.rs,gateway_boundary.rs,serving_boundary.rs}`
|
||||
|
||||
- [x] **Step 1: Move model catalog to `codex/models`**
|
||||
|
||||
Update references from `codex::accounts::models` to `codex::models`.
|
||||
|
||||
- [x] **Step 2: Move business logs to `codex/events`**
|
||||
|
||||
Update references from `codex::logs` to `codex::events`.
|
||||
|
||||
- [x] **Step 3: Move gateway and transport files**
|
||||
|
||||
Update `identity`, `installation`, `cli_import`, `client`, and `usage` references to the new names.
|
||||
|
||||
- [x] **Step 4: Move task and dispatch helper names**
|
||||
|
||||
Update `model`, `quota`, `refresh`, `audit`, and dispatch refresh references to the new names.
|
||||
|
||||
## Task 5: Rename platform foundations
|
||||
|
||||
**Files:**
|
||||
- Move: `src/platform/crypto.rs` to `src/platform/crypto/secret_box.rs`
|
||||
- Move: `src/platform/http/middleware.rs` to `src/platform/http/request_id.rs`
|
||||
- Move: `src/platform/identity/api_key.rs` to `src/platform/identity/client_key.rs`
|
||||
- Move: `src/platform/identity/api_key_repository.rs` to `src/platform/identity/client_key_repository.rs`
|
||||
- Modify: `src/platform/mod.rs`
|
||||
- Modify: `src/platform/http/mod.rs`
|
||||
- Modify: `src/platform/identity/mod.rs`
|
||||
- Test: `tests/architecture/platform_boundary.rs`
|
||||
|
||||
- [x] **Step 1: Move files**
|
||||
|
||||
Create `platform::crypto::secret_box`, re-export `SecretBox`, `CryptoError`, and `CryptoResult` from `platform::crypto`.
|
||||
|
||||
- [x] **Step 2: Replace imports**
|
||||
|
||||
Update all imports of `platform::http::middleware`, `platform::identity::api_key`, and `platform::identity::api_key_repository`.
|
||||
|
||||
## Task 6: Update tests, docs references, and verify
|
||||
|
||||
**Files:**
|
||||
- Modify tests under `tests/**`
|
||||
- Modify current docs under `docs/api.md`, `docs/architecture-capability-plan.md`, `docs/architecture-reorganization.md`, `docs/implementation-status.md`, and this spec/plan if paths drift.
|
||||
|
||||
- [x] **Step 1: Update architecture tests**
|
||||
|
||||
Make architecture tests assert the new public module paths.
|
||||
|
||||
- [x] **Step 2: Update integration test imports**
|
||||
|
||||
Replace old paths with new paths while leaving test behavior unchanged.
|
||||
|
||||
- [x] **Step 3: Run formatting**
|
||||
|
||||
Run: `cargo fmt --check`
|
||||
|
||||
Expected: no formatting diff. If it fails, run `cargo fmt` and re-run `cargo fmt --check`.
|
||||
|
||||
- [x] **Step 4: Run tests**
|
||||
|
||||
Run: `cargo test --locked`
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [x] **Step 5: Run Clippy**
|
||||
|
||||
Run: `cargo clippy --all-targets --all-features --locked -- -D warnings`
|
||||
|
||||
Expected: no warnings.
|
||||
|
||||
- [x] **Step 6: Check whitespace**
|
||||
|
||||
Run: `git diff --check`
|
||||
|
||||
Expected: no whitespace errors.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Backend Naming Architecture Design
|
||||
|
||||
## Goal
|
||||
|
||||
将后端目录整理成一名一义的结构,为 `/admin` Web 控制台预留承载面,并把现有管理 JSON API 收束到 `/api/admin` 的后端契约层。
|
||||
|
||||
## Scope
|
||||
|
||||
本次设计只覆盖 Rust 后端目录、模块命名、路由边界和测试命名。前端工程由独立目录和独立约定承载,后端只提供 `src/web` 作为构建产物的挂载位置。
|
||||
|
||||
## Route Boundaries
|
||||
|
||||
```text
|
||||
/admin/* Web 控制台入口,由 src/web 承载
|
||||
/api/admin/* 管理 API,由 src/admin/api 承载
|
||||
/v1/* OpenAI-compatible API,由 src/codex/serving 承载
|
||||
/auth/openai/callback OpenAI OAuth 回调
|
||||
/debug/* 本地诊断
|
||||
/health 存活检查
|
||||
```
|
||||
|
||||
`admin` 是管理域,`web` 是浏览器承载面,`codex` 是 Codex 上游内核,`platform` 是跨领域系统能力。任何目录名都应表达唯一责任,避免 `http`、`auth`、`api_key` 这类多义名泄漏到领域边界。
|
||||
|
||||
## Target Source Tree
|
||||
|
||||
```text
|
||||
src/
|
||||
main.rs
|
||||
lib.rs
|
||||
|
||||
runtime/{mod.rs, bootstrap.rs, router.rs, state.rs}
|
||||
runtime/tasks/{mod.rs, coordinator.rs, types.rs}
|
||||
|
||||
web/{mod.rs, router.rs, assets.rs, shell.rs, security.rs}
|
||||
|
||||
admin/{mod.rs, settings.rs}
|
||||
admin/api/{mod.rs, router.rs, response.rs, session.rs, diagnostics.rs, settings.rs, models.rs, usage.rs}
|
||||
admin/api/accounts/{mod.rs, list.rs, create.rs, import.rs, export.rs, lifecycle.rs, quota.rs, cookies.rs, oauth.rs, health.rs}
|
||||
admin/api/client_keys/{mod.rs, list.rs, create.rs, import.rs, export.rs, lifecycle.rs}
|
||||
admin/api/logs/{mod.rs, query.rs, detail.rs, state.rs}
|
||||
admin/session/{mod.rs, service.rs, repository.rs}
|
||||
admin/client_keys/{mod.rs, service.rs}
|
||||
admin/tasks/{mod.rs, session_cleanup.rs}
|
||||
|
||||
codex/{mod.rs}
|
||||
codex/accounts/{mod.rs, model.rs, jwt.rs, pool.rs, lifecycle.rs, cloudflare_challenge.rs, usage_snapshots.rs}
|
||||
codex/accounts/cookies/{mod.rs, jar.rs, repository.rs}
|
||||
codex/accounts/repository/{mod.rs, accounts.rs, tokens.rs, leases.rs, quotas.rs, usage.rs}
|
||||
codex/accounts/service/{mod.rs, cookies.rs, health.rs, import.rs, lifecycle.rs, quota.rs, refresh.rs, pool_sync.rs}
|
||||
|
||||
codex/models/{mod.rs, catalog.rs, repository.rs, service.rs}
|
||||
|
||||
codex/gateway/{mod.rs, conversation_identity.rs, installation_id.rs}
|
||||
codex/gateway/fingerprint/{mod.rs, model.rs, repository.rs, update_checker.rs, updater.rs}
|
||||
codex/gateway/oauth/{mod.rs, client.rs, codex_cli.rs, refresh.rs, token.rs}
|
||||
codex/gateway/protocol/{mod.rs, schema.rs, error.rs, openai_to_codex.rs, codex_to_openai.rs}
|
||||
codex/gateway/transport/{mod.rs, http_client.rs, headers.rs, rate_limits.rs, sse.rs, usage_events.rs}
|
||||
codex/gateway/transport/websocket/{mod.rs, codec.rs, pool.rs}
|
||||
|
||||
codex/events/{mod.rs, event.rs, repository.rs, service.rs}
|
||||
codex/usage/{mod.rs, service.rs}
|
||||
codex/tasks/{mod.rs, model_refresh.rs, quota_refresh.rs, token_refresh.rs}
|
||||
|
||||
codex/serving/{mod.rs, chat.rs, responses.rs, diagnostics.rs}
|
||||
codex/serving/http/{mod.rs, router.rs, auth.rs, chat.rs, responses.rs, models.rs, diagnostics.rs, errors.rs}
|
||||
codex/serving/dispatch/{mod.rs, affinity.rs, routing.rs, fallback.rs, limits.rs, account_refresh.rs, stream.rs, stream_audit.rs, usage.rs}
|
||||
|
||||
platform/{mod.rs}
|
||||
platform/crypto/{mod.rs, secret_box.rs}
|
||||
platform/http/{mod.rs, auth.rs, health.rs, request_id.rs}
|
||||
platform/identity/{mod.rs, admin_session.rs, client_key.rs, client_key_repository.rs, error.rs}
|
||||
platform/storage/{mod.rs, db.rs, paths.rs, schema.sql}
|
||||
platform/logging/{mod.rs, rotation.rs}
|
||||
|
||||
config/{mod.rs, loader.rs, types.rs}
|
||||
utils/{mod.rs, json.rs, pagination.rs}
|
||||
```
|
||||
|
||||
## Naming Decisions
|
||||
|
||||
`admin/http` becomes `admin/api` because the module exposes the management API contract, not generic HTTP utilities.
|
||||
|
||||
`admin/auth` splits into `admin/session` and `admin/client_keys`. Admin login is session management; local `cpr_` keys are client credentials for `/v1/*`, not upstream provider keys.
|
||||
|
||||
`api_keys` becomes `client_keys` where the code manages local client credentials. The route can remain `/api/admin/api-keys` for product compatibility, but Rust modules should state the stronger domain meaning.
|
||||
|
||||
`codex/accounts/models` moves to `codex/models`. Model catalog and plan snapshots are Codex catalog concerns, not account-owned state.
|
||||
|
||||
`codex/logs` becomes `codex/events`. The SQLite event stream is a business event store used by the admin console; process file logging belongs under `platform/logging`.
|
||||
|
||||
`platform/http/middleware.rs` becomes `platform/http/request_id.rs` because it has one stable responsibility.
|
||||
|
||||
## Migration Policy
|
||||
|
||||
Implementation should be mechanical first: move files, update module declarations, update imports, then run formatting and tests. Business logic changes are out of scope unless required to preserve existing behavior under the new route boundary.
|
||||
|
||||
The migration must keep `/v1/*` OpenAI-compatible behavior unchanged and keep admin session auth separate from local client API keys.
|
||||
@@ -7,7 +7,7 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{platform::http::middleware::RequestId, runtime::state::AppState};
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
use super::account_service_error;
|
||||
@@ -0,0 +1,42 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension, Json,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
use super::{admin_account_data_from_stored, validated_account_import_error};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateAccountRequest {
|
||||
pub token: Option<String>,
|
||||
pub refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_account(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<CreateAccountRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
let stored = state
|
||||
.services
|
||||
.accounts
|
||||
.import_validated(payload.token, payload.refresh_token)
|
||||
.await
|
||||
.map_err(|error| validated_account_import_error(error, &request_id))?;
|
||||
|
||||
// 手动添加账号的响应只返回可展示元数据,OAuth token 永不回显。
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(admin_account_data_from_stored(stored), request_id),
|
||||
))
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{platform::http::middleware::RequestId, runtime::state::AppState};
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
use super::account_service_error;
|
||||
@@ -10,23 +10,12 @@ use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::{
|
||||
codex::accounts::repository::StoredAccount,
|
||||
platform::http::middleware::RequestId,
|
||||
codex::accounts::repository::StoredAccount, platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
utils::pagination::{clamp_limit, Page},
|
||||
};
|
||||
|
||||
use super::super::{
|
||||
require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse,
|
||||
};
|
||||
use super::{account_export_ids, account_service_error, account_status_value, AdminAccountData};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountsQuery {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
use super::{account_export_ids, account_service_error, account_status_value};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -41,34 +30,6 @@ enum AccountExportFormat {
|
||||
Sub2Api,
|
||||
}
|
||||
|
||||
pub async fn accounts(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<AccountsQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
let limit = clamp_limit(query.limit.unwrap_or(50));
|
||||
let page = state
|
||||
.services
|
||||
.accounts
|
||||
.list(query.cursor, limit)
|
||||
.await
|
||||
.map_err(|error| account_service_error(error, &request_id))?;
|
||||
let Page { items, next_cursor } = page;
|
||||
let page = Page {
|
||||
items: items.into_iter().map(AdminAccountData::from).collect(),
|
||||
next_cursor,
|
||||
};
|
||||
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminPageEnvelope::ok(page, limit, request_id),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn export_accounts(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
codex::accounts::service::{AccountProbeResult, HealthCheckError},
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -13,13 +13,13 @@ use crate::{
|
||||
accounts::service::{AccountImportEntry, StoreImportAccountError},
|
||||
gateway::oauth::{default_codex_home, read_cli_auth_from_home},
|
||||
},
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
utils::json::{first_string, string_at},
|
||||
};
|
||||
|
||||
use super::super::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
use super::{admin_account_data_from_stored, validated_account_import_error};
|
||||
use super::validated_account_import_error;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AccountImportFormat {
|
||||
@@ -52,42 +52,12 @@ pub struct AccountImportData {
|
||||
pub source_format: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateAccountRequest {
|
||||
pub token: Option<String>,
|
||||
pub refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportCliAuthRequest {
|
||||
pub codex_home: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_account(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<CreateAccountRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
let stored = state
|
||||
.services
|
||||
.accounts
|
||||
.import_validated(payload.token, payload.refresh_token)
|
||||
.await
|
||||
.map_err(|error| validated_account_import_error(error, &request_id))?;
|
||||
|
||||
// 手动添加账号的响应只返回可展示元数据,OAuth token 永不回显。
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(admin_account_data_from_stored(stored), request_id),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn import_accounts(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
codex::accounts::{model::AccountStatus, service::RefreshAccountError},
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
utils::pagination::{clamp_limit, Page},
|
||||
};
|
||||
|
||||
use super::super::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse};
|
||||
use super::{account_service_error, AdminAccountData};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountsQuery {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
pub async fn accounts(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<AccountsQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
let limit = clamp_limit(query.limit.unwrap_or(50));
|
||||
let page = state
|
||||
.services
|
||||
.accounts
|
||||
.list(query.cursor, limit)
|
||||
.await
|
||||
.map_err(|error| account_service_error(error, &request_id))?;
|
||||
let Page { items, next_cursor } = page;
|
||||
let page = Page {
|
||||
items: items.into_iter().map(AdminAccountData::from).collect(),
|
||||
next_cursor,
|
||||
};
|
||||
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminPageEnvelope::ok(page, limit, request_id),
|
||||
))
|
||||
}
|
||||
@@ -14,9 +14,11 @@ use super::AdminError;
|
||||
pub mod cookies;
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod export;
|
||||
pub mod health;
|
||||
pub mod import;
|
||||
pub mod lifecycle;
|
||||
pub mod list;
|
||||
pub mod mutate;
|
||||
pub mod oauth;
|
||||
pub mod quota;
|
||||
|
||||
@@ -24,22 +26,21 @@ pub use cookies::{
|
||||
delete_account_cookies, get_account_cookies, set_account_cookies, AccountCookiesData,
|
||||
DeleteAccountCookiesData, SetAccountCookiesRequest,
|
||||
};
|
||||
pub use create::{
|
||||
create_account, import_accounts, import_cli_auth, AccountImportData, CreateAccountRequest,
|
||||
ImportCliAuthRequest,
|
||||
};
|
||||
pub use create::{create_account, CreateAccountRequest};
|
||||
pub use delete::{
|
||||
batch_delete_accounts, delete_account, BatchDeleteAccountsData, BatchDeleteAccountsRequest,
|
||||
DeleteAccountData,
|
||||
};
|
||||
pub use export::{export_accounts, AccountExportQuery};
|
||||
pub use health::{health_check_accounts, AccountProbeData, HealthCheckData, HealthCheckRequest};
|
||||
pub use list::{accounts, export_accounts, AccountExportQuery, AccountsQuery};
|
||||
pub use mutate::{
|
||||
pub use import::{import_accounts, import_cli_auth, AccountImportData, ImportCliAuthRequest};
|
||||
pub use lifecycle::{
|
||||
batch_update_account_status, refresh_account, reset_account_usage, update_account_label,
|
||||
update_account_status, BatchUpdateAccountStatusData, BatchUpdateAccountStatusRequest,
|
||||
ResetAccountUsageData, UpdateAccountLabelData, UpdateAccountLabelRequest,
|
||||
UpdateAccountStatusData, UpdateAccountStatusRequest,
|
||||
};
|
||||
pub use list::{accounts, AccountsQuery};
|
||||
pub use oauth::{
|
||||
auth_callback, auth_code_relay, auth_device_login, auth_device_poll, auth_login_start,
|
||||
AdminAuthCallbackQuery, AdminAuthCodeRelayData, AdminAuthCodeRelayRequest,
|
||||
@@ -11,10 +11,10 @@ use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
admin::auth::service::{AdminAuthOAuthError, AdminAuthPkceExchange},
|
||||
admin::session::service::{AdminAuthOAuthError, AdminAuthPkceExchange},
|
||||
codex::gateway::oauth::{DeviceCode, OAuthError},
|
||||
config::AppConfig,
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
AccountQuotaError, AccountQuotaWarning as ServiceAccountQuotaWarning,
|
||||
AccountQuotaWarnings as ServiceAccountQuotaWarnings,
|
||||
},
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use axum::{
|
||||
extract::State, http::HeaderMap, http::StatusCode, response::IntoResponse, Extension, Json,
|
||||
};
|
||||
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::{api_key_service_error, CreateApiKeyRequest, CreatedClientApiKeyData};
|
||||
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
|
||||
pub async fn create_api_key(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<CreateApiKeyRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
match state.services.api_keys.create(&payload.name).await {
|
||||
Ok(created) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
CreatedClientApiKeyData::new(created.key, created.plaintext),
|
||||
request_id,
|
||||
),
|
||||
)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::{
|
||||
api_key_service_error, ApiKeyExportQuery, ClientApiKeyExportData, ClientApiKeyExportEntry,
|
||||
};
|
||||
use crate::admin::api::{
|
||||
account_export_ids, require_admin_session, AdminEnvelope, AdminError, AdminResponse,
|
||||
};
|
||||
|
||||
pub async fn export_api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<ApiKeyExportQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let ids = account_export_ids(query.ids.as_deref());
|
||||
let keys = match state.services.api_keys.export(ids).await {
|
||||
Ok(keys) => keys,
|
||||
Err(error) => return Err(api_key_service_error(error, request_id)),
|
||||
};
|
||||
|
||||
// 安全边界:本地 cpr_ key 只导出可展示元数据,绝不导出 plaintext、key_hash 或 pepper。
|
||||
let data = ClientApiKeyExportData {
|
||||
source_format: "rustLocalClientApiKeys",
|
||||
rotation_required: true,
|
||||
api_keys: keys
|
||||
.into_iter()
|
||||
.map(ClientApiKeyExportEntry::from)
|
||||
.collect(),
|
||||
};
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(data, request_id),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use axum::{
|
||||
extract::State, http::HeaderMap, http::StatusCode, response::IntoResponse, Extension, Json,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::{api_key_service_error, ClientApiKeyImportData, ImportedClientApiKeyData};
|
||||
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
|
||||
pub async fn import_api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<Value>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let imported = match state.services.api_keys.import(&payload).await {
|
||||
Ok(imported) => imported,
|
||||
Err(error) => return Err(api_key_service_error(error, request_id)),
|
||||
};
|
||||
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
ClientApiKeyImportData {
|
||||
imported: imported.imported,
|
||||
skipped: imported.skipped,
|
||||
rotated: true,
|
||||
keys: imported
|
||||
.keys
|
||||
.into_iter()
|
||||
.map(ImportedClientApiKeyData::new)
|
||||
.collect(),
|
||||
},
|
||||
request_id,
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension, Json,
|
||||
};
|
||||
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::{
|
||||
api_key_not_found, api_key_service_error, BatchDeleteClientApiKeysData,
|
||||
BatchDeleteClientApiKeysRequest, ClientApiKeyData, DeleteClientApiKeyData,
|
||||
UpdateClientApiKeyLabelRequest, UpdateClientApiKeyStatusData, UpdateClientApiKeyStatusRequest,
|
||||
};
|
||||
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
|
||||
pub async fn batch_delete_api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<BatchDeleteClientApiKeysRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let deleted = match state.services.api_keys.batch_delete(payload.ids).await {
|
||||
Ok(deleted) => deleted,
|
||||
Err(error) => return Err(api_key_service_error(error, request_id)),
|
||||
};
|
||||
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
BatchDeleteClientApiKeysData {
|
||||
deleted: deleted.deleted,
|
||||
not_found: deleted.not_found,
|
||||
},
|
||||
request_id,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn update_api_key_label(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(key_id): Path<String>,
|
||||
Json(payload): Json<UpdateClientApiKeyLabelRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
// label 是后台显示备注,不能影响 cpr_ key 的 hash、prefix 或启用状态。
|
||||
match state
|
||||
.services
|
||||
.api_keys
|
||||
.update_label(&key_id, payload.label)
|
||||
.await
|
||||
{
|
||||
Ok(Some(key)) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(ClientApiKeyData::from(key), request_id),
|
||||
)),
|
||||
Ok(None) => Err(api_key_not_found(request_id)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_api_key_status(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(key_id): Path<String>,
|
||||
Json(payload): Json<UpdateClientApiKeyStatusRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state
|
||||
.services
|
||||
.api_keys
|
||||
.update_status(key_id, &payload.status)
|
||||
.await
|
||||
{
|
||||
Ok(Some(updated)) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
UpdateClientApiKeyStatusData {
|
||||
id: updated.id,
|
||||
enabled: updated.enabled,
|
||||
},
|
||||
request_id,
|
||||
),
|
||||
)),
|
||||
Ok(None) => Err(api_key_not_found(request_id)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_api_key(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(key_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.api_keys.delete(&key_id).await {
|
||||
Ok(true) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(DeleteClientApiKeyData { deleted: true }, request_id),
|
||||
)),
|
||||
Ok(false) => Err(api_key_not_found(request_id)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
utils::pagination::{clamp_limit, Page},
|
||||
};
|
||||
|
||||
use super::{api_key_service_error, ApiKeysQuery, ClientApiKeyData};
|
||||
use crate::admin::api::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse};
|
||||
|
||||
pub async fn api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<ApiKeysQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let limit = clamp_limit(query.limit.unwrap_or(50));
|
||||
match state.services.api_keys.list(query.cursor, limit).await {
|
||||
Ok(page) => {
|
||||
let Page { items, next_cursor } = page;
|
||||
let page = Page {
|
||||
items: items.into_iter().map(ClientApiKeyData::from).collect(),
|
||||
next_cursor,
|
||||
};
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminPageEnvelope::ok(page, limit, request_id),
|
||||
))
|
||||
}
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
use axum::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
admin::client_keys::service::{ApiKeyServiceError, ImportedClientApiKey},
|
||||
platform::identity::client_key_repository::StoredClientApiKey,
|
||||
};
|
||||
|
||||
use super::AdminError;
|
||||
|
||||
pub mod create;
|
||||
pub mod export;
|
||||
pub mod import;
|
||||
pub mod lifecycle;
|
||||
pub mod list;
|
||||
|
||||
pub use create::create_api_key;
|
||||
pub use export::export_api_keys;
|
||||
pub use import::import_api_keys;
|
||||
pub use lifecycle::{
|
||||
batch_delete_api_keys, delete_api_key, update_api_key_label, update_api_key_status,
|
||||
};
|
||||
pub use list::api_keys;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiKeysQuery {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiKeyExportQuery {
|
||||
pub ids: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateApiKeyRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyData {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatedClientApiKeyData {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
pub plaintext: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateClientApiKeyStatusRequest {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateClientApiKeyLabelRequest {
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateClientApiKeyStatusData {
|
||||
pub id: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BatchDeleteClientApiKeysRequest {
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BatchDeleteClientApiKeysData {
|
||||
pub deleted: u32,
|
||||
pub not_found: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteClientApiKeyData {
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyExportData {
|
||||
pub source_format: &'static str,
|
||||
pub rotation_required: bool,
|
||||
pub api_keys: Vec<ClientApiKeyExportEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyExportEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyImportData {
|
||||
pub imported: u32,
|
||||
pub skipped: u32,
|
||||
pub rotated: bool,
|
||||
pub keys: Vec<ImportedClientApiKeyData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportedClientApiKeyData {
|
||||
pub source_id: Option<String>,
|
||||
pub source_prefix: Option<String>,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
pub plaintext: String,
|
||||
}
|
||||
|
||||
impl From<StoredClientApiKey> for ClientApiKeyData {
|
||||
fn from(key: StoredClientApiKey) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoredClientApiKey> for ClientApiKeyExportEntry {
|
||||
fn from(key: StoredClientApiKey) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreatedClientApiKeyData {
|
||||
pub(super) fn new(key: StoredClientApiKey, plaintext: String) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
plaintext,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImportedClientApiKeyData {
|
||||
pub(super) fn new(imported: ImportedClientApiKey) -> Self {
|
||||
let ImportedClientApiKey {
|
||||
key,
|
||||
plaintext,
|
||||
source_id,
|
||||
source_prefix,
|
||||
} = imported;
|
||||
Self {
|
||||
source_id,
|
||||
source_prefix,
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
plaintext,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn api_key_service_error(error: ApiKeyServiceError, request_id: String) -> AdminError {
|
||||
match error {
|
||||
ApiKeyServiceError::RepositoryUnavailable => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"API key repository is not initialized",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::HasherUnavailable => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"API key hasher is not initialized",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::List => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to list API keys",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Export => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to export API keys",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Import => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to import API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Create => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to create API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Delete => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to delete API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::UpdateLabel => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to update API key label",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::UpdateStatus => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to update API key status",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Verify => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to verify API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::InvalidStatus(message) => {
|
||||
AdminError::new(StatusCode::BAD_REQUEST, 40001, message, request_id)
|
||||
}
|
||||
ApiKeyServiceError::EmptyName => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"API key name is required",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::EmptyIds => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"API key ids are required",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::LabelTooLong => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"API key label must be 64 characters or fewer",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::NoImportableKeys => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"No importable API keys found",
|
||||
request_id,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn api_key_not_found(request_id: String) -> AdminError {
|
||||
AdminError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
40401,
|
||||
"API key not found",
|
||||
request_id,
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
codex::serving::http::diagnostics::diagnostics_data, platform::http::middleware::RequestId,
|
||||
codex::serving::http::diagnostics::diagnostics_data, platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::log_service_error;
|
||||
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
|
||||
pub async fn log_detail(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(log_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.get(&log_id).await {
|
||||
Ok(Some(log)) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(log, request_id),
|
||||
)),
|
||||
Ok(None) => Err(AdminError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
40401,
|
||||
"Log event not found",
|
||||
request_id,
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use crate::codex::events::service::LogServiceError;
|
||||
|
||||
use super::AdminError;
|
||||
|
||||
pub mod detail;
|
||||
pub mod query;
|
||||
pub mod state;
|
||||
|
||||
pub use detail::log_detail;
|
||||
pub use query::logs;
|
||||
pub use state::{clear_logs, logs_state, update_logs_state};
|
||||
|
||||
pub(super) fn log_service_error(error: LogServiceError, request_id: String) -> AdminError {
|
||||
match error {
|
||||
LogServiceError::RepositoryUnavailable => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Event log repository is not initialized",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::List => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to list event logs",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Get => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to load event log",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Count => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to count event logs",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Clear => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to clear event logs",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Write => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to write event log",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::InvalidCapacity => AdminError::new(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
42201,
|
||||
"logsCapacity must be greater than 0",
|
||||
request_id,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
codex::events::service::LogListFilter, platform::http::request_id::RequestId,
|
||||
runtime::state::AppState, utils::pagination::clamp_limit,
|
||||
};
|
||||
|
||||
use super::log_service_error;
|
||||
use crate::admin::api::{require_admin_session, AdminError, AdminPageEnvelope, AdminResponse};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogsQuery {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub kind: Option<String>,
|
||||
pub level: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
pub account_id: Option<String>,
|
||||
pub route: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status_code: Option<i64>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn logs(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<LogsQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
let limit = clamp_limit(query.limit.unwrap_or(50));
|
||||
let cursor = query.cursor.clone();
|
||||
match state.services.logs.list(cursor, limit, query.into()).await {
|
||||
Ok(page) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminPageEnvelope::ok(page, limit, request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogsQuery> for LogListFilter {
|
||||
fn from(query: LogsQuery) -> Self {
|
||||
Self {
|
||||
kind: non_empty(query.kind),
|
||||
level: non_empty(query.level),
|
||||
request_id: non_empty(query.request_id),
|
||||
account_id: non_empty(query.account_id),
|
||||
route: non_empty(query.route),
|
||||
model: non_empty(query.model),
|
||||
status_code: query.status_code,
|
||||
search: non_empty(query.search),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension, Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
codex::events::service::{ClearLogs, LogState, LogStateUpdate},
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
use super::log_service_error;
|
||||
use crate::admin::api::{require_admin_session, AdminEnvelope, AdminError, AdminResponse};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogStateData {
|
||||
pub enabled: bool,
|
||||
pub capacity: u32,
|
||||
pub capture_body: bool,
|
||||
pub stored_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClearLogsData {
|
||||
pub cleared: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateLogStateRequest {
|
||||
pub enabled: Option<bool>,
|
||||
pub capacity: Option<u32>,
|
||||
pub capture_body: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn logs_state(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.state().await {
|
||||
Ok(state) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(LogStateData::from(state), request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_logs_state(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<UpdateLogStateRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.update_state(payload.into()).await {
|
||||
Ok(state) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(LogStateData::from(state), request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear_logs(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.clear().await {
|
||||
Ok(cleared) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(ClearLogsData::from(cleared), request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogState> for LogStateData {
|
||||
fn from(state: LogState) -> Self {
|
||||
Self {
|
||||
enabled: state.enabled,
|
||||
capacity: state.capacity,
|
||||
capture_body: state.capture_body,
|
||||
stored_count: state.stored_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClearLogs> for ClearLogsData {
|
||||
fn from(cleared: ClearLogs) -> Self {
|
||||
Self {
|
||||
cleared: cleared.cleared,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateLogStateRequest> for LogStateUpdate {
|
||||
fn from(request: UpdateLogStateRequest) -> Self {
|
||||
Self {
|
||||
enabled: request.enabled,
|
||||
capacity: request.capacity,
|
||||
capture_body: request.capture_body,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
pub mod accounts;
|
||||
pub mod api_keys;
|
||||
pub mod auth;
|
||||
pub mod client_keys;
|
||||
pub mod diagnostics;
|
||||
pub mod logs;
|
||||
pub mod models;
|
||||
pub mod response;
|
||||
pub mod router;
|
||||
pub mod session;
|
||||
pub mod settings;
|
||||
pub mod usage;
|
||||
|
||||
use self::{
|
||||
accounts::{account_export_ids, account_status_value},
|
||||
auth::require_admin_session,
|
||||
session::require_admin_session,
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
@@ -7,8 +7,8 @@ use axum::{
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
codex::accounts::models::service::{ModelRefreshResult, ModelServiceError},
|
||||
platform::http::middleware::RequestId,
|
||||
codex::models::service::{ModelRefreshResult, ModelServiceError},
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
use axum::{
|
||||
routing::{delete, get, patch, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
use crate::runtime::state::AppState;
|
||||
|
||||
use super::{
|
||||
accounts::{
|
||||
account_quota, accounts, auth_callback, auth_code_relay, auth_device_login,
|
||||
auth_device_poll, auth_login_start, batch_delete_accounts, batch_update_account_status,
|
||||
create_account, delete_account, delete_account_cookies, export_accounts,
|
||||
get_account_cookies, health_check_accounts, import_accounts, import_cli_auth,
|
||||
quota_warnings, refresh_account, reset_account_usage, set_account_cookies,
|
||||
update_account_label, update_account_status,
|
||||
},
|
||||
client_keys::{
|
||||
api_keys, batch_delete_api_keys, create_api_key, delete_api_key, export_api_keys,
|
||||
import_api_keys, update_api_key_label, update_api_key_status,
|
||||
},
|
||||
diagnostics::diagnostics,
|
||||
logs::{clear_logs, log_detail, logs, logs_state, update_logs_state},
|
||||
models::refresh_models,
|
||||
session::{auth_logout, auth_status, login},
|
||||
settings::{settings, update_settings},
|
||||
usage::{usage_stats, usage_stats_summary},
|
||||
};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/admin/login", post(login))
|
||||
.route("/api/admin/auth/status", get(auth_status))
|
||||
.route("/api/admin/auth/logout", post(auth_logout))
|
||||
.route("/api/admin/auth/login-start", post(auth_login_start))
|
||||
.route("/api/admin/auth/code-relay", post(auth_code_relay))
|
||||
.route("/api/admin/auth/callback", get(auth_callback))
|
||||
.route("/auth/openai/callback", get(auth_callback))
|
||||
.route("/api/admin/auth/device-login", post(auth_device_login))
|
||||
.route(
|
||||
"/api/admin/auth/device-poll/{device_code}",
|
||||
get(auth_device_poll),
|
||||
)
|
||||
.route("/api/admin/diagnostics", get(diagnostics))
|
||||
.route("/api/admin/logs", get(logs).delete(clear_logs))
|
||||
.route(
|
||||
"/api/admin/logs/state",
|
||||
get(logs_state).patch(update_logs_state),
|
||||
)
|
||||
.route("/api/admin/logs/{log_id}", get(log_detail))
|
||||
.route("/api/admin/settings", get(settings).patch(update_settings))
|
||||
.route("/api/admin/refresh-models", post(refresh_models))
|
||||
.route("/api/admin/usage-stats", get(usage_stats))
|
||||
.route("/api/admin/usage-stats/summary", get(usage_stats_summary))
|
||||
.route("/api/admin/accounts", get(accounts).post(create_account))
|
||||
.route(
|
||||
"/api/admin/accounts/health-check",
|
||||
post(health_check_accounts),
|
||||
)
|
||||
.route("/api/admin/accounts/quota-warnings", get(quota_warnings))
|
||||
.route(
|
||||
"/api/admin/accounts/batch-delete",
|
||||
post(batch_delete_accounts),
|
||||
)
|
||||
.route(
|
||||
"/api/admin/accounts/batch-status",
|
||||
post(batch_update_account_status),
|
||||
)
|
||||
.route("/api/admin/accounts/export", get(export_accounts))
|
||||
.route(
|
||||
"/api/admin/accounts/{account_id}/refresh",
|
||||
post(refresh_account),
|
||||
)
|
||||
.route(
|
||||
"/api/admin/accounts/{account_id}/reset-usage",
|
||||
post(reset_account_usage),
|
||||
)
|
||||
.route("/api/admin/accounts/{account_id}/quota", get(account_quota))
|
||||
.route(
|
||||
"/api/admin/accounts/{account_id}/cookies",
|
||||
get(get_account_cookies)
|
||||
.post(set_account_cookies)
|
||||
.delete(delete_account_cookies),
|
||||
)
|
||||
.route("/api/admin/accounts/{account_id}", delete(delete_account))
|
||||
.route(
|
||||
"/api/admin/accounts/{account_id}/label",
|
||||
patch(update_account_label),
|
||||
)
|
||||
.route(
|
||||
"/api/admin/accounts/{account_id}/status",
|
||||
patch(update_account_status),
|
||||
)
|
||||
.route("/api/admin/accounts/import", post(import_accounts))
|
||||
.route("/api/admin/accounts/import-cli", post(import_cli_auth))
|
||||
.route("/api/admin/api-keys", get(api_keys).post(create_api_key))
|
||||
.route("/api/admin/api-keys/export", get(export_api_keys))
|
||||
.route("/api/admin/api-keys/import", post(import_api_keys))
|
||||
.route(
|
||||
"/api/admin/api-keys/batch-delete",
|
||||
post(batch_delete_api_keys),
|
||||
)
|
||||
.route("/api/admin/api-keys/{key_id}", delete(delete_api_key))
|
||||
.route(
|
||||
"/api/admin/api-keys/{key_id}/label",
|
||||
patch(update_api_key_label),
|
||||
)
|
||||
.route(
|
||||
"/api/admin/api-keys/{key_id}/status",
|
||||
patch(update_api_key_status),
|
||||
)
|
||||
}
|
||||
@@ -10,12 +10,12 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
admin::auth::service::{
|
||||
admin::session::service::{
|
||||
AdminAuthPoolSummary as ServiceAdminAuthPoolSummary, AdminAuthServiceError,
|
||||
AdminAuthStatus as ServiceAdminAuthStatus, AdminAuthUser as ServiceAdminAuthUser,
|
||||
AdminSessionValidationError,
|
||||
},
|
||||
platform::http::{auth::admin_session_id, middleware::RequestId},
|
||||
platform::http::{auth::admin_session_id, request_id::RequestId},
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ use serde_json::Value;
|
||||
use crate::{
|
||||
admin::settings::{SettingsServiceError, SettingsUpdate},
|
||||
config::{AppConfig, QuotaWarningThresholds},
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
AccountRepositoryError, AccountUsageListRecord, AccountUsageSummary,
|
||||
},
|
||||
codex::usage::service::UsageServiceError,
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
utils::pagination::{clamp_limit, Page},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
pub mod service;
|
||||
@@ -2,8 +2,8 @@ use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
platform::identity::{
|
||||
api_key::ApiKeyHasher,
|
||||
api_key_repository::{ClientApiKeyRepository, StoredClientApiKey},
|
||||
client_key::ApiKeyHasher,
|
||||
client_key_repository::{ClientApiKeyRepository, StoredClientApiKey},
|
||||
},
|
||||
utils::{json::first_string, pagination::Page},
|
||||
};
|
||||
@@ -1,519 +0,0 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension, Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
admin::auth::api_key::{ApiKeyServiceError, ImportedClientApiKey},
|
||||
platform::{http::middleware::RequestId, identity::api_key_repository::StoredClientApiKey},
|
||||
runtime::state::AppState,
|
||||
utils::pagination::{clamp_limit, Page},
|
||||
};
|
||||
|
||||
use super::{
|
||||
account_export_ids, require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope,
|
||||
AdminResponse,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiKeysQuery {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiKeyExportQuery {
|
||||
pub ids: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateApiKeyRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyData {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreatedClientApiKeyData {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
pub plaintext: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateClientApiKeyStatusRequest {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateClientApiKeyLabelRequest {
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateClientApiKeyStatusData {
|
||||
pub id: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BatchDeleteClientApiKeysRequest {
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BatchDeleteClientApiKeysData {
|
||||
pub deleted: u32,
|
||||
pub not_found: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteClientApiKeyData {
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyExportData {
|
||||
pub source_format: &'static str,
|
||||
pub rotation_required: bool,
|
||||
pub api_keys: Vec<ClientApiKeyExportEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyExportEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClientApiKeyImportData {
|
||||
pub imported: u32,
|
||||
pub skipped: u32,
|
||||
pub rotated: bool,
|
||||
pub keys: Vec<ImportedClientApiKeyData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportedClientApiKeyData {
|
||||
pub source_id: Option<String>,
|
||||
pub source_prefix: Option<String>,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub prefix: String,
|
||||
pub enabled: bool,
|
||||
pub created_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
pub plaintext: String,
|
||||
}
|
||||
|
||||
impl From<StoredClientApiKey> for ClientApiKeyData {
|
||||
fn from(key: StoredClientApiKey) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoredClientApiKey> for ClientApiKeyExportEntry {
|
||||
fn from(key: StoredClientApiKey) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CreatedClientApiKeyData {
|
||||
fn new(key: StoredClientApiKey, plaintext: String) -> Self {
|
||||
Self {
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
plaintext,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImportedClientApiKeyData {
|
||||
fn new(imported: ImportedClientApiKey) -> Self {
|
||||
let ImportedClientApiKey {
|
||||
key,
|
||||
plaintext,
|
||||
source_id,
|
||||
source_prefix,
|
||||
} = imported;
|
||||
Self {
|
||||
source_id,
|
||||
source_prefix,
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
label: key.label,
|
||||
prefix: key.prefix,
|
||||
enabled: key.enabled,
|
||||
created_at: key.created_at,
|
||||
last_used_at: key.last_used_at,
|
||||
plaintext,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn api_key_service_error(error: ApiKeyServiceError, request_id: String) -> AdminError {
|
||||
match error {
|
||||
ApiKeyServiceError::RepositoryUnavailable => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"API key repository is not initialized",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::HasherUnavailable => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"API key hasher is not initialized",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::List => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to list API keys",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Export => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to export API keys",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Import => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to import API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Create => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to create API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Delete => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to delete API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::UpdateLabel => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to update API key label",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::UpdateStatus => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to update API key status",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::Verify => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to verify API key",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::InvalidStatus(message) => {
|
||||
AdminError::new(StatusCode::BAD_REQUEST, 40001, message, request_id)
|
||||
}
|
||||
ApiKeyServiceError::EmptyName => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"API key name is required",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::EmptyIds => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"API key ids are required",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::LabelTooLong => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"API key label must be 64 characters or fewer",
|
||||
request_id,
|
||||
),
|
||||
ApiKeyServiceError::NoImportableKeys => AdminError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
40001,
|
||||
"No importable API keys found",
|
||||
request_id,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn api_key_not_found(request_id: String) -> AdminError {
|
||||
AdminError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
40401,
|
||||
"API key not found",
|
||||
request_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<ApiKeysQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let limit = clamp_limit(query.limit.unwrap_or(50));
|
||||
match state.services.api_keys.list(query.cursor, limit).await {
|
||||
Ok(page) => {
|
||||
let Page { items, next_cursor } = page;
|
||||
let page = Page {
|
||||
items: items.into_iter().map(ClientApiKeyData::from).collect(),
|
||||
next_cursor,
|
||||
};
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminPageEnvelope::ok(page, limit, request_id),
|
||||
))
|
||||
}
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn export_api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<ApiKeyExportQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let ids = account_export_ids(query.ids.as_deref());
|
||||
let keys = match state.services.api_keys.export(ids).await {
|
||||
Ok(keys) => keys,
|
||||
Err(error) => return Err(api_key_service_error(error, request_id)),
|
||||
};
|
||||
|
||||
// 安全边界:本地 cpr_ key 只导出可展示元数据,绝不导出 plaintext、key_hash 或 pepper。
|
||||
let data = ClientApiKeyExportData {
|
||||
source_format: "rustLocalClientApiKeys",
|
||||
rotation_required: true,
|
||||
api_keys: keys
|
||||
.into_iter()
|
||||
.map(ClientApiKeyExportEntry::from)
|
||||
.collect(),
|
||||
};
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(data, request_id),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn import_api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<Value>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let imported = match state.services.api_keys.import(&payload).await {
|
||||
Ok(imported) => imported,
|
||||
Err(error) => return Err(api_key_service_error(error, request_id)),
|
||||
};
|
||||
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
ClientApiKeyImportData {
|
||||
imported: imported.imported,
|
||||
skipped: imported.skipped,
|
||||
rotated: true,
|
||||
keys: imported
|
||||
.keys
|
||||
.into_iter()
|
||||
.map(ImportedClientApiKeyData::new)
|
||||
.collect(),
|
||||
},
|
||||
request_id,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn create_api_key(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<CreateApiKeyRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
match state.services.api_keys.create(&payload.name).await {
|
||||
Ok(created) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
CreatedClientApiKeyData::new(created.key, created.plaintext),
|
||||
request_id,
|
||||
),
|
||||
)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn batch_delete_api_keys(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<BatchDeleteClientApiKeysRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
let deleted = match state.services.api_keys.batch_delete(payload.ids).await {
|
||||
Ok(deleted) => deleted,
|
||||
Err(error) => return Err(api_key_service_error(error, request_id)),
|
||||
};
|
||||
|
||||
Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
BatchDeleteClientApiKeysData {
|
||||
deleted: deleted.deleted,
|
||||
not_found: deleted.not_found,
|
||||
},
|
||||
request_id,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn update_api_key_label(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(key_id): Path<String>,
|
||||
Json(payload): Json<UpdateClientApiKeyLabelRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
// label 是后台显示备注,不能影响 cpr_ key 的 hash、prefix 或启用状态。
|
||||
match state
|
||||
.services
|
||||
.api_keys
|
||||
.update_label(&key_id, payload.label)
|
||||
.await
|
||||
{
|
||||
Ok(Some(key)) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(ClientApiKeyData::from(key), request_id),
|
||||
)),
|
||||
Ok(None) => Err(api_key_not_found(request_id)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_api_key_status(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(key_id): Path<String>,
|
||||
Json(payload): Json<UpdateClientApiKeyStatusRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state
|
||||
.services
|
||||
.api_keys
|
||||
.update_status(key_id, &payload.status)
|
||||
.await
|
||||
{
|
||||
Ok(Some(updated)) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(
|
||||
UpdateClientApiKeyStatusData {
|
||||
id: updated.id,
|
||||
enabled: updated.enabled,
|
||||
},
|
||||
request_id,
|
||||
),
|
||||
)),
|
||||
Ok(None) => Err(api_key_not_found(request_id)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_api_key(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(key_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.api_keys.delete(&key_id).await {
|
||||
Ok(true) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(DeleteClientApiKeyData { deleted: true }, request_id),
|
||||
)),
|
||||
Ok(false) => Err(api_key_not_found(request_id)),
|
||||
Err(error) => Err(api_key_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::IntoResponse,
|
||||
Extension, Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
codex::logs::service::{ClearLogs, LogListFilter, LogServiceError, LogState, LogStateUpdate},
|
||||
platform::http::middleware::RequestId,
|
||||
runtime::state::AppState,
|
||||
utils::pagination::clamp_limit,
|
||||
};
|
||||
|
||||
use super::{require_admin_session, AdminEnvelope, AdminError, AdminPageEnvelope, AdminResponse};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogsQuery {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
pub kind: Option<String>,
|
||||
pub level: Option<String>,
|
||||
pub request_id: Option<String>,
|
||||
pub account_id: Option<String>,
|
||||
pub route: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status_code: Option<i64>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogStateData {
|
||||
pub enabled: bool,
|
||||
pub capacity: u32,
|
||||
pub capture_body: bool,
|
||||
pub stored_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClearLogsData {
|
||||
pub cleared: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateLogStateRequest {
|
||||
pub enabled: Option<bool>,
|
||||
pub capacity: Option<u32>,
|
||||
pub capture_body: Option<bool>,
|
||||
}
|
||||
|
||||
pub async fn logs(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<LogsQuery>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
let limit = clamp_limit(query.limit.unwrap_or(50));
|
||||
let cursor = query.cursor.clone();
|
||||
match state.services.logs.list(cursor, limit, query.into()).await {
|
||||
Ok(page) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminPageEnvelope::ok(page, limit, request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn logs_state(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.state().await {
|
||||
Ok(state) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(LogStateData::from(state), request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_logs_state(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<UpdateLogStateRequest>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.update_state(payload.into()).await {
|
||||
Ok(state) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(LogStateData::from(state), request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn log_detail(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
Path(log_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.get(&log_id).await {
|
||||
Ok(Some(log)) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(log, request_id),
|
||||
)),
|
||||
Ok(None) => Err(AdminError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
40401,
|
||||
"Log event not found",
|
||||
request_id,
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear_logs(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_id): Extension<RequestId>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<impl IntoResponse, AdminError> {
|
||||
let request_id = request_id.as_str().to_string();
|
||||
require_admin_session(&state, &headers, &request_id).await?;
|
||||
|
||||
match state.services.logs.clear().await {
|
||||
Ok(cleared) => Ok(AdminResponse::new(
|
||||
StatusCode::OK,
|
||||
AdminEnvelope::ok(ClearLogsData::from(cleared), request_id),
|
||||
)),
|
||||
Err(error) => Err(log_service_error(error, request_id)),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogState> for LogStateData {
|
||||
fn from(state: LogState) -> Self {
|
||||
Self {
|
||||
enabled: state.enabled,
|
||||
capacity: state.capacity,
|
||||
capture_body: state.capture_body,
|
||||
stored_count: state.stored_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClearLogs> for ClearLogsData {
|
||||
fn from(cleared: ClearLogs) -> Self {
|
||||
Self {
|
||||
cleared: cleared.cleared,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateLogStateRequest> for LogStateUpdate {
|
||||
fn from(request: UpdateLogStateRequest) -> Self {
|
||||
Self {
|
||||
enabled: request.enabled,
|
||||
capacity: request.capacity,
|
||||
capture_body: request.capture_body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogsQuery> for LogListFilter {
|
||||
fn from(query: LogsQuery) -> Self {
|
||||
Self {
|
||||
kind: non_empty(query.kind),
|
||||
level: non_empty(query.level),
|
||||
request_id: non_empty(query.request_id),
|
||||
account_id: non_empty(query.account_id),
|
||||
route: non_empty(query.route),
|
||||
model: non_empty(query.model),
|
||||
status_code: query.status_code,
|
||||
search: non_empty(query.search),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn log_service_error(error: LogServiceError, request_id: String) -> AdminError {
|
||||
match error {
|
||||
LogServiceError::RepositoryUnavailable => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Event log repository is not initialized",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::List => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to list event logs",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Get => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to load event log",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Count => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to count event logs",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Clear => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to clear event logs",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::Write => AdminError::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
50001,
|
||||
"Failed to write event log",
|
||||
request_id,
|
||||
),
|
||||
LogServiceError::InvalidCapacity => AdminError::new(
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
42201,
|
||||
"logsCapacity must be greater than 0",
|
||||
request_id,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use axum::{
|
||||
routing::{delete, get, patch, post},
|
||||
Router,
|
||||
};
|
||||
|
||||
use crate::runtime::state::AppState;
|
||||
|
||||
use super::{
|
||||
accounts::{
|
||||
account_quota, accounts, auth_callback, auth_code_relay, auth_device_login,
|
||||
auth_device_poll, auth_login_start, batch_delete_accounts, batch_update_account_status,
|
||||
create_account, delete_account, delete_account_cookies, export_accounts,
|
||||
get_account_cookies, health_check_accounts, import_accounts, import_cli_auth,
|
||||
quota_warnings, refresh_account, reset_account_usage, set_account_cookies,
|
||||
update_account_label, update_account_status,
|
||||
},
|
||||
api_keys::{
|
||||
api_keys, batch_delete_api_keys, create_api_key, delete_api_key, export_api_keys,
|
||||
import_api_keys, update_api_key_label, update_api_key_status,
|
||||
},
|
||||
auth::{auth_logout, auth_status, login},
|
||||
diagnostics::diagnostics,
|
||||
logs::{clear_logs, log_detail, logs, logs_state, update_logs_state},
|
||||
models::refresh_models,
|
||||
settings::{settings, update_settings},
|
||||
usage::{usage_stats, usage_stats_summary},
|
||||
};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/admin/login", post(login))
|
||||
.route("/admin/auth/status", get(auth_status))
|
||||
.route("/admin/auth/logout", post(auth_logout))
|
||||
.route("/admin/auth/login-start", post(auth_login_start))
|
||||
.route("/admin/auth/code-relay", post(auth_code_relay))
|
||||
.route("/admin/auth/callback", get(auth_callback))
|
||||
// OpenAI OAuth 客户端注册的是 /auth/callback;handler 内仍要求管理员会话。
|
||||
.route("/auth/callback", get(auth_callback))
|
||||
.route("/admin/auth/device-login", post(auth_device_login))
|
||||
.route(
|
||||
"/admin/auth/device-poll/{device_code}",
|
||||
get(auth_device_poll),
|
||||
)
|
||||
.route("/admin/diagnostics", get(diagnostics))
|
||||
.route("/admin/logs", get(logs).delete(clear_logs))
|
||||
.route(
|
||||
"/admin/logs/state",
|
||||
get(logs_state).patch(update_logs_state),
|
||||
)
|
||||
.route("/admin/logs/{log_id}", get(log_detail))
|
||||
.route("/admin/settings", get(settings).patch(update_settings))
|
||||
.route("/admin/refresh-models", post(refresh_models))
|
||||
.route("/admin/usage-stats", get(usage_stats))
|
||||
.route("/admin/usage-stats/summary", get(usage_stats_summary))
|
||||
.route("/admin/accounts", get(accounts).post(create_account))
|
||||
.route("/admin/accounts/health-check", post(health_check_accounts))
|
||||
.route("/admin/accounts/quota-warnings", get(quota_warnings))
|
||||
.route("/admin/accounts/batch-delete", post(batch_delete_accounts))
|
||||
.route(
|
||||
"/admin/accounts/batch-status",
|
||||
post(batch_update_account_status),
|
||||
)
|
||||
.route("/admin/accounts/export", get(export_accounts))
|
||||
.route(
|
||||
"/admin/accounts/{account_id}/refresh",
|
||||
post(refresh_account),
|
||||
)
|
||||
.route(
|
||||
"/admin/accounts/{account_id}/reset-usage",
|
||||
post(reset_account_usage),
|
||||
)
|
||||
.route("/admin/accounts/{account_id}/quota", get(account_quota))
|
||||
.route(
|
||||
"/admin/accounts/{account_id}/cookies",
|
||||
get(get_account_cookies)
|
||||
.post(set_account_cookies)
|
||||
.delete(delete_account_cookies),
|
||||
)
|
||||
.route("/admin/accounts/{account_id}", delete(delete_account))
|
||||
.route(
|
||||
"/admin/accounts/{account_id}/label",
|
||||
patch(update_account_label),
|
||||
)
|
||||
.route(
|
||||
"/admin/accounts/{account_id}/status",
|
||||
patch(update_account_status),
|
||||
)
|
||||
.route("/admin/accounts/import", post(import_accounts))
|
||||
.route("/admin/accounts/import-cli", post(import_cli_auth))
|
||||
.route("/admin/api-keys", get(api_keys).post(create_api_key))
|
||||
.route("/admin/api-keys/export", get(export_api_keys))
|
||||
.route("/admin/api-keys/import", post(import_api_keys))
|
||||
.route("/admin/api-keys/batch-delete", post(batch_delete_api_keys))
|
||||
.route("/admin/api-keys/{key_id}", delete(delete_api_key))
|
||||
.route(
|
||||
"/admin/api-keys/{key_id}/label",
|
||||
patch(update_api_key_label),
|
||||
)
|
||||
.route(
|
||||
"/admin/api-keys/{key_id}/status",
|
||||
patch(update_api_key_status),
|
||||
)
|
||||
}
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod http;
|
||||
pub mod api;
|
||||
pub mod client_keys;
|
||||
pub mod session;
|
||||
pub mod settings;
|
||||
pub mod tasks;
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod api_key;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
@@ -5,7 +5,7 @@ use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
admin::auth::repository::AdminAuthRepository,
|
||||
admin::session::repository::AdminAuthRepository,
|
||||
codex::accounts::service::{AccountService, ValidatedAccountImportError},
|
||||
codex::accounts::{
|
||||
model::AccountStatus,
|
||||
@@ -2,7 +2,7 @@ use chrono::Utc;
|
||||
use tokio::time::{interval, Duration};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::admin::auth::repository::AdminAuthRepository;
|
||||
use crate::admin::session::repository::AdminAuthRepository;
|
||||
use crate::runtime::tasks::types::SchedulerHandle;
|
||||
|
||||
/// 会话清理调度器 - 定期删除过期的管理员会话
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
pub mod cf_path_block;
|
||||
pub mod cloudflare_challenge;
|
||||
pub mod cookies;
|
||||
pub mod jwt;
|
||||
pub mod lifecycle;
|
||||
pub mod model;
|
||||
pub mod models;
|
||||
pub mod pool;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
|
||||
@@ -9,13 +9,6 @@ use crate::{
|
||||
utils::pagination::{decode_cursor, encode_cursor, Page},
|
||||
};
|
||||
|
||||
mod lease;
|
||||
mod quota;
|
||||
mod token;
|
||||
mod usage;
|
||||
|
||||
pub use usage::AccountUsageRepository;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AccountRepositoryError {
|
||||
#[error("database error: {0}")]
|
||||
@@ -155,8 +148,8 @@ pub struct AccountUsageSummary {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AccountRepository {
|
||||
pool: SqlitePool,
|
||||
secret_box: SecretBox,
|
||||
pub(super) pool: SqlitePool,
|
||||
pub(super) secret_box: SecretBox,
|
||||
}
|
||||
|
||||
const INSERT_ACCOUNT_SQL: &str = r"
|
||||
@@ -644,7 +637,7 @@ impl AccountRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fn status_to_db(status: AccountStatus) -> &'static str {
|
||||
pub(crate) fn status_to_db(status: AccountStatus) -> &'static str {
|
||||
match status {
|
||||
AccountStatus::Active => "active",
|
||||
AccountStatus::Expired => "expired",
|
||||
@@ -667,11 +660,13 @@ fn status_from_db(value: &str) -> AccountRepositoryResult<AccountStatus> {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_optional_rfc3339(value: Option<String>) -> AccountRepositoryResult<Option<DateTime<Utc>>> {
|
||||
pub(crate) fn parse_optional_rfc3339(
|
||||
value: Option<String>,
|
||||
) -> AccountRepositoryResult<Option<DateTime<Utc>>> {
|
||||
value.as_deref().map(parse_rfc3339).transpose()
|
||||
}
|
||||
|
||||
fn parse_rfc3339(value: &str) -> AccountRepositoryResult<DateTime<Utc>> {
|
||||
pub(crate) fn parse_rfc3339(value: &str) -> AccountRepositoryResult<DateTime<Utc>> {
|
||||
Ok(DateTime::parse_from_rfc3339(value)?.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
@@ -679,7 +674,7 @@ fn nonnegative_i64_to_u64(value: i64) -> u64 {
|
||||
value.max(0) as u64
|
||||
}
|
||||
|
||||
fn optional_positive_i64_to_u64(value: Option<i64>) -> Option<u64> {
|
||||
pub(crate) fn optional_positive_i64_to_u64(value: Option<i64>) -> Option<u64> {
|
||||
value
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.filter(|value| *value > 0)
|
||||
@@ -0,0 +1,9 @@
|
||||
mod accounts;
|
||||
mod leases;
|
||||
mod quotas;
|
||||
mod tokens;
|
||||
mod usage;
|
||||
|
||||
pub use accounts::*;
|
||||
pub(crate) use accounts::{optional_positive_i64_to_u64, parse_optional_rfc3339, status_to_db};
|
||||
pub use usage::AccountUsageRepository;
|
||||
@@ -185,7 +185,7 @@ mod tests {
|
||||
);
|
||||
let access_token = SecretString::new("access-secret".to_string().into());
|
||||
let refresh_token = SecretString::new("refresh-secret".to_string().into());
|
||||
let expires_at = super::super::parse_rfc3339("2026-06-14T00:00:00Z").unwrap();
|
||||
let expires_at = super::super::accounts::parse_rfc3339("2026-06-14T00:00:00Z").unwrap();
|
||||
|
||||
let write = repository
|
||||
.prepare_token_write(&access_token, Some(&refresh_token), Some(expires_at))
|
||||
@@ -198,6 +198,6 @@ mod tests {
|
||||
write.access_token_expires_at.as_deref(),
|
||||
Some("2026-06-14T00:00:00+00:00")
|
||||
);
|
||||
assert!(super::super::parse_rfc3339(&write.updated_at).is_ok());
|
||||
assert!(super::super::accounts::parse_rfc3339(&write.updated_at).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
repository::{AccountRepository, StoredAccount},
|
||||
},
|
||||
codex::gateway::fingerprint::model::Fingerprint,
|
||||
codex::gateway::transport::client::{
|
||||
codex::gateway::transport::http_client::{
|
||||
build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,8 +13,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
runtime_pool::pool_account_from_stored, AccountImportCounts, AccountImportEntry,
|
||||
AccountService, StoreImportAccountError, ValidatedAccountImportError,
|
||||
pool_sync::pool_account_from_stored, AccountImportCounts, AccountImportEntry, AccountService,
|
||||
StoreImportAccountError, ValidatedAccountImportError,
|
||||
};
|
||||
|
||||
enum StoredImportAccount {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::codex::accounts::{model::AccountStatus, repository::AccountRepository};
|
||||
|
||||
use super::{
|
||||
runtime_pool::pool_account_from_stored, AccountService, AccountServiceError,
|
||||
BatchDeleteAccounts, BatchUpdateAccountStatus, UpdateAccountStatus,
|
||||
pool_sync::pool_account_from_stored, AccountService, AccountServiceError, BatchDeleteAccounts,
|
||||
BatchUpdateAccountStatus, UpdateAccountStatus,
|
||||
};
|
||||
|
||||
impl AccountService {
|
||||
@@ -1,10 +1,10 @@
|
||||
pub mod cookies;
|
||||
pub mod health;
|
||||
pub mod import;
|
||||
pub mod mutation;
|
||||
pub mod lifecycle;
|
||||
pub mod pool_sync;
|
||||
pub mod quota;
|
||||
pub mod refresh;
|
||||
pub mod runtime_pool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
health::skipped_probe_result, runtime_pool::pool_account_from_stored, AccountProbeOutcome,
|
||||
health::skipped_probe_result, pool_sync::pool_account_from_stored, AccountProbeOutcome,
|
||||
AccountProbeResult, AccountService, RefreshAccountError,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod event;
|
||||
pub mod repository;
|
||||
pub mod rotation;
|
||||
pub mod service;
|
||||
@@ -1,7 +1,7 @@
|
||||
use sqlx::{QueryBuilder, Row, Sqlite, SqlitePool};
|
||||
|
||||
use crate::{
|
||||
codex::logs::event::{EventLevel, EventLog},
|
||||
codex::events::event::{EventLevel, EventLog},
|
||||
utils::pagination::{clamp_limit, decode_cursor, encode_cursor, Page},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
codex::logs::{
|
||||
codex::events::{
|
||||
event::EventLog,
|
||||
repository::{EventLogFilters, EventLogRepository},
|
||||
},
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod conversation_identity;
|
||||
pub mod fingerprint;
|
||||
pub mod identity;
|
||||
pub mod installation;
|
||||
pub mod installation_id;
|
||||
pub mod oauth;
|
||||
pub mod protocol;
|
||||
pub mod transport;
|
||||
|
||||
@@ -19,7 +19,7 @@ const OPENAI_OAUTH_AUTHORIZE_ENDPOINT: &str = "https://auth.openai.com/oauth/aut
|
||||
const OPENAI_OAUTH_DEVICE_CODE_ENDPOINT: &str = "https://auth.openai.com/oauth/device/code";
|
||||
const OPENAI_OAUTH_TOKEN_ENDPOINT: &str = "https://auth.openai.com/oauth/token";
|
||||
const OAUTH_CALLBACK_PORT: u16 = 1455;
|
||||
const OAUTH_CALLBACK_PATH: &str = "/auth/callback";
|
||||
const OAUTH_CALLBACK_PATH: &str = "/auth/openai/callback";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OAuthConfig {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pub mod cli_import;
|
||||
pub mod client;
|
||||
pub mod codex_cli;
|
||||
pub mod refresh;
|
||||
pub mod token;
|
||||
|
||||
pub use cli_import::*;
|
||||
pub use client::*;
|
||||
pub use codex_cli::*;
|
||||
pub use refresh::*;
|
||||
pub use token::*;
|
||||
|
||||
@@ -11,13 +11,12 @@ use serde_json::{Map, Value};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
codex::accounts::models::catalog::BackendModelEntry,
|
||||
codex::gateway::fingerprint::model::Fingerprint,
|
||||
codex::gateway::transport::{
|
||||
headers::build_ordered_codex_headers,
|
||||
sse::SseError,
|
||||
types::CodexResponsesRequest,
|
||||
usage::{extract_sse_usage, TokenUsage},
|
||||
usage_events::{extract_sse_usage, TokenUsage},
|
||||
websocket::{
|
||||
append_rate_limit_updates, create_response_via_websocket,
|
||||
create_response_via_websocket_stream, create_response_via_websocket_stream_with_pool,
|
||||
@@ -26,6 +25,7 @@ use crate::{
|
||||
SharedRateLimitUpdates, WebSocketSupportError,
|
||||
},
|
||||
},
|
||||
codex::models::catalog::BackendModelEntry,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod client;
|
||||
pub mod headers;
|
||||
pub mod http_client;
|
||||
pub mod rate_limits;
|
||||
pub mod sse;
|
||||
pub mod types;
|
||||
pub mod usage;
|
||||
pub mod usage_events;
|
||||
pub mod websocket;
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
pub mod accounts;
|
||||
pub mod events;
|
||||
pub mod gateway;
|
||||
pub mod logs;
|
||||
pub mod models;
|
||||
pub mod serving;
|
||||
pub mod tasks;
|
||||
pub mod usage;
|
||||
|
||||
@@ -2,7 +2,7 @@ use chrono::Utc;
|
||||
use sqlx::{Row, SqlitePool};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::codex::accounts::models::catalog::{CodexModelInfo, ModelPlanSnapshot};
|
||||
use crate::codex::models::catalog::{CodexModelInfo, ModelPlanSnapshot};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ModelSnapshotRepositoryError {
|
||||
@@ -3,19 +3,19 @@ use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
codex::accounts::models::{
|
||||
catalog::{ModelCatalog, ModelPlanSnapshot},
|
||||
repository::ModelSnapshotRepository,
|
||||
},
|
||||
codex::accounts::{
|
||||
model::{Account, AccountStatus},
|
||||
pool::AccountPool,
|
||||
repository::AccountRepository,
|
||||
},
|
||||
codex::gateway::fingerprint::model::Fingerprint,
|
||||
codex::gateway::transport::client::{
|
||||
codex::gateway::transport::http_client::{
|
||||
build_reqwest_client, CodexBackendClient, CodexRequestContext,
|
||||
},
|
||||
codex::models::{
|
||||
catalog::{ModelCatalog, ModelPlanSnapshot},
|
||||
repository::ModelSnapshotRepository,
|
||||
},
|
||||
config::AppConfig,
|
||||
};
|
||||
|
||||
@@ -12,15 +12,15 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
codex::accounts::models::catalog::ModelCatalog,
|
||||
codex::accounts::models::service::ModelService,
|
||||
codex::events::event::EventLevel,
|
||||
codex::gateway::protocol::{
|
||||
codex_to_openai::{
|
||||
chat_completion_from_codex_sse, chat_completion_stream_from_codex_sse, openai_error,
|
||||
},
|
||||
openai_to_codex::{translate_chat_to_codex, ChatCompletionRequest},
|
||||
},
|
||||
codex::logs::event::EventLevel,
|
||||
codex::models::catalog::ModelCatalog,
|
||||
codex::models::service::ModelService,
|
||||
codex::serving::dispatch::{
|
||||
classify_upstream_account_retry, no_available_accounts_response,
|
||||
normalize_service_tier_for_upstream, CodexRequestLogContext, CodexUpstreamService,
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde::Serialize;
|
||||
use crate::{
|
||||
codex::gateway::{
|
||||
fingerprint::model::Fingerprint,
|
||||
transport::client::{
|
||||
transport::http_client::{
|
||||
build_reqwest_client, CodexBackendClient, CodexClientError, CodexRequestContext,
|
||||
},
|
||||
},
|
||||
|
||||
+1
-1
@@ -7,9 +7,9 @@ use crate::{
|
||||
model::{Account, AccountStatus},
|
||||
repository::TokenUpdate,
|
||||
},
|
||||
codex::events::event::{EventLevel, EventLog},
|
||||
codex::gateway::oauth::{RefreshFailure, TokenPair},
|
||||
codex::gateway::transport::types::CodexResponsesRequest,
|
||||
codex::logs::event::{EventLevel, EventLog},
|
||||
};
|
||||
|
||||
use super::CodexUpstreamDependencies;
|
||||
@@ -7,7 +7,7 @@ use crate::codex::{
|
||||
model::{Account, AccountStatus},
|
||||
pool::AccountAcquireRequest,
|
||||
},
|
||||
gateway::transport::{client::CodexClientError, rate_limits::cooldown_with_jitter},
|
||||
gateway::transport::{http_client::CodexClientError, rate_limits::cooldown_with_jitter},
|
||||
};
|
||||
|
||||
use super::{usage::record_request_attempt, CodexUpstreamDependencies};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
mod account_refresh;
|
||||
pub mod affinity;
|
||||
mod audit;
|
||||
pub mod fallback;
|
||||
mod limits;
|
||||
pub mod refresh;
|
||||
// 上游调度辅助命名为 routing,避免出现 dispatch::dispatch 的模块套娃。
|
||||
pub mod routing;
|
||||
pub mod stream;
|
||||
mod stream_audit;
|
||||
pub mod usage;
|
||||
|
||||
use std::{sync::Arc, time::Instant};
|
||||
@@ -31,26 +31,26 @@ use crate::{
|
||||
pool::{AccountAcquireRequest, AccountPool},
|
||||
repository::AccountRepository,
|
||||
},
|
||||
codex::events::{
|
||||
event::{EventLevel, EventLog},
|
||||
service::LogService,
|
||||
},
|
||||
codex::gateway::conversation_identity::{build_conversation_identity, ensure_prompt_cache_key},
|
||||
codex::gateway::fingerprint::model::Fingerprint,
|
||||
codex::gateway::identity::{build_conversation_identity, ensure_prompt_cache_key},
|
||||
codex::gateway::installation::get_installation_id,
|
||||
codex::gateway::installation_id::get_installation_id,
|
||||
codex::gateway::oauth::TokenRefresher,
|
||||
codex::gateway::protocol::codex_to_openai::openai_error,
|
||||
codex::gateway::transport::{
|
||||
client::{
|
||||
http_client::{
|
||||
build_reqwest_client, CodexBackendClient, CodexBackendStream,
|
||||
CodexBackendWebSocketStream, CodexClientError, CodexRequestContext,
|
||||
},
|
||||
types::CodexResponsesRequest,
|
||||
usage::TokenUsage,
|
||||
usage_events::TokenUsage,
|
||||
websocket::{
|
||||
transport_for_request, CodexTransport, CodexWebSocketError, CodexWebSocketPool,
|
||||
},
|
||||
},
|
||||
codex::logs::{
|
||||
event::{EventLevel, EventLog},
|
||||
service::LogService,
|
||||
},
|
||||
config::AppConfig,
|
||||
};
|
||||
|
||||
@@ -65,13 +65,13 @@ pub(crate) use self::{
|
||||
};
|
||||
|
||||
use self::affinity::{compute_variant_hash, SessionAffinityMap, SessionAffinityRepository};
|
||||
use self::audit::{StreamAudit, WebSocketStreamAudit};
|
||||
use self::fallback::{
|
||||
apply_upstream_account_retry_with_deps, apply_upstream_retry_and_acquire_fallback_with_deps,
|
||||
};
|
||||
use self::stream_audit::{StreamAudit, WebSocketStreamAudit};
|
||||
use self::{
|
||||
account_refresh::refresh_account_after_unauthorized,
|
||||
limits::apply_rate_limit_headers_with_deps,
|
||||
refresh::refresh_account_after_unauthorized,
|
||||
routing::request_domain,
|
||||
stream::{completed_response_metadata, ensure_stream_metadata},
|
||||
usage::{record_empty_response_with_deps, record_usage_with_deps},
|
||||
@@ -200,7 +200,7 @@ impl CodexUpstreamService {
|
||||
request: &CodexResponsesRequest,
|
||||
account: &Account,
|
||||
request_id: &str,
|
||||
) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError>
|
||||
) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError>
|
||||
{
|
||||
send_codex_request_with_refresh_retry_deps(&self.deps, request, account, request_id).await
|
||||
}
|
||||
@@ -466,7 +466,7 @@ async fn send_codex_request_with_refresh_retry_deps(
|
||||
request: &CodexResponsesRequest,
|
||||
account: &Account,
|
||||
request_id: &str,
|
||||
) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError> {
|
||||
) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError> {
|
||||
let result = match send_codex_request(deps, request, account, request_id).await {
|
||||
Err(CodexClientError::Upstream {
|
||||
status,
|
||||
@@ -503,7 +503,7 @@ async fn send_codex_request(
|
||||
request: &CodexResponsesRequest,
|
||||
account: &Account,
|
||||
request_id: &str,
|
||||
) -> Result<crate::codex::gateway::transport::client::CodexBackendResponse, CodexClientError> {
|
||||
) -> Result<crate::codex::gateway::transport::http_client::CodexBackendResponse, CodexClientError> {
|
||||
let request_domain = request_domain(&deps.config.api.base_url);
|
||||
let cookie_header = match (deps.cookie_repository.as_ref(), request_domain.as_deref()) {
|
||||
(Some(repo), Some(domain)) => repo.cookie_header(&account.id, domain).await.ok().flatten(),
|
||||
|
||||
@@ -8,11 +8,11 @@ use crate::{
|
||||
codex::gateway::transport::types::CodexResponsesRequest,
|
||||
codex::{
|
||||
accounts::pool::AccountPool,
|
||||
events::event::EventLevel,
|
||||
gateway::transport::{
|
||||
usage::extract_sse_usage,
|
||||
usage_events::extract_sse_usage,
|
||||
websocket::{append_rate_limit_updates, CodexWebSocketError, SharedRateLimitUpdates},
|
||||
},
|
||||
logs::event::EventLevel,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
codex::accounts::repository::UsageDelta, codex::gateway::transport::usage::TokenUsage,
|
||||
codex::accounts::repository::UsageDelta, codex::gateway::transport::usage_events::TokenUsage,
|
||||
};
|
||||
|
||||
use super::CodexUpstreamDependencies;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
codex::gateway::protocol::{
|
||||
codex_to_openai::openai_error, openai_to_codex::ChatCompletionRequest,
|
||||
},
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
admin::auth::service::AdminAuthPoolSummary,
|
||||
admin::session::service::AdminAuthPoolSummary,
|
||||
codex::{accounts::pool::AccountCapacitySummary, gateway::fingerprint::model::Fingerprint},
|
||||
config::AppConfig,
|
||||
platform::http::middleware::RequestId,
|
||||
platform::http::request_id::RequestId,
|
||||
runtime::state::AppState,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
codex::gateway::protocol::codex_to_openai::openai_error,
|
||||
codex::gateway::transport::client::CodexClientError,
|
||||
codex::gateway::transport::http_client::CodexClientError,
|
||||
};
|
||||
|
||||
pub(crate) fn codex_client_error_response(error: CodexClientError) -> (StatusCode, Json<Value>) {
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::{codex::accounts::models::catalog::ModelCatalog, runtime::state::AppState};
|
||||
use crate::{codex::models::catalog::ModelCatalog, runtime::state::AppState};
|
||||
|
||||
use super::{
|
||||
auth::authorize_client_api_key,
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::{
|
||||
Extension,
|
||||
};
|
||||
|
||||
use crate::{platform::http::middleware::RequestId, runtime::state::AppState};
|
||||
use crate::{platform::http::request_id::RequestId, runtime::state::AppState};
|
||||
|
||||
use super::{auth::authorize_client_api_key, errors::missing_client_api_key_response};
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::{
|
||||
codex::accounts::models::service::ModelService,
|
||||
codex::events::event::EventLevel,
|
||||
codex::gateway::fingerprint::model::Fingerprint,
|
||||
codex::gateway::protocol::codex_to_openai::openai_error,
|
||||
codex::gateway::transport::types::CodexResponsesRequest,
|
||||
codex::logs::event::EventLevel,
|
||||
codex::models::service::ModelService,
|
||||
codex::serving::dispatch::{
|
||||
affinity::SessionAffinityRepositoryResult, classify_upstream_account_retry,
|
||||
completed_response_json, no_available_accounts_response,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user