# 发布前自检清单 > **一句话**:我们平时测的是「能不能用」,用户测的是「用得对不对」。 > 这份清单把后者会盯的每一项,变成前者必须过的关。 > > **用法**:每版发布前从头过一遍,**全部打勾才提交**。 > 跳过的项必须在 §9 执行记录里写原因——「跳过」本身不是问题,**没记录的跳过**才是。 > > 配套:`docs/EXTENDING.md` §7 是「新增一个适配器」的专项清单; > 这一份是「每次发布」的通用清单,两者不重叠。 ## 0. 三条铁律 | 编号 | 铁律 | 踩过的坑 | |---|---|---| | **L1** | 用户会盯的每个数字,日志里必须**有字段** | #2 缓存命中率:`cacheReadTokens` 根本没落盘,缓存真掉到 0% 也看不见 | | **L2** | 兜底路径必须和在线路径**字段同构** | #4 内置模型表少写 `efforts`,一断网推理档位就消失 | | **L3** | 用户会打断的每一处,都要能**优雅停下** | #7 点了停止,`router.execute` 还在换号打上游 | 判断有没有违反 L1 的方法很土但有效:**把用户会问的问题列出来,逐个问「日志里哪个字段能回答它」。** 答不上来的,就是这个版本的盲区。 --- ## 1. 机械门禁(5 分钟,必须全绿) ```powershell npm run build # src → lib(类型擦除) npm run check # 只校验可擦除语法,不写文件 git diff --quiet -- lib # 构建产物与 src 同步:有 diff 说明你改了 src 没重新构建 npm test # 宿主单测 + 客户端 + 冒烟 npm run scan # 公开仓库脱敏扫描 ``` - [ ] 以上 5 条全绿,记录数字:`宿主 __/__ · 客户端 __/__ · 冒烟 ✓` - [ ] 版本号 **4 处**一致:`package.json`、`package-lock.json`(2 处)、`plugin.json` - [ ] `README.md` 里写死的用例数字与实际一致(改过测试就必须同步) - [ ] `git status --short` 只含预期文件 - [ ] 新增文件要么在 `package.json` 的 `files` 白名单里,要么确认本来就不发布 - [ ] 动了 `src/**` 就必须重新构建并提交 `lib/**`(宿主读的是 `lib/`,不是 `src/`) > `npm run scan` 只覆盖机械可判的那部分(手机号 / 邮箱 / Bearer / 凭证赋值 / 疑似真 uid)。 > 判不了的(比如一段抓包 JSON 里的**结构**可能反推出账号)留给 §7 的人工项。 > 它扫的是源码仓的 `test/` `docs/` 和根目录文件;`scripts/` 不随包发布,所以装在用户机器上没有这个命令。 ## 2. 观测面(用户问「怎么会这样」时拿得出证据) - [ ] `usage` 全字段落盘:prompt / completion / reasoning / **cacheRead** / **cacheWrite** - [ ] 关键响应头落盘:`Retry-After`、上游 request-id、渠道校验头 - [ ] 错误原文落盘且不被截到看不见(当前上限 2000 字符) - [ ] 每条「失败」都带**账号 + 分类**,不许出现「免账号」 **判定**:随便挑一条真实请求,日志能直接回答三件事—— 缓存命中多少 / 什么时候恢复 / 上游原话是什么。答不上就是流程没落地,不是「这次刚好没数据」。 ## 3. 兜底路径(网络断了、接口挂了、解析失败的时候) - [ ] 内置表与在线数据的字段集一致:id / 显示名 / **推理档位** / 输入模态 / 倍率 - [ ] 兜底**不覆盖**落盘缓存(有缓存先用缓存,否则一次网络抖动就把 21 条真实目录换成 11 条内置表) - [ ] 走兜底时日志是 `warn`,且能从 `modelListSource()` 看出是 `'fallback'` **判定**:把端点改成不可达跑一次 → 模型列表字段与在线时一致;恢复后缓存没被污染。 ## 4. 错误语义(同一个状态码可以有完全不同的意思) - [ ] 分类顺序:**文案 / 业务码 → 状态码**(文案优先,状态码只作兜底) - [ ] 每个 `ErrorKind` 至少一条**反向**用例:「不得被判成 X」 - [ ] 不换号的 kind 有显式断言:`channel_reject`、`aborted` - [ ] 新 kind 三处同时有:宿主 `ERROR_LABELS`、客户端 `FAILURE_TEXT` / `FAILURE_TONE`、`docs/ERROR-HANDLING.md` **判定**:`grep` 每个 kind 名,`src/` 与 `test/` 里各至少出现 2 次(一次分类、一次反向)。 ## 5. 用户行为(他会怎么用手,而不是怎么读文档) - [ ] 中途取消:不换号、不计失败、日志显示「已取消」 - [ ] 取消后不再发上游请求(`requests.jsonl` 不再新增记录) - [ ] 长退避可被打断,不是「点了没反应」 - [ ] 连点两次停止不会把账号状态搞乱 ## 6. 真实负载(只能真机,单测替不了) ```powershell node test/live-load.mjs --agent workbuddy --list # 先看目录(含档位),不发对话 node test/live-load.mjs --agent workbuddy --rounds 3 # 同前缀 3 轮,量缓存命中 node test/live-load.mjs --agent workbuddy --requests 16 --concurrency 8 # 并发压一下 ``` - [ ] 真账号**并发 3–5、连续 20 次**:记录成功率与 P50 / P95 耗时 - [ ] 至少一条国内渠道 + 一条国际渠道 - [ ] 主动撞一次 429:确认遵守 `Retry-After`,账号没被降权 / 冷却 - [ ] 缓存:同一前缀连续 3 轮,第 2、3 轮 `cacheReadTokens > 0` - [ ] 断网 / 超时 / 空响应各来一次:提示是人话,且带「原始原因:<原文>」 > `live-load.mjs` 走的是**构建后的适配器 + 真实凭证**,不经过 DSH 运行时, > 所以它的请求**不会**进 `requests.jsonl`;要同时看日志就用界面真发一条。 > 它会逐个试账号(上游风控是**按账号**判的),并把 429 的 `kind` 与 `retryAfterMs` 打出来。 ## 7. 结论要有验证方式(不许有裸的肯定句) - [ ] 文档里每条结论都标了验证方式:`真机 A/B` / `单测` / `抓包` / `未验证` - [ ] 没验证的标 🟡(沿用 `docs/KNOWN-ISSUES.md` 的图例),**禁止无标记的肯定句** - [ ] 公开仓库里不出现真机 uid / token / 余额 / request-id(`npm run scan` + 人眼过一遍新增文档) - [ ] 抓包样本进仓库前先脱敏,且保留「结构」而不是「值」 ## 8. issue 回流(每条 issue 都要变成资产) - [ ] 每条 issue 修完 → 至少一条回归用例,用例名或所在小节注明 issue 号 - [ ] 问一句「**这属于哪一类**」,然后同类全表扫 (例:#4 是「兜底表缺字段」→ 该扫的是**所有**兜底表的**所有**字段) - [ ] 回复 issue 时给全四样:现象 / 根因 / 修法 / 怎么验证 --- ## 9. 执行记录 | 版本 | 日期 | 机械门禁 | 真机负载 | 备注 | |---|---|---|---|---| | 0.3.9 | 2026-11-02 | 704/704 · 87/87 · 冒烟 ✓ · 扫描 ✓ | 未执行(登录/额度/签到逻辑,单测 + 本机真机复核) | Qoder 多账号 + 国际版额度/签到 | | 0.3.8 | 2026-11-02 | 699/699 · 87/87 · 冒烟 ✓ · 扫描 ✓ | 未执行(纯口径修复,未动网络路径) | issue #8 usage 口径、#9 Trae 下架 | | 0.3.7 | 2026-10-30 | 698/698 · 86/86 · 冒烟 ✓ · 扫描 ✓ | Raccoon / CN 通过;INTL 账号被风控(已停用) | issue #2–#7 批量修复;本清单首次生效 | 0.3.7 真机明细(2026-10-30 下午): - ✅ **观测面**:插件真实请求日志里 `cacheReadTokens` 有值(6656 / 6912 / 7040)—— 字段落盘生效 - ✅ **缓存(Raccoon)**:同前缀 3 轮,第 2、3 轮 `cacheRead 1024`;并发 4 × 8 `P50 1.6s / P95 1.7s` - ✅ **缓存(CN,issue #2 的正题)**:换到可直连 CN 的网络后,`hy4-preview` 同前缀 4 轮 `prompt=3511`,前 3 轮 `cacheRead=3392`(≈96.6%),第 4 轮 0 ⇒ 上游缓存**按节点/TTL 生效**,不是每轮必中; 短前缀(981)预热过后也能命中(896)。三个会话头实测**已发出且跨轮稳定** - ✅ **限流**:Raccoon 并发 8 × 16 触发 `HTTP 429` ×5 与 `[200103] limiter_request_error` ×6,全部分类为 `rate_limit` - ✅ **限流报文里的恢复时间**:上面那条 `[200103] … retry after:0s` 原本一头雾水(只认时间戳),现已解析相对时长, `0s` 抬到 1 秒(否则账号冷却=0 会变成热循环);头里没有 `Retry-After` 时从报文兜底。 真机复测:并发 6 × 12,2 条 `[200103]` 失败**带回 `retryAfterMs=1000`**(另 5 条 `HTTP 429` 上游确实没给任何时间) - ✅ **CN 并发**:并发 6 × 12 → **12/12 成功**,`P50 4.9s / P95 7.4s`,未触发限流 - ✅ **界面可见**:请求日志新增「缓存」列(命中率 + `title` 里的原始 `读/输入` 数字); `0.0%`(真没命中)与 `—`(老记录没这个字段)分开画,不许混 - ✅ **不误判风控**:`11140 · 账号存在异常行为` 仍判 `risk_control`,没被新的 `channel_reject` 吞掉 - ✅ **兜底表档位**:CN 目录拉不到时走内置表,`hy4-preview` 带 `efforts=high / default=high`; 网络恢复后拉到 **21 条在线目录**,同一模型档位一致(#4 的两条路径都验过) - ⚠️ **INTL 账号被上游风控**:`11140 · request illegal`,换模型复现一致 ⇒ 账号级,已在插件里停用 - ⚠️ **未覆盖**:界面点「停止」(`aborted`)、断网/超时的界面提示 —— 需要人在 UI 上操作 - 📌 **环境备注**:`copilot.tencent.com` 在原来那条网络上被 **SNI 级阻断**(同 IP 换 SNI 即握手成功),换网络后 200