Skip to content

Commit 833d75b

Browse files
committed
docs: 共享文档去本机化,本机环境另立 docs/local-environment.md
上一轮把「本机怎么重启、本机日志在哪」直接写进了 README/TECHNICAL/AGENTS,这些内容 换个环境就不成立,不该随仓库分发。按「换台机器就不成立的 → 本地文档;换环境也成立的 → 共享文档」重新划分。 - 新增 docs/local-environment.md(docs/ 已在 .gitignore):本机路径、工具版本、 launchd label/plist 位置、端口、DB 与密钥位置、当前状态快照、两个验证坑与 B5 事故复盘 - README「部署注意」:保留通用的「改 src/ 必须重启」机制与各部署形态的重启命令 (launchd/systemd/docker 三种都属于通用文档),删掉本机专有日志路径与「本机实际运行 的 plist 一旦丢失就无法复现」;macOS 一节改为提醒模板里四处绝对路径需按实际位置修改, 并指出 docs/local-environment.md(不进仓库,需自建) - AGENTS.md:规则保留,把本机 launchd 命令换成指向本地文档 - TECHNICAL.md:技术栈表删掉「本机 3.12.14 / 本机 0.12.x」等易漂移的环境版本, 只留兼容边界(>=3.12);证据出处类「本机 N 条样本」改为环境中立的「开发环境」 - PROPOSAL/README.en 同步 行为无变化,仅文档;ruff + 1426 passed / 行+分支 100%(已移走 .env 验证不依赖本地环境)。
1 parent ee0133d commit 833d75b

5 files changed

Lines changed: 33 additions & 31 deletions

File tree

‎AGENTS.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
- 前端(web/):`pnpm exec tsc --noEmit` + `pnpm exec vitest run` + `pnpm build`
1111
- 注意平台差异:本地 macOS 通过不代表 CI(ubuntu-latest)通过;平台相关分支
1212
(platform.system/machine 等)必须用 monkeypatch 测全所有分支
13-
- **测试不得依赖本地 `.env`**:本地 `.env` 会补上 `APP_SECRET` 等必填项,CI 没有。
13+
- **测试不得依赖本地 `.env`**:本地 `.env` 补上 `APP_SECRET` 等必填项,CI 没有。
1414
构造 `Settings` 必须显式传值;验证时临时移走 `.env` 再跑全量(B5 首条 CI 即栽于此)
15-
- **重启本地服务后才算验证完成**:改 `src/` 必须重启 launchd 服务
16-
(`launchctl kickstart -k gui/$(id -u)/com.coding2api`),只改前端则重新构建即可。
17-
否则会出现「新前端 + 旧后端」错配:新端点 404,前端弹出与真实原因无关的兜底文案
18-
(B5 上线时「创建用户失败」的真实原因是后端进程还跑着迁移前的代码)
15+
- **改 `src/` 必须重启服务才算验证完成**(只改前端则重新构建即可)。否则出现
16+
「新前端 + 旧后端」错配:新端点 404,前端弹出与真实原因无关的兜底文案。
17+
具体重启命令随部署形态而定,见 `docs/local-environment.md` 或 README「部署注意」
1918

2019
## GitHub Actions(硬约束)
2120

‎PROPOSAL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ Provider 承担上游协议私有部分:发请求、解析事件、分类错
174174

175175
TTL(`CONVERSATION_STICKY_SECONDS`,默认 1h,≤0 关闭)内固定复用,不再按到期积分 / 健康度重排——对话中途换号会触发上游风控并丢掉上游侧提示词缓存。请求体带 `user_id`(顶层或 `metadata` 内)时**不派生**第 2 级兜底键:同一用户的并行对话消息前缀可能相同,派生会把它们误钉到同一凭证。
176176

