CC Switch「连续失败三次就熔断」这说法是错的
如果你按手册去理解 CC Switch 的供应商健康徽章,很容易得到一个结论:连续失败到第 3 次,这家供应商就被熔断、请求会跳过它。可按同一份手册 4.3:109-115 自己给出的默认值算,失败阈值是 4(Claude 那一套是 8),连续失败 3 次根本到不了熔断线;而源码里熔断还有第二条错误率路径,它又可能在连续失败次数更低的时候先把熔断器打开。手册 4.3:208-232 的常见问题里,头一条问的就是「熔断没触发」。
问题不在你的配置,而在这句话本身就是错的。它出自手册里两处与源码逻辑对不上的描述,而同一份手册的另一个章节又给了第三种说法,三处互不相同,其中只有一处和代码逻辑吻合。
这篇文章的依据
本文对应的仓库快照是 3217f725(仓库内版本号 3.20.1),核对日 2026-08-31。所有结论都来自静态阅读 src/、src-tauri/ 的源码和 docs/user-manual/zh/ 的中文手册原文——我们没有编译、没有运行、也没有安装过这个桌面应用,因此下文不涉及任何界面观感、点击体验或响应快慢的描述,只谈代码里写着什么、文档里写着什么,以及两者在哪里对不上。
现象:同一枚徽章,手册给了三套说法
先把三处原文摆在一起。
| 手册位置 | 绿 | 黄 | 红 |
|---|---|---|---|
1.3:89-91 | 健康(连续失败 0 次) | 警告(连续失败 1-2 次) | 不健康(连续失败 ≥3 次,可能触发熔断) |
4.1:108-110 | 健康(连续失败 0 次) | 降级(连续失败 1-2 次) | 不健康(连续失败 ≥3 次) |
4.3:162-164 | 健康(连续失败次数为 0) | 警告(有失败但未触发熔断) | 熔断(已触发熔断,暂时跳过) |
前两行是数字口径,第三行是描述口径。三处的档位名字也不一样:警告、降级、不健康、熔断,四个词在三张表里排列组合。
更要命的是,「≥3 次触发熔断」这个数在手册内部就没法自洽。同一份手册的 4.3:109-115 给了熔断器的五项配置及默认值,其中失败阈值这一行写的是通用默认 4、Claude 默认 8、可调范围 1-20。也就是说,按手册自己给出的默认值,连续失败 3 次根本到不了熔断线。手册在 1.3 和 4.1 两处说 3 次会熔断,在 4.3 的参数表里又说要 4 次(Claude 8 次),三个位置分属不同章。
顺带一提,同一节的最佳实践里还有一张按场景给的建议表(4.3:193-197),把失败阈值建议配成 2、3、5,其中「一般场景」建议的 3 比默认的 4 还严——这张建议表和默认值表也没有对齐过。
怎么确认是这个问题
这类「文档数字飘了」的情况,判定动作很简单,两个文件对着看就行。
第一步,看徽章组件到底按什么分档。 打开 src/components/providers/ProviderHealthBadge.tsx,第 23-52 行的 getStatus() 只有三条分支,没有任何一处出现 1、2、3 这样的次数常量:
consecutiveFailures === 0→ 走第一档,文案键health.operational,源码里的兜底文案是「正常」;- 否则
isHealthy !== false→ 走第二档,文案键health.degraded,兜底文案「降级」; - 否则 → 走第三档,文案键
health.circuitOpen,兜底文案「熔断」。
读到这里,前面那两行数字口径就已经站不住了。红色那一档不是由「连续失败次数达到 3」触发的,而是由 isHealthy === false 触发的,也就是熔断器已经打开;黄色那一档也没有「2 次」这个上限,只要失败次数大于 0 且还没熔断,它就一直是黄的,失败 7 次也是黄的。同一个组件第 70 行给徽章设置的 title 文案默认值是「连续失败 N 次」,只带次数、不带档位——这也说明分档信息压根不在次数里。
三套说法中,只有 4.3:162-164 那套描述性写法(「有失败但未触发熔断」「已触发熔断,暂时跳过」)与这段代码是吻合的。
第二步,看熔断阈值的默认值落在哪几层。 这个值在仓库里有三处落点,可以互相印证:
- Rust 运行时的默认配置:
src-tauri/src/proxy/circuit_breaker.rs:63-73的impl Default for CircuitBreakerConfig,五个字段分别是failure_threshold: 4、success_threshold: 2、timeout_seconds: 60、error_rate_threshold: 0.6、min_requests: 10; - 数据库建表默认值:
src-tauri/src/database/schema.rs:133-135,circuit_failure_threshold的DEFAULT就是 4,其余四列依次是 2、60、0.6、10; - 按应用分叉的种子值:
src-tauri/src/database/dao/proxy.rs:328那个match app_type,"claude" => (6, 90, 180, 8, 3, 90, 0.7, 15),元组后五位正是 Claude 那一列的 8 / 3 / 90 / 0.7 / 15。
这三处和手册 4.3:109-115 的参数表逐项对得上——熔断器的默认值那张表是对的,错的是健康徽章那两处描述。定位到这一层,你就知道该信谁了。
需要强调的是:上面这些数字是 v3.20.1 的默认配置,它们是可配的(手册给的可调范围是失败阈值 1-20、恢复成功阈值 1-10、恢复等待 0-300 秒),也会随版本变。它们更不是「你会不会掉线」的保证,只是代码在没有用户配置时用的起始值。
顺手把熔断三态读明白
既然红档由熔断器决定,那就得知道熔断器是怎么开合的。这部分逻辑集中在 src-tauri/src/proxy/circuit_breaker.rs。
三个状态定义在第 16-23 行的 CircuitState 枚举里,带 #[serde(rename_all = "snake_case")],源码注释原文分别是:Closed 关闭状态、正常工作;Open 打开状态、熔断激活、拒绝请求;HalfOpen 半开状态、尝试恢复、允许部分请求通过。
开:两条独立的触发路径,有先后顺序。 在 record_failure()(circuit_breaker.rs:301 起)里,Closed 状态下记一次失败时先判连续失败次数,failures >= failure_threshold 就直接转 Open,打的日志码是 CB-004,文案「熔断器触发: 连续失败 N 次 → Open」。只有这一条不满足,才会去算错误率:先要求 total >= min_requests,再算 failed / total,超过 error_rate_threshold 才转 Open,日志码 CB-005。min_requests 存在的意义是防小样本误判——总共只发了 2 个请求全失败,错误率是 100%,但样本量不够,不触发。
这条分支顺序解释了一个容易困惑的现象:熔断不只有「连续失败」一条路。一个供应商完全可能在连续失败次数没到阈值的情况下,因为错误率路径被熔断。
合:靠成功次数和超时。 Open 到 HalfOpen 是靠时间——is_available()(:133)和 allow_request()(:157)都会检查 opened_at.elapsed().as_secs() >= timeout_seconds,到点才转,日志码 CB-001。HalfOpen 到 Closed 靠成功次数达到 success_threshold,日志码 CB-002;HalfOpen 期间探测一失败就退回 Open,日志码 CB-003;手动重置是 CB-006。
半开期只放一个探测请求,而且这个 1 不可配。 allow_half_open_probe()(:315-333)里第 317 行是一句 let max_half_open_requests = 1u32;——局部变量,不在 CircuitBreakerConfig 里,也不在 proxy_config 的任何一列里,用户改不到它。配套设计是名额归还:AllowResult(:99-103)带一个 used_half_open_permit 布尔值,文档注释要求调用方在请求结束后把它传回 record_success / record_failure 以正确释放名额;另有一个只还名额、不动健康统计的口子 release_half_open_permit()(:339),注释说明它用于「请求结果不应计入 Provider 健康度,但仍需释放占用的探测名额,避免 HalfOpen 状态卡死」的场景。
处置后怎么验证
改完认知(或改完阈值)之后,有三个可核对的点:
一是配置粒度。熔断配置是每应用一份的,proxy_config 表以 app_type 为主键,所以 Claude 那一套和其它应用那一套互不干扰。你在一个应用下观察到的行为,不能直接套到另一个应用头上。
二是是否需要重启。运行时的 CircuitBreaker.config 是 Arc<RwLock<CircuitBreakerConfig>>(circuit_breaker.rs:90),并且有专门的 update_config()(:121),From<&AppProxyConfig> for CircuitBreakerConfig(:51-61)负责把数据库里的 circuit_* 五列映射成运行时配置——这是热更新的结构,不是启动时读一次就锁死。
三是看日志码而不是看颜色。CB-001 到 CB-006 六个码分别对应上面六种转移,日志里出现哪个码,就说明发生了哪种转移。徽章只有三档,分辨不出「连续失败触发」和「错误率触发」的区别,日志码可以。
什么情况说明不是这个原因
以下几种情况,即使你看到的现象和「3 次说法」对不上,根因也不在这里,别往这上面套:
- 你根本没开代理和故障转移。 手册
4.3:16-19列了四个前提:启动代理服务、开启应用接管、配置故障转移队列、开启自动故障转移,缺一不可。四条没齐时,健康统计与熔断都不在链路上,徽章显示什么都不说明问题。 - 你已经改过阈值。 上面所有数字都只是默认值,配置项改过之后当然按你改的算。判定前先确认当前应用的实际配置。
- 触发的是错误率路径。 前面说过,
min_requests满足之后错误率超线也会熔断,这时连续失败次数可能远小于失败阈值。看到「没到 4 次就熔断了」,先去日志里确认是CB-004还是CB-005。 - 不同应用用的是不同的默认值。
dao/proxy.rs:328起的那个match里,Claude 走一套更宽松的值(手册4.3:117给的理由是「Claude 由于请求耗时较长,默认配置更为宽松,容忍更多失败次数」),Gemini 的最大重试是 5,Grok Build 走通用默认值。顺带一提,源码里的应用分支比手册故障转移那节列出的三个 Tab 要多。 - 供应商是被健康检查判掉的,不是被熔断器判掉的。 手册
4.5:106-120写明代理开启后会定期对队列内供应商做模型检查,不健康的会被暂时跳过;熔断恢复时也要先做一次模型检查,通过才恢复。这是另一条链路,有自己的一套超时与阈值。
版本口径
最后交代一句时间范围:src-tauri/src/proxy/circuit_breaker.rs 这个文件在 v3.19.2 与 v3.20.1 两个快照之间 diff 完全为空,逐字节相同——三个状态、五个阈值、默认值 4/2/60/0.6/10、半开限额 1,两版一致。数据库层那份硬编码默认值同样未变。所以本文引的行号在这两版通用;但这只说明这两版一致,不构成对后续版本的承诺,阈值和默认值随时可能变。
至于健康徽章那两处描述会不会被改回来,那是文档维护的事,我们只负责指出:读这份手册的参数时,以 4.3 的默认值表和源码为准;读徽章语义时,以 ProviderHealthBadge.tsx 的三条分支为准。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册、
路由指南与发布说明,以及 src/、src-tauri/、tests/ 的源码整理,
核对日 2026-08-31,对应仓库快照 3217f725(仓库内版本号 3.20.1)。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
留言讨论
评论发布后会被人工复核,违规内容将被删除。
如果发表没有反应,可以前往联系我们告诉我们。