mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47: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 精确匹配 + 保护祖先链。
105 lines
3.7 KiB
Go
105 lines
3.7 KiB
Go
package lua
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// 「启动即跑一轮」判据。
|
||
//
|
||
// 为什么需要它(照参考的事后复盘):只用 interval 时,宿主重启后短寿命凭据
|
||
// (cline 1h / codearts ~2h / raccoon 3h)要等满一个周期才续期,期间一直显示
|
||
// 「已过期」、积分行 401。修复 = 声明 first_due_now 的任务在启动后短暂延迟内
|
||
// 先跑一轮。
|
||
//
|
||
// 但它**必须 opt-in**:真正周期性的任务(如每日签到)不该因为重启就重跑。
|
||
func TestScheduleFirstDueNow(t *testing.T) {
|
||
// ① 声明了 first_due_now 的任务:首轮应在 catchUpDelay 内,而不是等一个周期
|
||
now := time.Now()
|
||
catchUp := ScheduledTask{Fn: "f", Every: 7200, FirstDueNow: true}
|
||
rt := &PluginRuntime{}
|
||
d := rt.firstDue(catchUp)
|
||
if d.After(now.Add(catchUpDelay + time.Second)) {
|
||
t.Errorf("first_due_now 的任务首轮应在 %v 内,实得 %v 后",
|
||
catchUpDelay, d.Sub(now))
|
||
}
|
||
if d.Before(now) {
|
||
t.Errorf("首轮时间不该在过去:%v", d.Sub(now))
|
||
}
|
||
|
||
// ② 没声明的:必须等满一个周期(否则每日/每小时的语义会被破坏)
|
||
plain := ScheduledTask{Fn: "f", Every: 7200}
|
||
d2 := rt.firstDue(plain)
|
||
if d2.Sub(now) < 7000*time.Second {
|
||
t.Errorf("未声明 first_due_now 的任务不该提前跑:实得 %v", d2.Sub(now))
|
||
}
|
||
|
||
// ③ at 形式(每日固定时刻)不受标记影响 —— 它本来就该等到那个时刻
|
||
daily := ScheduledTask{Fn: "f", At: "08:00"}
|
||
d3 := rt.firstDue(daily)
|
||
if d3.Sub(now) > 25*time.Hour {
|
||
t.Errorf("每日任务的首次时间超出一天:%v", d3.Sub(now))
|
||
}
|
||
if d3.Sub(now) < catchUpDelay && !daily.FirstDueNow {
|
||
// 只有当 08:00 恰好在 catchUpDelay 内才可能,属正常
|
||
t.Logf("每日任务首轮临近(可能恰好在 08:00 附近):%v", d3.Sub(now))
|
||
}
|
||
|
||
// ④ 声明了 first_due_now 的每日任务:优先补跑(凭据不等时刻)
|
||
dailyCatchUp := ScheduledTask{Fn: "f", At: "08:00", FirstDueNow: true}
|
||
d4 := rt.firstDue(dailyCatchUp)
|
||
if d4.Sub(now) > catchUpDelay+time.Second {
|
||
t.Errorf("声明 first_due_now 时即便有 at 也应先补跑:%v", d4.Sub(now))
|
||
}
|
||
}
|
||
|
||
// 解析层判据:first_due_now 必须真的从插件源码读出来(写错键名会静默变成
|
||
// false —— 那就是"看起来配了、实际没补跑")。
|
||
func TestScheduleParsesFirstDueNow(t *testing.T) {
|
||
const src = `
|
||
local plugin = { name = "schedprobe", version = "1.0" }
|
||
plugin.exports = { "noop" }
|
||
function plugin.noop() end
|
||
function plugin.refresh_creds() end
|
||
function plugin.auto_claim() end
|
||
plugin.schedule = {
|
||
{ fn = "auto_claim", every = 86400, at = "08:00", jitter = 300 },
|
||
{ fn = "refresh_creds", every = 1800, first_due_now = true, jitter = 60 },
|
||
}
|
||
return plugin
|
||
`
|
||
_, ps, _ := runtimePlugins(t)
|
||
if err := ps.LoadSource("schedprobe", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ps.mu.RLock()
|
||
var sched []ScheduledTask
|
||
for _, p := range ps.plugins {
|
||
if p.Info.Name == "schedprobe" {
|
||
sched = p.Schedule
|
||
}
|
||
}
|
||
ps.mu.RUnlock()
|
||
if len(sched) != 2 {
|
||
t.Fatalf("应解析出 2 个任务,实得 %d: %+v", len(sched), sched)
|
||
}
|
||
byFn := map[string]ScheduledTask{}
|
||
for _, task := range sched {
|
||
byFn[task.Fn] = task
|
||
}
|
||
if task, ok := byFn["refresh_creds"]; !ok {
|
||
t.Fatal("refresh_creds 任务未解析")
|
||
} else if !task.FirstDueNow {
|
||
t.Error("first_due_now 未被解析为 true(键名写错会静默失效)")
|
||
} else if task.Every != 1800 {
|
||
t.Errorf("every = %d,应为 1800", task.Every)
|
||
}
|
||
if task, ok := byFn["auto_claim"]; !ok {
|
||
t.Fatal("auto_claim 任务未解析")
|
||
} else if task.FirstDueNow {
|
||
t.Error("auto_claim 未声明 first_due_now,应为 false(每日任务不该因重启重跑)")
|
||
} else if task.At != "08:00" {
|
||
t.Errorf("at = %q,应为 08:00", task.At)
|
||
}
|
||
}
|