177-
> 键名核实状态:`prompt_cache_key`(OpenAI 官方顶层参数)与 `metadata.user_id`(Anthropic Messages API 官方字段)已核实;`conversation_id`/`conversationId`/顶层 `user_id` 非两家标准键,属客户端惯用约定,本机 71 份真实 dump(PI 客户端)中**未观测到**,作为兼容探测接受(命中即用、未命中无害)。
177+
> 键名核实状态:`prompt_cache_key`(OpenAI 官方顶层参数)与 `metadata.user_id`(Anthropic Messages API 官方字段)已核实;`conversation_id`/`conversationId`/顶层 `user_id` 非两家标准键,属客户端惯用约定,开发环境 71 份真实 dump(PI 客户端)中**未观测到**,作为兼容探测接受(命中即用、未命中无害)。
178178
179179
**手动 pin 优先于粘性**:存在可选(enabled、未禁用、未冷却)的 pinned 凭证时粘性让位,否则管理员显式「指定」会在对话中途无形失效。粘住的凭证报错仍走正常轮换,成功后重新粘到实际服务的凭证。指纹链掺入用户名,防不同用户的相同消息数组串到同一凭证;条目纯内存,重启后丢粘性只影响一轮选号。
180180

‎README.en.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ launchctl kickstart -k gui/$(id -u)/com.coding2api # macOS (launchd)
9898
docker compose up -d --force-recreate # Docker / compose
9999
sudo systemctl restart coding2api # systemd
100100

101-
# Confirm the upgrade took effect
102-
sqlite3 data/coding2api.sqlite3 "PRAGMA user_version;" # expect 14
103-
curl -s -o /dev/null -w '%{http_code}\n' .../api/users # expect 401; 404 = old backend
101+
# Confirm the upgrade took effect (check the version first, then the routes)
102+
sqlite3 data/coding2api.sqlite3 "PRAGMA user_version;" # expect 14
103+
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/api/users # expect 401; 404 = old backend
104104
```
105105

106106
Schema upgrades are additive — `users` / `audit_events` are new tables and existing rows
107107
(credentials, usage) are preserved. The restart performs the migration and bootstrap in one
108-
step. Details in [`TECHNICAL.md` §6.4](TECHNICAL.md) (Chinese).
108+
step. Details in [`TECHNICAL.md` §6.4](TECHNICAL.md) and [`README.md`](README.md) (Chinese).
109109

110110
## Documentation
111111

‎README.md‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ codex -c "model_providers.coding2api={ name='coding2api', base_url='http://127.0
140140

141141
支持文本、流式正文、思考摘要(`reasoning` item)、函数工具调用与 `finish_reason=length` → `response.incomplete`。**不支持** `store=true`、`previous_response_id`(服务端无状态,不假装支持)、Responses 私有工具(`web_search` / `computer` / `custom` 等)——一律显式 400,不静默降级。`include=["reasoning.encrypted_content"]`(Codex 每轮必带)接受但忽略,本网关不产加密推理内容。
142142

143-
> 验证边界:本机无 Codex CLI;协议形状取自官方 `openai` SDK 类型并以其为客户端跑通全部契约,另对真实上游冒烟,未经真实 Codex CLI 端到端验证。
143+
> 验证边界:开发环境无 Codex CLI;协议形状取自官方 `openai` SDK 类型并以其为客户端跑通全部契约,另对真实上游冒烟,未经真实 Codex CLI 端到端验证。
144144
145145
### 余额查询
146146

@@ -302,22 +302,19 @@ sudo systemctl restart coding2api
302302
# 裸跑:Ctrl-C 后重新执行启动命令
303303
```
304304

305-
**升级老部署(含 user_version 13 → 14)时的确认清单**:
305+
**确认升级已生效**(先查版本,再查路由):
306306

