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 }