package core import ( "strings" "testing" ) // Cline 的 429/402 话术与冷却时长(照参考 cline-rate-limit.ts)。 // // ★ 这是**比参考更严重**的形态:参考退到"1 小时快照兜底",用户看到 // 「永远 60 分钟」(等满一小时再试仍被拒,且每失败一次就从当下重新计一小时 // ⇒ 无限循环)。而我们的内核把 429 当普通限流、用**固定 30 秒**冷却 —— // 那会变成**每 30 秒重试一次、永远失败**,在猛敲上游。 // // 抓包真值(参考实测,no-retry: true —— 没有任何 retry-after 头): // // HTTP 429 // {"error":{"code":"INFERENCE_CAP_ERROR", // "message":"Error 429: Daily free limit reached on model // deepseek/deepseek-v4-flash. Try again in 19h 39m"}} func TestClineAdapterParsesProseWait(t *testing.T) { c, _ := poolCore(t) vm := c.VM() // ① 当日免费额度用尽:必须被归一成**内核已认的中立短语** //(否则内核按 30 秒短冷却处理 ⇒ 每 30 秒猛敲上游) body := `{"error":{"code":"INFERENCE_CAP_ERROR","message":` + `"Error 429: Daily free limit reached on model deepseek/deepseek-v4-flash. Try again in 19h 39m"}}` out, ok, err := vm.TransformError("channels-cline", 429, body) if err != nil || !ok { t.Fatalf("transform_error: err=%v ok=%v", err, ok) } if !strings.Contains(out, "额度已用完") { t.Errorf("必须归一成内核已认的「额度已用完」(否则不会被当成额度耗尽处理),实得 %q", out) } // 真实等待时长必须出现在话术里(19h39m ≈ 19 小时) if !strings.Contains(out, "19") { t.Errorf("必须把真实等待时长写进话术(用户才知道要等多久),实得 %q", out) } if !strings.Contains(out, "重试无用") { t.Errorf("当日额度用尽要明确告知重试无用(否则用户以为等一会就好),实得 %q", out) } // ⚠️ 绝不能出现参考踩过的「1 小时」误导 if strings.Contains(out, "1 小时") && !strings.Contains(out, "19") { t.Errorf("不得退回快照式的「1 小时」误导,实得 %q", out) } // ② 时长 token 必须**累加**(`1h 30m` = 90 分钟),不能只取第一个 —— // 通过真实的 transform_error 路径验证(不自己重算解析器)。 acc, _, _ := vm.TransformError("channels-cline", 429, `{"error":{"message":"Daily free limit reached. Try again in 1h 30m"}}`) if !strings.Contains(acc, "1 小时 30 分") { t.Errorf("`1h 30m` 必须累加成人话时长(1 小时 30 分),实得 %q", acc) } // 秒级也要能解析(否则秒级限流会退化成通用文案) secs, _, _ := vm.TransformError("channels-cline", 429, `{"error":{"message":"Rate limited. Try again in 45s"}}`) if !strings.Contains(secs, "分") && !strings.Contains(secs, "秒") { t.Errorf("秒级时长应被解析并说出,实得 %q", secs) } // ③ 402:等待不会恢复,必须说清楚要充值(不能只说"限流") out402, _, _ := vm.TransformError("channels-cline", 402, `{"error":{"message":"Insufficient credits"}}`) if !strings.Contains(out402, "充值") { t.Errorf("402 应明确提示需充值(等待无用),实得 %q", out402) } // ④ 普通 429 带时长:如实说,但不谎报成额度耗尽 out429, _, _ := vm.TransformError("channels-cline", 429, `{"error":{"message":"Rate limited. Try again in 30s"}}`) if strings.Contains(out429, "额度已用完") { t.Errorf("普通限流不得谎报成额度耗尽(那会让内核长时间冷却一个健康源),实得 %q", out429) } if !strings.Contains(out429, "限流") { t.Errorf("普通 429 应说成限流,实得 %q", out429) } // ⑤ 多种外壳都要认(`{error:"…"}` 字符串 / `{msg}` / 非 JSON 纯文本) //(只认一种会在别的形态下静默丢掉整条信息) for _, shell := range []string{ `{"error":"Daily free limit reached. Try again in 1h"}`, `{"msg":"Daily free limit reached. Try again in 1h"}`, `Daily free limit reached. Try again in 1h`, } { o, _, _ := vm.TransformError("channels-cline", 429, shell) if !strings.Contains(o, "额度已用完") { t.Errorf("外壳 %q 未被识别为额度耗尽,实得 %q", shell, o) } } }