27 个日志错误码与错误到 HTTP 状态的映射表
CC Switch 的本地代理在出错时会同时产生两样东西:一条带 前缀-编号 形式错误码的日志,和一个返回给 CLI 的 HTTP 状态码。这两样东西不是一回事,也不是一一对应的——日志错误码有 27 个,HTTP 状态码只有十来个,中间是一次有损压缩。
排查代理问题最常见的卡点就在这里:你手上只有 CLI 报出来的那个数字,而那个数字背后可能站着好几种完全不同的失败原因。
以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本,没有安装也没有运行过这个桌面应用。
27 个码:log_codes.rs 全表
这批码集中定义在 src-tauri/src/proxy/log_codes.rs 一个文件里,用 grep -c "pub const" src-tauri/src/proxy/log_codes.rs 数出来是 27 个。这些码统一是 前缀-编号 的形式,前缀共六个:
| 前缀 | 含义 | 码 | 定义位置 |
|---|---|---|---|
| CB | 熔断器 | CB-001 Open→HalfOpen、CB-002 HalfOpen→Closed、CB-003 HalfOpen 探测失败、CB-004 连续失败触发、CB-005 错误率触发、CB-006 手动重置 | log_codes.rs:14-21 |
| SRV | 服务器 | SRV-001 启动、SRV-002 停止、SRV-003 停止超时、SRV-004 任务异常、SRV-005 accept 错、SRV-006 连接错 | log_codes.rs:24-31 |
| FWD | 转发器 | FWD-001 单 provider 失败将重试、FWD-002 全部 provider 失败、FWD-003 单 provider 失败 | log_codes.rs:34-38 |
| FO | 故障转移 | FO-001 切换成功、FO-002 配置读取错、FO-003 Live 备份错、FO-004 全部熔断、FO-005 无供应商 | log_codes.rs:41-47 |
| RSP | 响应处理 | RSP-001 构建流错、RSP-002 读 body 错、RSP-003 构建响应错、RSP-004 流超时、RSP-005 流错误 | log_codes.rs:50-56 |
| USG | 用量 | USG-001 落库失败、USG-002 定价未找到 | log_codes.rs:59-62 |
这张表的第一个读法是看前缀而不是看编号。前缀直接告诉你失败发生在管线的哪一段:CB 是熔断器状态机自己的动作,SRV 是 HTTP 服务本身(起停、accept、连接),FWD 是往上游转发的那一跳,FO 是转发之后触发的供应商切换,RSP 是拿到上游响应之后的处理与流式转发,USG 是最后落库计价。一条请求从进到出会依次经过这几段,所以码前缀本身就是一个粗粒度的定位坐标。
第二个读法是注意这六个前缀没有覆盖到哪些环节。整个代理模块用 find src-tauri/src/proxy -name '*.rs' | wc -l 数是 66 个 .rs 文件、合计 61198 行,而带码的只有上面六类。三个 thinking 整流器(thinking_rectifier.rs、thinking_budget_rectifier.rs、thinking_optimizer.rs)、图片降级与重试、services/stream_check.rs 的连通性检查,在这张表里都没有属于自己的前缀。也就是说,这些环节做了什么、改写了什么,不会以这六类错误码的形式出现在日志里——你不能靠搜码去追它们。
第三,USG 只有两个码,且都不是「请求失败」:USG-001 是落库失败,USG-002 是定价未找到。用量这一层出问题不影响请求本身已经返回的结果。
另一张表:错误到 HTTP 状态
映射写在 src-tauri/src/proxy/error_mapper.rs,规则本身就写在文件头的注释里(error_mapper.rs:7-32):
| 内部错误 | HTTP 状态 |
|---|---|
| 上游错误 | 直接使用上游返回的状态码 |
| 超时 / 流空闲超时 | 504 |
| 转发失败 | 502 |
| 无可用 Provider | 503 |
| 重试耗尽 | 503 |
| 认证错误 | 401 |
| 配置错误 / 请求错误 | 400 |
| 转换错误 | 422 |
| 其他 | 500 |
AlreadyRunning | 409 |
NotRunning | 503 |
ProxyError 枚举本身的变体比这张表的行数多,error.rs:10-70 里能看到 ResponseBodyTooLarge、AllProvidersCircuitOpen、NoProvidersConfigured、MaxRetriesExceeded、StreamIdleTimeout 等等。多对少,这就是前面说的有损压缩。
反直觉的那一处:503 是个大杂烩
把两张表并排看,最要命的是 503 这一格。
按 error_mapper.rs 的规则,「无可用 Provider」和「重试耗尽」都是 503,NotRunning(代理服务没在跑)也是 503。而在故障转移的候选选择里,候选为空时代码是明确区分两种错因的:全部熔断走 AllProvidersCircuitOpen 并打 [FO-004],队列里根本没有可用条目走 NoProvidersConfigured 并打 [FO-005](src-tauri/src/proxy/provider_router.rs:98-106)。
这两件事在现实里是两个完全不同的处境:
FO-004:你配了供应商,队列也不空,但它们全部处于熔断打开状态。这是运行时健康问题,跟你最近这一阵请求的成败有关。FO-005:故障转移队列里压根没东西可选。这是配置问题,跟请求成败无关。
代码在日志层把它们分成了两个码,到了 HTTP 层却塌成同一个 503。再叠上「重试耗尽」和「服务没在跑」,你从客户端看到的 503 至少对应四种起因。所以「代理返回 503」这个信号本身几乎没有定位价值,它只告诉你「这一跳没成」。
同一件事在 401 上反过来发生一次。映射规则里有两条路径能产出 401:一是上游返回了 401,按「上游错误用上游状态码」原样透传;二是代理自己判定的认证错误 → 401(error_mapper.rs:7-32)。这两个 401 在客户端眼里长得一模一样,但一个说的是「你的 Key 在上游那边不认」,另一个说的是这一层自己的判定。
怎么把丢掉的信息找回来
这就是那 27 个码的价值所在:HTTP 状态码是压缩后的结果,日志码是压缩前的原因。排查顺序应该是从状态码起手、立刻转到日志码,而不是围着状态码打转。
可以照做的几步:
- 确认日志是开着的。代理配置默认
enable_logging: true(src-tauri/src/proxy/types.rs:42-56,同一处还能看到默认监听127.0.0.1:15721与max_retries: 3)。 - 按前缀搜,而不是按关键词搜。拿到一个 503,就在日志里搜
FO-004和FO-005;拿到 504,就搜RSP-004;怀疑是某一家上游挂了,就搜FWD-001/FWD-003。前缀比错误消息文本稳定得多。 - 区分
FWD-001与FWD-003。这两个码的语义差在一层:FWD-001是「单 provider 失败、还会继续重试」,FWD-003是「单 provider 失败」,后者没有「将重试」这层含义(log_codes.rs:34-38)。如果你看到的是一连串FWD-001收尾一个FWD-002,那是重试链走完了;如果只有孤零零一个FWD-003,说明这一跳的日志里没有留下「还会继续试」的记号——但具体是否还有后续候选取决于当次的队列状态,源码在这处只给了码的语义,没给更细的判定依据。 - 别忘了熔断器的码是被动产生的。
CB-004是连续失败触发、CB-005是错误率触发,它们记录的是熔断器已经打开这件事。想知道阈值是多少,得回去看配置:代码默认failure_threshold: 4、success_threshold: 2、timeout_seconds: 60、error_rate_threshold: 0.6、min_requests: 10(src-tauri/src/proxy/circuit_breaker.rs:63-73),而数据库 seed 里 claude 这一行是(6, 90, 180, 600, 8, 3, 90, 0.7, 15)、codex 是(3, 60, 120, 600, 4, 2, 60, 0.6, 10)(src-tauri/src/database/schema.rs:152、:161)——同一个参数在代码默认值和数据库 seed 里取值不同,按哪一档生效取决于这条配置从哪读,本文不做推断。 - 504 要连着超时配置一起看。流式转发的超时是分段的:首个 chunk 用首字节超时,之后用静默超时,两者为 0 时禁用,超时会 yield 一个「流式响应超时」的 io error 并 break(
src-tauri/src/proxy/response_processor.rs:700-737)。对照上面 seed 的数值,claude 是首字 90 秒、静默 180 秒,codex 是 60 / 120。504 到底卡在首字还是卡在中途,日志文本里的措辞能分开,状态码分不开。
一个容易忽略的落差:状态码进了库,错误码没进
请求日志落到 proxy_request_logs 表,主键 request_id(src-tauri/src/database/schema.rs:197-211)。这张表里有 status_code 列,而且专门为它建了索引——五个索引分别是 provider+app_type、created_at、model、session_id、status_code(schema.rs:213-231)。
也就是说,HTTP 状态码是结构化、可按列检索、可做聚合的;那 27 个日志码不在这张表里,它们存在于日志文本中。这个落差决定了两种查法的分工:想统计「最近一周有多少次非 2xx」,走数据库这条路很顺;想知道「这些非 2xx 各自是什么原因」,就必须回到日志文本里按前缀搜。指望在表里按错误码分组是行不通的。
什么情况说明不是这一层的问题
排查文章最容易省掉的一步是「排除」,这里补上。按 error_mapper.rs 的第一条规则,上游错误直接用上游返回的状态码。所以:
- 如果你拿到的非 2xx 状态码,在日志里找不到任何
FWD-/FO-/RSP-前缀的码,那按映射规则它更可能是上游原样透传下来的状态,而不是代理这一层判定的失败。这时候去调代理的重试、熔断阈值是无效的。 - 如果日志里有
SRV-前缀的码在报,问题在 HTTP 服务本身(起停、accept、连接),跟你选哪个供应商无关。SRV-003是停止超时——服务停止时会等任务结束,带 5 秒超时保护,超时返回ProxyError::StopTimeout(src-tauri/src/proxy/server.rs:233-250)。 - 如果只看到
USG-001/USG-002,那是用量落库与定价环节,请求本身的成败不由它决定。
最后说一句分寸:上面所有阈值与默认值都是源码里的默认配置,不是「你用起来会怎样」的保证;这 27 个码与那张映射表也都是 2026-08-10 我们读到的 c39c903 快照的状态。能复用的不是具体的码,而是「先看前缀定位管线段落,再回配置看阈值」这个顺序。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、
src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。