mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 04:07:25 +00:00
feat(channels): 自动领取得以真正跑起来 + 自动获取可用模型
用户要求两件事:① 自动签到与领取 ② 自动获取可用模型。 两者都有调度缺口,且能力表与参考真值不符(表错了只静默少领,无任何报错)。 ## ① 自动签到的能力表(三处偏差,全部修正) | 平台 | 我们 | 参考 | 后果 / 修法 | |---|---|---|---| | 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 精确匹配 + 保护祖先链。
This commit is contained in:
@ -55,6 +55,18 @@ type ScheduledTask struct {
|
||||
// Jitter, in seconds, spreads the first fire randomly so many plugins (or
|
||||
// many replicas) do not all hit the same upstream at :00.
|
||||
Jitter int `json:"jitter,omitempty"`
|
||||
// FirstDueNow makes the FIRST run happen shortly after boot instead of after
|
||||
// a full period.
|
||||
//
|
||||
// Why it exists (measured defect, borrowed from the reference implementation's
|
||||
// post-mortem): with interval-only scheduling a restart leaves short-lived
|
||||
// credentials expired for up to a whole period. The reference's providers
|
||||
// have cline at 1h, codearts at ~2h and raccoon at 3h, so a 30-minute
|
||||
// refresh interval left them showing "已过期" and their quota rows returning
|
||||
// 401 after every host restart. A startup catch-up round fixes that — but it
|
||||
// must be opt-in, because a task that is genuinely periodic (a daily
|
||||
// check-in, say) should NOT fire again just because the host restarted.
|
||||
FirstDueNow bool `json:"first_due_now,omitempty"`
|
||||
// Timeout is parsed for forward compatibility but is NOT enforced: a running
|
||||
// Lua call cannot be interrupted in this LuaJIT build (see the capability
|
||||
// note on checkWedged). Overrun is handled by disarming the plugin at the
|
||||
@ -106,6 +118,7 @@ func parseSchedule(L *golua.State) scheduleSpec {
|
||||
t.At = readString(L, -1, "at")
|
||||
t.Jitter = readNumberInt(L, -1, "jitter")
|
||||
t.Timeout = readNumberInt(L, -1, "timeout")
|
||||
t.FirstDueNow = readBoolField(L, -1, "first_due_now")
|
||||
if t.Fn != "" && (t.Every > 0 || t.At != "") {
|
||||
spec.tasks = append(spec.tasks, t)
|
||||
}
|
||||
@ -195,6 +208,12 @@ func (ps *Plugins) startRuntime(p *Plugin, spec scheduleSpec) *PluginRuntime {
|
||||
// plugins do not all wake at process start.
|
||||
func (rt *PluginRuntime) firstDue(t ScheduledTask) time.Time {
|
||||
now := time.Now()
|
||||
// A catch-up run: fire shortly after boot so a restart does not leave
|
||||
// short-lived credentials expired for a whole period (see FirstDueNow).
|
||||
// The short delay lets the rest of the plugin finish booting first.
|
||||
if t.FirstDueNow {
|
||||
return now.Add(catchUpDelay)
|
||||
}
|
||||
if t.At != "" {
|
||||
return nextDailyOccurrence(t.At, now)
|
||||
}
|
||||
@ -319,6 +338,11 @@ func (rt *PluginRuntime) runDue() {
|
||||
}
|
||||
}
|
||||
|
||||
// catchUpDelay is how long a FirstDueNow task waits after boot. Long enough for
|
||||
// the plugin's own boot work to finish, short enough that a restart heals
|
||||
// expired credentials promptly rather than after a full period.
|
||||
const catchUpDelay = 5 * time.Second
|
||||
|
||||
// nextAfter computes the following due time for a task that just fired.
|
||||
func (rt *PluginRuntime) nextAfter(t ScheduledTask, now time.Time) time.Time {
|
||||
if t.At != "" {
|
||||
@ -489,6 +513,17 @@ func randInt63n(n int64) int64 {
|
||||
|
||||
// readNumberInt reads a numeric field as an int. Missing/zero yields 0, which
|
||||
// every caller treats as "not set" (a task with every<=0 and no at is disabled).
|
||||
// readBoolField reads a boolean field, treating only a real Lua boolean as set.
|
||||
// A missing field (nil) means false — the caller's declared default.
|
||||
func readBoolField(L *golua.State, idx int, field string) bool {
|
||||
L.GetField(idx, field)
|
||||
defer L.Pop(1)
|
||||
if L.Type(-1) != golua.LUA_TBOOLEAN {
|
||||
return false
|
||||
}
|
||||
return L.ToBoolean(-1)
|
||||
}
|
||||
|
||||
func readNumberInt(L *golua.State, idx int, field string) int {
|
||||
L.GetField(idx, field)
|
||||
defer L.Pop(1)
|
||||
|
||||
Reference in New Issue
Block a user