307307
```bash
308-
# 1) 版本已迁移(应为 14)与账号已导入
308+
# 1) schema 版本已迁移(期望 14)且账号已导入
309309
sqlite3 data/coding2api.sqlite3 "PRAGMA user_version; SELECT username, role, enabled FROM users;"
310-
# 2) 路由存在:应返回 401(未登录)而不是 404
310+
# 2) 路由存在:期望 401(未登录),404 = 旧后端进程
311311
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/api/users
312-
# 3) 启动日志里能看到引导结果
313-
grep 账号引导 logs/launchd.err.log | tail -2
312+
# 3) 启动日志里能看到引导结果(日志路径见下文「日志」)
314313
```
315314

316-
升级是**只加不改**的:`users` / `audit_events` 为新增表,凭证与统计原样保留(不删列、不重建表)。首次启动会把 `USERS_FILE` 里的用户导入一次(已存在的用户名不覆盖),再把 `ADMIN_USERNAMES` 点名者提权为 `admin`——**这一步只做一次**,此后角色只在管理台「用户管理」页改。
317-
318315
### macOS(launchd)
319316

320-
仓库在 `deploy/launchd/com.coding2api.plist` 留了一份模板(本机实际运行的 plist 一旦丢失就无法复现)。它把后端交给 launchd 常驻,并按后端 `:8000` 直接服务前端产物。
317+
仓库在 `deploy/launchd/com.coding2api.plist` 留了一份模板,把后端交给 launchd 常驻,并按后端 `:8000` 直接服务前端产物。**模板里的绝对路径需要按你的实际部署位置修改**(`ProgramArguments`、`WorkingDirectory`、两个日志 `Standard*Path` 共四处)。
321318

322319
```bash
323320
cp deploy/launchd/com.coding2api.plist ~/Library/LaunchAgents/
@@ -330,7 +327,11 @@ launchctl bootout gui/$(id -u)/com.coding2api #
330327

331328
- **后端在 `http://127.0.0.1:8000/` 直接服务 `web/dist`**(`src/webapp/static.py` 用 `FileResponse` 每次请求现读磁盘),所以只改前端时**构建完刷新即可,无需重启后端**;`localhost:5173` 是 Vite 开发态(HMR),两者可并存。**注意这条只对前端成立**——改了 `src/` 下的后端代码必须重启进程(见上文「升级后必须重启后端」)。
332329
- **构建失败不阻断启动**:plist 是 `KeepAlive` + `ThrottleInterval=10`,若构建失败就退出,launchd 会每 10 秒重拉一次变成死循环。脚本改为「构建失败 → 警告 → 用既有产物继续起服务」。
333-
- **pnpm 装在 nvm 下**:launchd 不读 shell rc,PATH 只有 plist 里的系统目录,`build-web.sh` 会自己从 `~/.nvm/alias/default` 解析出 node/pnpm 路径。手动构建用 `./scripts/build-web.sh`(`--force` 强制重建)。
330+
- **pnpm 常装在 nvm 下**:launchd 不读 shell rc,PATH 只有 plist 里的系统目录,`build-web.sh` 会自己从 `~/.nvm/alias/default` 解析出 node/pnpm 路径(非 nvm 安装则回落到 PATH)。手动构建用 `./scripts/build-web.sh`(`--force` 强制重建)。
331+
332+
> **本机环境(不进仓库)**:当前开发机的路径、工具版本、端口、日志位置与特有操作记在
333+
> `docs/local-environment.md`——该目录已在 `.gitignore` 中,克隆仓库的人不会有这个文件,
334+
> 需要时照上面的结构自建一份即可。
334335
335336
## 日志
336337

