Files
ModelRouter/internal/core/channels_autoclaim_test.go
JianFeeeee 044ff1129b chore(main): 回流 release/v2.0.x —— 插件自装适配器三连(qoder/cline)+ 自动领取 covers_today + isFree 权威来源
按发布纪律回流。这四笔原本**误提交在 main** 上(我的操作失误):
纪律要求修复先在 release/v2.0.x 提交、再回流 main。已迁移:
① release 上 cherry-pick 这四笔(干净落地);
② main reset 回 9bec158(那四笔未推送,无远端影响);
③ 内容对齐回 main —— 等价于只捡拾修复、不带版本号。

内容:
- channels-qoder 适配器:10605(排队) 与 110(额度耗尽) 分开说,厂商码留在插件域
- channels-cline 适配器:解析英文句子里的等待时长(内核按额度耗尽冷却而非 30s 猛敲)
- auto_claim:按 covers_today 记账(Qoder 10:00 刷新前不得算已处理)+ 间隔 2 小时
- isFree:改为按平台声明权威来源(cline 用远端 free[]/命名约定,opencode 用 cost)

校验:git diff release/v2.0.x = 0 行;go build 通过。
2026-10-14 20:01:24 +08:00

160 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"strings"
"testing"
)
// 自动签到的**能力表**必须与参考真值一致 —— 这张表是 auto_claim 的唯一依据,
// 错了就是"静默少领"或"每次调用都报自相矛盾的错"。
//
// 实测踩到的三处偏差(本轮修正):
//
// buddy 我们写 claim=true,但 buddy 既无 checkin 又无 claim_fn →
// 每次调用返回"不支持每日签到(平台未声明 checkin)"(表说支持、
// 函数说不支持)。参考里 buddy **没有** claim。
// raccoon 我们声明空表 → 白丢一个可领渠道。参考有:桌面端登录奖励
// POST /api/web/desktop/v1/login/points/grant(每号一次)。
// zcode 有签到,但每次领取现场产阿里云 captcha(headful Chromium,
// 按设备限流 150 次/小时)→ 参考把它列进 auto-checkin 的**排除表**。
// 自动跳过,手动仍可领。
func TestAutoClaimCapabilityTableMatchesReference(t *testing.T) {
c, ps := poolCore(t)
_ = c
// 参考真值(逐个 grep 参考仓的 claim 导出确认)
wantClaim := map[string]bool{
"trae": true, "minimax": true, "lobsterai": true, "qoder": true,
"loomy": true, "raccoon": true, "codearts": true,
"cline": false, "gemini": false, "zcode": true, "buddy": false,
}
// 自动签到必须跳过的(领取有代价)
wantAutoExclude := map[string]bool{"zcode": true}
for plat, want := range wantClaim {
t.Run("claim_"+plat, func(t *testing.T) {
got := creditsCapability(t, ps, plat)
if got == nil {
t.Fatalf("%s 无能力声明", plat)
}
if has := got["claim"] == true; has != want {
t.Errorf("%s claim = %v,参考为 %v", plat, has, want)
}
if got, ok := got["auto_exclude"].(bool); ok != wantAutoExclude[plat] {
t.Errorf("%s auto_exclude = %v,应为 %v", plat, got, wantAutoExclude[plat])
}
})
}
// 关键一致性:声称 claim=true 的平台**必须**真的可领
//(有 checkin 表或有 claim_fn)—— 否则就是 buddy 那种自相矛盾。
for plat, want := range wantClaim {
if !want {
continue
}
m, err := ps.Call("channels", "claims_capable", map[string]interface{}{"platform": plat})
if err != nil {
t.Fatalf("%s: %v", plat, err)
}
w, _ := m.(map[string]interface{})
j, _ := w["json"].(map[string]interface{})
if j == nil {
t.Fatalf("%s 无返回", plat)
}
if j["capable"] != true {
t.Errorf("%s 声称 claim=true 但不可领(%v)—— 会出现「表说支持、"+
"函数说不支持」的自相矛盾", plat, j["reason"])
}
}
// 反向:不声称 claim 的平台必须被判为不可领
m, _ := ps.Call("channels", "claims_capable", map[string]interface{}{"platform": "buddy"})
w, _ := m.(map[string]interface{})
j, _ := w["json"].(map[string]interface{})
if j != nil && j["capable"] == true {
t.Error("buddy 不该被判为可领(它没有 claim)")
}
}
// 自动签到的**调度配置**必须覆盖两项需求:领取 + 模型获取。
//
// auto_claim 走调度器(不是 HTTP export),故断言调度声明。
func TestAutoClaimScheduledWithModelRefresh(t *testing.T) {
b, err := readBundledChannels(t)
if err != nil {
t.Fatal(err)
}
for _, want := range []struct{ name, needle string }{
{"每日领取", `fn = "auto_claim"`},
{"模型目录自动获取", `fn = "refresh_models"`},
{"凭据自动续期", `fn = "refresh_creds"`},
{"启动即补跑", `first_due_now = true`},
} {
if !strings.Contains(b, want.needle) {
t.Errorf("调度表缺 %s(%s)", want.name, want.needle)
}
}
// 模型刷新间隔照参考 MODEL_REFRESH_INTERVAL_MS(2 小时)
if !strings.Contains(b, `fn = "refresh_models", every = 7200`) {
t.Error("refresh_models 间隔应为 7200(参考 2h)")
}
// 按**行**判断(固定字符窗会溢到下一行,误报)
lineOf := func(fn string) string {
for _, ln := range strings.Split(b, "\n") {
if strings.Contains(ln, `fn = "`+fn+`"`) {
return ln
}
}
return ""
}
// auto_claim **现在**也 first_due_now,且间隔远小于一天。
//
// ★ 这里改过一次,原因值得记下:原判据写的是「auto_claim 是每日任务,
// 不该 first_due_now(重启就重领是错的)」—— 那个假设**本身是缺陷的一部分**。
// 活动刷新时刻**各平台不同**(Qoder 是每日 10:00 UTC+8),一天只跑一次
// 必然有平台整天漏领(参考实测:每天静默漏 100 积分)。修法有两半:
// ① 频率提高 + first_due_now(任何时刻重启都能补上);
// ② 「今天已覆盖」靠 covers_today 记账跳过(**这才是防重领的正确机制**,
// 而不是靠"一天只跑一次"来碰巧避开)。
// 只做 ② 不做 ① 仍会漏领;只做 ① 不做 ② 会重复打上游。
if ln := lineOf("auto_claim"); !strings.Contains(ln, "first_due_now = true") {
t.Errorf("auto_claim 应 first_due_now(否则重启后当天漏领):%s", ln)
}
// 两个自动任务必须 first_due_now(否则重启要等满一轮才自愈)
for _, fn := range []string{"refresh_models", "refresh_creds"} {
ln := lineOf(fn)
if ln == "" {
continue
}
if !strings.Contains(ln, "first_due_now = true") {
t.Errorf("%s 应声明 first_due_now(参考点名的重启自愈缺陷):%s", fn, ln)
}
}
// auto_exclude 必须挡住 zcode(领取有代价:headful Chromium)
if !strings.Contains(b, "auto_exclude = true") {
t.Error("zcode 应标记 auto_exclude(照参考 isAutoCheckinExcluded)")
}
if i := strings.Index(b, `if cap.auto_exclude then`); i < 0 {
t.Error("auto_claim 必须尊重 auto_exclude(否则会拉起浏览器)")
}
}
// creditsCapability 读插件的能力表条目。
func creditsCapability(t *testing.T, ps interface {
Call(string, string, map[string]interface{}) (interface{}, error)
}, plat string) map[string]interface{} {
t.Helper()
m, err := ps.Call("channels", "credits_capability", map[string]interface{}{"platform": plat})
if err != nil {
t.Fatalf("credits_capability(%s): %v", plat, err)
}
w, _ := m.(map[string]interface{})
j, _ := w["json"].(map[string]interface{})
if j == nil {
return nil
}
if cap, ok := j["capability"].(map[string]interface{}); ok {
return cap
}
return nil
}