mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 20:27:23 +00:00
用户要求两件事:① 自动签到与领取 ② 自动获取可用模型。 两者都有调度缺口,且能力表与参考真值不符(表错了只静默少领,无任何报错)。 ## ① 自动签到的能力表(三处偏差,全部修正) | 平台 | 我们 | 参考 | 后果 / 修法 | |---|---|---|---| | buddy | claim=true | 无 | **自相矛盾**:表说支持、claim_daily 说不支持(既无 checkin 也无 claim_fn)→ 改为只报余额 | | raccoon | 空表 | 有 | 白丢一个可领渠道 → 补 claimRaccoon(POST /api/web/desktop/v1/login/points/grant,幂等判据是 `granted`,重复领取同返 200 但 granted=false) | | zcode | claim=true 无实现 | auto-exclude | 补 claimZcode(3 步:event/report → preview → claim/plan),并照参考 isAutoCheckinExcluded 标记 auto_exclude | **同为"表说支持、函数不支持"的还有 loomy** —— 补 claimLoomy (POST /points/first-login;语义是**触发每日额度重置**而非 +5000 积分; 幂等判据 `alreadyProcessed` → already-claimed 而非 claimed)。 一条一致性判据抓出全部四例:声称 claim=true 的平台必须真的可领 (有 checkin 表或有 claim_fn)。 ## ② 两个调度任务(此前只有每日领取一个) 参考有三个调度器,我们只有一个;模型获取**根本没有调度**: - refresh_models:每 2 小时拉远端目录(照参考 MODEL_REFRESH_INTERVAL_MS), 成功后重同步该平台所有源(模型清单写在源里)。失败**不清缓存**。 - refresh_creds:每 30 分钟检查续期(照参考 REFRESH_INTERVAL_MS)。 ⚠️ **不看 enabled**(照参考铁律:停用只影响选号,不该停续期); ⚠️ **只对距过期不足 1 小时的账号发请求**(照参考 expiry-sync), 否则启动时会打出几十个无谓请求。 ## ③ 调度器新增 first_due_now(内核) 照参考的事后复盘:只用 interval 时,宿主重启后短寿命凭据(cline 1h / codearts ~2h / raccoon 3h)要等满一个周期才续期,期间一直显示「已过期」、 积分行 401。故声明式加 first_due_now —— 任务在启动后 5s 先补跑一轮。 **且必须 opt-in**:真正周期性的任务(每日签到)不该因重启就重跑。 判据同时断言"auto_claim 不得带该标记"。 ## 实测(生产) - 模型刷新:buddy 真拉到 **22 个远端模型**(含兜底表没有的 hy4-preview、 step-5-preview、codewise-* 等);zcode 2 个;其余报"无可用凭据"(登录未完成) - 推理链路:`POST /v1/chat/completions` 返回 "OK" ## 宿主排障(顺手) VNC 端到端测试失败,查证**不是回归**(stash 改动后同样失败):宿主有 **10 小时前的孤儿 `Xvfb :91`**(PID 1074031)占着显示号,加上 126 个孤儿 SHM 段、18 个陈旧 X socket。按纪律(先 SIGTERM 再 SIGKILL)回收后测试通过。 另记一笔:`ps | awk '/[X]vfb/'` 这种写法会匹配到**执行它的 shell 自身** (命令行里含该字样),清理必须用 /proc/<pid>/exe 精确匹配 + 保护祖先链。
151 lines
5.5 KiB
Go
151 lines
5.5 KiB
Go
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(重启就重领是错的)
|
||
if ln := lineOf("auto_claim"); strings.Contains(ln, "first_due_now") {
|
||
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
|
||
}
|