换不换腿,整个系统只看一个函数的返回值:shouldTryNextLeg(error)。输入是上一条腿的错误文本,输出是一个布尔——true = 这条腿废了,换下一条;false = 书本身写坏了,认输。全部判据是三个正则数组,钉在 book-legs.ts 里。
这段代码在哪
src/book-legs.ts:QUOTA_PATTERNS、MODEL_REJECTED_PATTERNS、NOT_QUOTA_PATTERNS三个数组 +shouldTryNextLeg()。test/book-legs.test.mjs:所有模式对应的单测,错误串全部抄自事故当天 journalctl 的真实日志。
抽成独立文件、不 import server.ts 的原因写在文件头:「判别与顺序都抽在这里,是为了能脱开 server.ts(import 即起服务)单测」。
它做什么
判定顺序是先否定、后肯定——先看「明确不是配额」,再看模型被拒,最后看配额/凭据:
export function shouldTryNextLeg(error: string | undefined | null): boolean {
const s = String(error ?? "");
if (!s) return false;
if (NOT_QUOTA_PATTERNS.some((re) => re.test(s))) return false;
if (MODEL_REJECTED_PATTERNS.some((re) => re.test(s))) return true;
return QUOTA_PATTERNS.some((re) => re.test(s));
}
三类「该换腿」的模式:
| 类别 | 代表模式 | 对应的真实错误 |
|---|---|---|
| ① 配额满 | /usage limit/i、/quota/i、/rate[ _-]?limit/i、/\b429\b/、/api error: 403/i、/overloaded/i | Kimi「You've reached your 5-hour usage limit」;Kimi 兼容端点把配额耗尽报成 403 |
| ② 凭据失效 | /not logged in/i、/\b401\b/、/invalid.{0,12}api[ _-]?key/i、/credit balance is too low/i、/refresh[ _-]?token/i、/token.{0,24}(revoked\|invalidated)/i、/session has ended/i | codex「Not logged in」;refresh token 被吊销 |
| ③ 模型被账号拒 | /model.{0,40}does not exist/i、/do not have access to it/i、/model.{0,40}is not supported/i、/model[ _-]?not[ _-]?found/i | 404「The model `gpt-5.5` does not exist…」;400「'gpt-5.6' model is not supported…」 |
「不该换腿」的否定模式只有两个,但位置最高:
const NOT_QUOTA_PATTERNS: RegExp[] = [
/error_max_turns/i,
/high risk/i,
];
error_max_turns 是书写太长撞轮数上限,high risk 是 Kimi 内容风控——两者换腿重跑必然重蹈覆辙。注释:「这些词出现时,即便文本里混进了上面某个词也不换腿」。单测里就钉了一个混合串:"error_max_turns (quota fine)" 必须判 false。
为什么这样写
- 模式必须锚在 model 一词附近:③ 的四条模式都带着
model.{0,40}这类窗口。不锚定的话,任何ENOENT(「config file does not exist」)或「功能不支持」(「compression is not supported」)都会被当成换腿理由,白烧一份额度。单测专门有一组「『不存在/不支持』只认模型那一种,别误伤文件与功能」。 - 刻意不写裸
/token/:上下文超长的报错是「250000 tokens > 200000 maximum」「max_tokens exceeded」「token limit reached」——全含 token 二字,但它们是书写坏了,不是凭据问题。所以凭据模式写的是refresh[ _-]?token和token.{0,24}(revoked|invalidated)这种带动作的。单测有一组就叫「上下文超长不是凭据问题,别被 token 二字带偏」。 - 宁可宽松:文件注释说清了误判的不对称——「漏判=退回『一条腿倒下全线瘫痪』的老样子,误判=多花一条腿重跑一次。前者伤用户,后者只费自己的额度」。所以模式宁宽勿窄。
- 每条模式都有生日:refresh token 那两条是 2026-09-02 傍晚《易经在说什么》栽出来的(事故原文见第十章);模型被拒那四条是 2026-09-07 五单退款栽出来的。模式不是设计出来的,是事故喂出来的——所以单测的错误串坚持抄日志原文,不自己编。
怎么排查
- 怀疑「该换没换」:从日志抄出失败那行的
ERROR=文本,在仓库里先npm run build,然后一行验证:node --input-type=module -e "import {shouldTryNextLeg} from './dist/book-legs.js'; console.log(shouldTryNextLeg('把错误串贴这'))"(-e默认按 CJS 解析,不加--input-type=module顶层 import 会报语法错)。 - 改模式前先跑测试:
npm test(先 tsc build 再 node --test)。新增一条模式,必须同时在test/book-legs.test.mjs里加「该换」和「不该换」两组例子。 - 日志里
[book] leg … 失败,但不是配额/凭据问题,不换腿 →后面跟的就是被判 false 的错误原文,直接拿去对模式。