‎TECHNICAL.md‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ PROPOSAL.md 定方向,本文档定实现。每个模块标注来源决策(Q
88

99
| 层 | 选型 | 版本 | 决策 |
1010
|---|---|---|---|
11-
| 运行时 | Python | 3.12(本机 3.12.14;`requires-python >=3.12`,CI/Dockerfile 均 3.12) | Q2 |
12-
| 包管理 | uv(venv + pyproject.toml + uv.lock) | 本机 0.12.x | T-Q1 |
11+
| 运行时 | Python | 3.12(`requires-python >=3.12`,CI/Dockerfile 均 3.12) | Q2 |
12+
| 包管理 | uv(venv + pyproject.toml + uv.lock) | 锁定版本见 `uv.lock` | T-Q1 |
1313
| Web | FastAPI + Uvicorn | 0.141 / 0.52 | Q2=A |
1414
| 配置 | pydantic-settings | 2.15 | T-Q3 |
1515
| HTTP | httpx 双客户端(流式/短请求分离) | 0.28.1 | T-Q4 |
@@ -241,7 +241,7 @@ def health(q: Quota | None) -> HealthScore:
241241
| 判据 | 实测结论 |
242242
|---|---|
243243
| 仅 reasoning 无正文 | 真实存在,但**只在客户端下发 `max_tokens` 时**出现:glm-5.3-flash `max_tokens` 8/24/64 → content=0 / reasoning=33/96/307,`finish_reason=length`。生产路径(PI 等)只发 `reasoning_effort`、不发任何 max 键,本网关也不做 max 键映射 → 该形态在生产不可达。凭空加启发式会把「模型就是想空回」误判成截断 |
244-
| 空正文无工具调用 | 本机 5744 条统计 **0 例**,无证据 |
244+
| 空正文无工具调用 | 开发环境 5744 条统计 **0 例**,无证据 |
245245
| 代码块未闭合 | 纯启发式,**无任何实测支撑**,不做 |
246246

247247
另两条实测事实(影响 `length` 可观测性):`max_completion_tokens` 被 CB 上游**完全忽略**(`=1` 仍出 59 tokens);`max_tokens` 才生效(精确截断 + `length`);两键同发时后者胜出;TRAE 对两个键**都不生效**(80/80/80 字符)。CB 的 `usage.reasoning_tokens` 恒为 `None`(口径差异,见 §3.1);`enable_thinking: false` 被上游**忽略**(仍产 reasoning 且计入 `max_tokens`)。
@@ -250,7 +250,7 @@ def health(q: Quota | None) -> HealthScore:
250250

251251
### 3.5 会话粘性键(B1.5)与模型元数据/黑名单(B1.6)
252252

253-
**会话粘性键**(`src/engine/affinity.py`,详见 [PROPOSAL.md 会话粘性](PROPOSAL.md)):键优先级 `conversation_id` > `conversationId` > `prompt_cache_key`(同键名先 `metadata` 对象再请求体顶层)→ 无显式标识时回落「用户名 + 消息增量前缀指纹」;请求体带 `user_id`(顶层或 `metadata` 内)时不派生兜底键。键名核实:`prompt_cache_key` 是 OpenAI 官方顶层参数、`metadata.user_id` 是 Anthropic Messages API 官方字段;`conversation_id`/`conversationId`/顶层 `user_id` 非两家标准键,本机 71 份真实 dump(PI 客户端)**未观测到**,作为兼容探测接受(命中即用、未命中无害)。
253+
**会话粘性键**(`src/engine/affinity.py`,详见 [PROPOSAL.md 会话粘性](PROPOSAL.md)):键优先级 `conversation_id` > `conversationId` > `prompt_cache_key`(同键名先 `metadata` 对象再请求体顶层)→ 无显式标识时回落「用户名 + 消息增量前缀指纹」;请求体带 `user_id`(顶层或 `metadata` 内)时不派生兜底键。键名核实:`prompt_cache_key` 是 OpenAI 官方顶层参数、`metadata.user_id` 是 Anthropic Messages API 官方字段;`conversation_id`/`conversationId`/顶层 `user_id` 非两家标准键,开发环境 71 份真实 dump(PI 客户端)**未观测到**,作为兼容探测接受(命中即用、未命中无害)。
254254

255255
**模型元数据**(B1.6,2026-09-21 两边模型配置接口实测):两边上游都直接给出推理元数据,网关透传为 `/v1/models` 的 OpenAI 额外字段:
256256

@@ -357,9 +357,9 @@ response.completed | response.incomplete
357357
| `previous_response_id` → 400 | Codex HTTP 路径**不发**该字段(`ResponsesApiRequest` 无此字段) | 保持 400(本网关无状态) |
358358
| `store=true` → 400 | Codex 恒发 `store=false` | 保持 400(`false` 放行) |
359359

360-
**客户端取证结论**(`openai/codex`,非本机实测):Codex CLI 按 SSE 的 `event:` 行(而非 `data.type`)分派事件,所以两个字段都必须发;`response.completed.response` 在它那边是**强类型解析**(`id` 必填、`usage` 含 `input_tokens`/`output_tokens`/`total_tokens`),解析失败即整轮报错——故 `response` 对象按官方必填字段完整发出。Codex 回传的历史里 `reasoning`/`compaction` 只有密文、没有 chat 等价物,**有意无损丢弃**(不是静默降级:不影响回答质量,仅不再回传),其余 Responses 私有 item 类型(`local_shell_call`/`custom_tool_call` 等)显式 400。
360+
**客户端取证结论**(`openai/codex`,非开发环境实测):Codex CLI 按 SSE 的 `event:` 行(而非 `data.type`)分派事件,所以两个字段都必须发;`response.completed.response` 在它那边是**强类型解析**(`id` 必填、`usage` 含 `input_tokens`/`output_tokens`/`total_tokens`),解析失败即整轮报错——故 `response` 对象按官方必填字段完整发出。Codex 回传的历史里 `reasoning`/`compaction` 只有密文、没有 chat 等价物,**有意无损丢弃**(不是静默降级:不影响回答质量,仅不再回传),其余 Responses 私有 item 类型(`local_shell_call`/`custom_tool_call` 等)显式 400。
361361

362-
**验证状态**:本机无 Codex CLI、无 Responses 参考实现,故以官方 `openai` SDK(`responses.create(stream=True/False)`,含工具调用)作权威客户端跑通全部契约,并对**真实 CB 上游**冒烟(流式 / 非流式 / 工具调用三条,模型 `deepseek-v4-pro`),另用按 `codex-rs` 源码构造的真实请求体核对入站映射。**未经真实 Codex CLI 端到端验证**,剩余风险:客户端行为细节(如 reasoning item 无 `encrypted_content` 时的降级路径)。
362+
**验证状态**:开发环境无 Codex CLI、无 Responses 参考实现,故以官方 `openai` SDK(`responses.create(stream=True/False)`,含工具调用)作权威客户端跑通全部契约,并对**真实 CB 上游**冒烟(流式 / 非流式 / 工具调用三条,模型 `deepseek-v4-pro`),另用按 `codex-rs` 源码构造的真实请求体核对入站映射。**未经真实 Codex CLI 端到端验证**,剩余风险:客户端行为细节(如 reasoning item 无 `encrypted_content` 时的降级路径)。
363363

364364
---
365365

@@ -777,12 +777,14 @@ B5 的实际症状值得记住:新建用户报「用户名可能已存在,
777777
**诊断顺序**(先确认版本,再查业务):
778778

779779
```bash
780-
sqlite3 data/coding2api.sqlite3 "PRAGMA user_version;" # 期望 14
781-
curl -s -o /dev/null -w '%{http_code}\n' .../api/users # 期望 401,404 = 旧后端
782-
grep 账号引导 logs/launchd.err.log | tail -2 # 引导只跑一次
780+
# 1) schema 版本已迁移(期望 14)且账号已导入
781+
sqlite3 data/coding2api.sqlite3 "PRAGMA user_version; SELECT username, role, enabled FROM users;"
782+
# 2) 路由存在:期望 401(未登录),404 = 旧后端进程
783+
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/api/users
784+
# 3) 引导日志(路径随部署形态而定,见 README「日志」)
783785
```
784786

785-
**结论**:改 `src/` 必须重启进程;改 `web/` 只需重新构建。升级 schema 时重启同时完成迁移与引导(bootstrap 幂等)。
787+
**结论**:改 `src/` 必须重启进程;改 `web/` 只需重新构建。升级 schema 时重启同时完成迁移与引导(bootstrap 幂等):`users` / `audit_events` 为**新增表**,凭证与统计原样保留(不删列、不重建表)。
786788

787789
---
788790

@@ -832,7 +834,7 @@ fixture 存于 `src/provider/fixtures/`(真实 SSE/JSON 样本,覆盖正文
832834
- **手写 SQL 而非 ORM**:10 张表规模下 ORM 收益为负
833835
- **polling OAuth 不转回调**(Q17=C):上游协议决定;TRAE 回调走主端口 + `PUBLIC_BASE_URL`
834836
- **v1 无 Anthropic**(Q8=A):Event 层已预留,v1.1 只加 `compat/anthropic/` 适配器
835-
- **Responses 出口只做 Codex CLI 用到的子集**(Q32,详见 §3.7):不做 `store=true` / `previous_response_id`(服务端无状态,不假装支持);`include=["reasoning.encrypted_content"]` 按实测接受并忽略——Codex CLI 每轮必带,400 会直接打死主客户端;流式终止用 `response.completed` / `response.incomplete` / `response.failed`,**不发 `[DONE]`**(Responses 协议无该哨兵)。形状取自官方 `openai` SDK 类型并用其作客户端验证,对真实 CB 上游冒烟过;**未经真实 Codex CLI 端到端验证**(本机无 CLI)
837+
- **Responses 出口只做 Codex CLI 用到的子集**(Q32,详见 §3.7):不做 `store=true` / `previous_response_id`(服务端无状态,不假装支持);`include=["reasoning.encrypted_content"]` 按实测接受并忽略——Codex CLI 每轮必带,400 会直接打死主客户端;流式终止用 `response.completed` / `response.incomplete` / `response.failed`,**不发 `[DONE]`**(Responses 协议无该哨兵)。形状取自官方 `openai` SDK 类型并用其作客户端验证,对真实 CB 上游冒烟过;**未经真实 Codex CLI 端到端验证**(开发环境无 CLI)
836838
- **不做 reasoning 注入 / effort 档位映射**(原 B1.2,实测后取消):原计划对「强制推理模型族」注入 `thinking` + `reasoning_effort` 并回填历史 `reasoning_content`,实测前提不成立——(1)客户端已自带 `reasoning_effort`(仅 `low`/`medium`)且上游直接接受;(2)客户端已回传历史 `reasoning_content` 且上游接受;(3)原计划的默认模型清单与实际在用命名无关,且 `glm-5.1` 在 `MODEL_BLOCKLIST` 里,硬编码白名单会空转;(4)真要做「客户端丢弃时回填」必须服务端存对话内容,与脱敏纪律冲突。参考实现 IceeAn/codebuddy2api 走相反取向(对白名单模型强制 `reasoning_effort=max` 覆盖客户端),属单来源且会改写客户端意图,不采纳
837839
- **统计一律以 `usage_hourly` 为准**:`overview` / `by_provider` / `timeline` / `model-timeline` 均读小时汇总,只有 `events`(逐请求明细)读 `usage_events`。统一口径是为了让选「全部」时总览与图表同值(明细只留 90 天,汇总永久)。代价:最近 ≤5 分钟未进汇总的请求不计入,刷新一次即可
838840
- **小时汇总双写**:`record()` 写明细的同时增量累加当前小时行,新请求立即可见于统计页(不依赖 5 分钟一轮的 rollup);`rollup_hourly` 仍每 5 分钟全量重算作对账,两者结果一致(幂等)

0 commit comments

Comments
 (0)