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:
JianFeeeee
2026-10-14 10:46:05 +08:00
parent f55d514e85
commit 10ad4421f9
4 changed files with 695 additions and 5 deletions

View File

@ -52,7 +52,7 @@ plugin.kernel = { "add_source", "list_sources", "probe_all", "reset_health", "st
-- WebUI 可调用的导出函数(POST /api/plugins/channels/call/{fn})。
-- set_endpoints 让运维把渠道端点指向自建代理/镜像;测试用它打假上游。
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "model_list", "model_refresh", "model_set_disabled", "model_set_disabled_many", "model_set_all_disabled", "model_clear_dead", "provider_status", "provider_set_enabled", "provider_get_order", "provider_set_order", "credits_claim_all", "credits_status", "credits_balances", "credits_permanent_lock", "backup_export", "backup_import", "backup_status", "auto_checkin", "gateway_get_enabled", "gateway_set_enabled", "usage_token_ledger", "usage_badge", "usage_badge_preference", "onboarding_status", "onboarding_claim", "__trae_derive_probe", "__codearts_sign_probe", "__qoder_machine_probe", "__qoder_refresh_probe", "__codearts_dpop_probe", "__codearts_cred_probe", "__codearts_adapter_sign_probe", "__sync_source_probe", "__trae_solo_probe", "__adapter_install_probe" }
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "model_list", "model_refresh", "model_set_disabled", "model_set_disabled_many", "model_set_all_disabled", "model_clear_dead", "provider_status", "provider_set_enabled", "provider_get_order", "provider_set_order", "credits_claim_all", "credits_status", "credits_balances", "credits_permanent_lock", "backup_export", "backup_import", "backup_status", "auto_checkin", "gateway_get_enabled", "gateway_set_enabled", "usage_token_ledger", "usage_badge", "usage_badge_preference", "onboarding_status", "onboarding_claim", "__trae_derive_probe", "__codearts_sign_probe", "__qoder_machine_probe", "__qoder_refresh_probe", "__codearts_dpop_probe", "__codearts_cred_probe", "__codearts_adapter_sign_probe", "__sync_source_probe", "__trae_solo_probe", "__adapter_install_probe", "credits_capability", "claims_capable" }
@ -2897,15 +2897,26 @@ end
-- 渠道积分能力表(对齐参考 CHANNEL_CREDITS):声明各渠道有没有领取/余额。
local CHANNEL_CREDITS = {
codearts = { claim = true },
buddy = { claim = true, status = true },
-- ⚠️ buddy **没有**每日签到。此前这里写 claim=true 是错的:claim_daily 的
-- 前置条件(plat.checkin 或 claim_fn)都不满足,于是每次调用都返回
-- "CodeBuddy(腾讯) 不支持每日签到(平台未声明 checkin)" —— 一个自相矛盾的
-- 文案(表说支持、函数说不支持)。对齐参考:buddy 只有**余额**,无 claim。
buddy = { status = true },
lobsterai = { claim = true },
qoder = { claim = true },
trae = { claim = true },
cline = { unit = "dollars" },
loomy = { claim = true },
raccoon = {},
-- raccoon 的领取 = **桌面端登录奖励**(每号一次),参考
-- raccoon-credits.ts:claimRaccoonLoginReward。此前我们声明为空表 → 白丢一个
-- 可领渠道。幂等靠响应的 `granted` 字段(重复领取同样 HTTP 200 但 granted=false)。
raccoon = { claim = true, unit = "points" },
minimax = { claim = true, status = true },
zcode = { claim = true, unit = "tokens" },
-- zcode 有签到,但每次领取都要现场产**阿里云 captcha**(headful Chromium,
-- 且阿里云按设备限流 150 次/小时)。故照参考 isAutoCheckinExcluded:
-- **自动**签到跳过它(代价发生在调用期间,等它报错来不及);
-- 用户仍可**手动**点领取 —— 浏览器弹出来是用户能理解的交互。
zcode = { claim = true, unit = "tokens", auto_exclude = true },
gemini = { unit = "quota", quota = true },
}
@ -4924,10 +4935,92 @@ end
--
-- 声明了 schedule 的插件会得到内核 ticker 驱动(无浏览器也执行),
-- 且任务跑在本插件自己的 VM 上 —— 所以任务里读 state/保险库与钩子看到的是同一份。
-- 周期任务表。三项,各自理由见下(参考 index.ts / models.ts 的调度对照):
--
-- auto_claim 每天 08:00 领取额度(参考 auto-checkin 的日频语义)
-- refresh_models 每 2 小时刷新远端模型目录(参考 MODEL_REFRESH_INTERVAL_MS)
-- refresh_creds 每 30 分钟检查凭据续期(参考 REFRESH_INTERVAL_MS)
--
-- ⚠️ 「启动即跑一轮」由调度器的 first_due_now 标记保证(见 plugin_runtime.go
-- 的 firstDue):参考点名过这个缺陷 —— 短寿命凭据(cline 1 小时、codearts
-- 约 2 小时、raccoon 3 小时)在宿主停机期间早就过期,只用 interval 的话
-- 重启后要等满一个周期才续期,期间一直显示「已过期」。
plugin.schedule = {
{ fn = "auto_claim", every = 86400, at = "08:00", jitter = 300 },
{ fn = "refresh_models", every = 7200, first_due_now = true, jitter = 120 },
{ fn = "refresh_creds", every = 1800, first_due_now = true, jitter = 60 },
}
-- refresh_models():定时把远端模型目录拉回来并重同步所有源。
--
-- 为什么需要它:模型目录写进了**上游源**(源的 models 决定网关能路由哪些
-- 模型)。不刷新就只能用兜底表 —— 平台上新模型后网关永远看不到,
-- 表现为"某个新模型 404 / 不可选"。
function plugin.refresh_models()
local updated, failed = 0, {}
for _, plat in ipairs(allPlatforms()) do
if plat.models_remote then
local ids, err = refreshRemoteModels(plat.id)
if ids then
updated = updated + 1
-- 目录变了就重同步该平台所有源(模型清单在源里)
for _, a in ipairs(poolList(plat.id)) do
syncAccountSource(plat.id, a.id)
end
else
failed[#failed + 1] = plat.id .. ": " .. tostring(err)
end
end
end
if updated > 0 then
logEvent("模型目录已刷新(" .. updated .. " 个平台)")
end
-- 失败留痕但**不清缓存**(refreshRemoteModels 内部已保证):
-- 拉不到时继续用上次的好数据,绝不能把目录清空。
if #failed > 0 then
logEvent("模型刷新失败 " .. #failed .. " 个:" .. table.concat(failed, "; "):sub(1, 200))
end
end
-- refresh_creds():定时检查并续期凭据。
--
-- ⚠️ **不看 enabled**(照参考的既有铁律):停用只影响自动选号,不该让凭据
-- 停止续期。否则"所有账号都被停用"时续期根本不跑,凭据一路烂到
-- refresh_token 失效、只能重新登录。
--
-- ⚠️ **只对「距过期不足 lead 时间」的账号发请求**(照参考 expiry-sync 的
-- shouldRefreshNow):每次无脑续期会在启动时打出几十个无谓请求。
-- 没有过期时间的账号只做本地对账,不发请求。
function plugin.refresh_creds()
local lead = 3600 -- 提前 1 小时续(照参考的 lead 语义)
local now = os.time and os.time() or 0
local refreshed, skipped = 0, 0
for _, plat in ipairs(allPlatforms()) do
if plat.exchange_body or plat.refresh_url then
for _, a in ipairs(poolList(plat.id)) do
local cred = credentialFull(a.credentialRef)
if type(cred) == "table" then
local exp = tonumber(a.expiresAt) or credentialExpiresAtMs(cred)
local due = exp == nil or exp == 0
if not due then
-- expiresAt 单位可能是毫秒;统一成秒再比
local e = exp > 1e11 and math.floor(exp / 1000) or exp
due = (e - now) <= lead
end
if due then
local ok, err = plugin.account_refresh({ platform = plat.id, account_id = a.id })
if ok and ok.json and ok.json.ok then refreshed = refreshed + 1
else skipped = skipped + 1 end
else
skipped = skipped + 1
end
end
end
end
end
if refreshed > 0 then logEvent("凭据已续期 " .. refreshed .. " 个") end
end
function plugin.auto_claim()
-- 自动签到开关(对齐参考 usage.autoCheckin):关闭时不做任何领取。
plugin.state.settings = plugin.state.settings or {}
@ -4935,6 +5028,11 @@ function plugin.auto_claim()
for _, plat in ipairs(allPlatforms()) do
local cap = CHANNEL_CREDITS[plat.id]
if not cap or not cap.claim then goto continue end
-- auto_exclude:领取有**代价**的平台(zcode 每次领取现场产阿里云 captcha,
-- headful Chromium + 按设备限流 150 次/小时)。代价发生在**调用期间**,
-- "等它报错再判定"来不及,故必须在调之前排除(照参考
-- isAutoCheckinExcluded)。用户仍可手动领取。
if cap.auto_exclude then goto continue end
local legacy = accountOf(plat.id)
if legacy and legacy.enabled then
plugin.claim_daily({ platform = plat.id })
@ -6850,7 +6948,245 @@ end
return adapter
]==]
-- installTraeAdapter():装/更新本插件自己的 TRAE 直连适配器。
-- ============================================================================
-- ============================================================================
-- loomy 领取:触发每日额度重置(照参考 loomy-credits.ts:claimLoomyDailyQuota)
-- ============================================================================
-- ⚠️ 语义是「触发每日额度重置」,**不是**「+5000 积分」:
-- dailyBalance = dailyQuota - dailyConsumed,消耗后不回补。
-- ⚠️ 幂等判据是 `alreadyProcessed`(重复调用同样 HTTP 200)→ 映射成
-- already-claimed,不是 claimed(谎报收益比报错更糟)。
-- 本函数不抛错:失败也返回 failed,保证批量领取不因单个账号中断。
function claimLoomy(plat, tok, args)
local cred = (args and args.credential) or nil
local base = (cred and (cred.api_base or cred.apiBase)) or "https://loomy.xunfei.cn"
local root = base:gsub("/chat/completions$", "")
local url = root .. "/points/first-login"
local resp, err = __llmsproxy_http({
url = url, method = "POST",
headers = {
["Authorization"] = "Bearer " .. tok,
["Accept"] = "application/json",
["Content-Type"] = "application/json",
},
body = "{}",
})
if err or not resp then
return { done = true, kind = "failed", message = "领取失败: " .. tostring(err) }
end
local ok, rec = pcall(json.decode, resp.body or "")
if not ok or type(rec) ~= "table" then rec = {} end
if resp.status ~= 200 then
local msg = rec.message or rec.msg or ""
return { done = true, kind = "failed",
message = (msg ~= "" and tostring(msg)) or ("HTTP " .. tostring(resp.status)) }
end
local data = rec.data or rec
if type(data) ~= "table" then data = {} end
local dailyQuota = tonumber(data.dailyQuota)
local dailyBalance = tonumber(data.dailyBalance)
if data.alreadyProcessed == true then
return { done = true, kind = "already-claimed",
message = dailyQuota and
string.format("今日额度已初始化(每日 %s/%s)",
tostring(dailyBalance or 0), tostring(dailyQuota))
or "今日额度已初始化" }
end
local dailyConsumed = tonumber(data.dailyConsumed)
local granted = 0
if dailyQuota and dailyConsumed then
granted = math.max(0, dailyQuota - dailyConsumed)
end
return { done = true, kind = "claimed", credit = granted, unit = "quota" }
end
-- ============================================================================
-- zcode 领取:每日额度(照参考 zcode-upstream.ts:claimZcodePlan)
-- ============================================================================
-- 三步(照参考 zcode-auth.ts:claimDailyWith 的顺序):
-- ① 补激活上报(event/report,**不需要 Authorization**)
-- ② preview 查可领 plan(`?app_version=&platform=win32`)
-- ③ 逐个 plan POST billing/claim,body={plan_id}
--
-- ⚠️ claim **始终强制索要 captcha**(上游校验前置于 plan 校验)。参考的
-- captcha 有两个来源:DSH Desktop 的 webview guest 产的 param("内部载体"),
-- 或外挂 headful Chromium。**我们没有那个桌面壳** ⇒ 只能走浏览器路径。
--
-- ⇒ 因此:
-- * 本函数在**没有 captcha param 时如实失败**(不伪造、不重试),
-- 并给出可操作提示 —— 谎报"已领取"比报错更糟。
-- * auto_claim 已把 zcode 列进 auto_exclude(照参考
-- isAutoCheckinExcluded):代价发生在调用期间(拉 headful Chromium +
-- 阿里云按设备限流 150 次/小时),"等它报错再判定"来不及。
-- * 用户**手动**点领取时带上 captcha param 即可走通(面板交互)。
local ZCODE_ORIGIN = "https://zcode.z.ai"
-- zcodeHeaders:照参考 buildZcodeHeaders。X-Device-Mid 是**硬需求**
-- (缺它 billing 全家桶回 400)。
local function zcodeHeaders(cred, opts)
opts = opts or {}
local h = {
["Accept"] = "application/json",
["Content-Type"] = "application/json",
["X-Device-Mid"] = tostring(cred.device_mid or ""),
["X-App-Version"] = tostring(cred.app_version or "1.0.0"),
["X-Client-Platform"] = "win32",
}
if opts.authorization then h["Authorization"] = opts.authorization end
if opts.captcha then
h["X-Captcha-Param"] = opts.captcha.param
h["X-Captcha-Region"] = opts.captcha.region or ""
end
return h
end
-- zcodeClaimablePlans(cred):查可领 plan(preview)。
local function zcodeClaimablePlans(cred)
local av = tostring(cred.app_version or "1.0.0")
local url = ZCODE_ORIGIN .. "/api/v1/zcode-plan/billing/preview"
.. "?app_version=" .. rfc3986Escape(av) .. "&platform=win32"
local resp, err = __llmsproxy_http({
url = url, method = "GET",
headers = zcodeHeaders(cred, {
authorization = "Bearer " .. tostring(cred.zcode_jwt or cred.access_token or ""),
}),
})
if err or not resp then return nil, "查询可领计划失败: " .. tostring(err) end
local ok, rec = pcall(json.decode, resp.body or "")
if not ok or type(rec) ~= "table" then return nil, "响应不是 JSON" end
local plans = rec.plans or (rec.data and rec.data.plans) or {}
local out = {}
for _, pl in ipairs(plans) do
if type(pl) == "table" then
local id = pl.plan_id or pl.planId
if id then out[#out + 1] = tostring(id) end
end
end
return out, nil
end
function claimZcode(plat, tok, args)
local cred = (args and args.credential) or {}
local jwt = cred.zcode_jwt or cred.access_token or tok
cred = cred or {}
if not cred.zcode_jwt and not cred.access_token then cred.zcode_jwt = tok end
-- ① 补激活上报(照参考:不需要 Authorization)
__llmsproxy_http({
url = ZCODE_ORIGIN .. "/api/v1/event/report", method = "POST",
headers = { ["Content-Type"] = "application/json" },
body = json.encode({ event = "activation", app_version = tostring(cred.app_version or "1.0.0") }),
})
-- ② 查可领 plan
local plans, perr = zcodeClaimablePlans(cred)
if not plans then
return { done = true, kind = "failed", message = perr }
end
if #plans == 0 then
-- ⚠️ 照参考:「没有可领 plan」**不等于**「今天已领」—— 也可能活动未投放。
-- 说成 already-claimed 是谎报。
return { done = true, kind = "inactive",
message = "当前没有可领取的计划(可能活动未投放,或今日已领)" }
end
-- ③ 每个 plan 都要一个 captcha param。没有就如实失败。
local captcha = args and args.captcha
if type(captcha) ~= "table" or not captcha.param or captcha.param == "" then
return { done = true, kind = "action_required",
message = "zcode 领取需要验证码参数(captcha param)。"
.. "本机无 DSH Desktop 内部载体,需在面板手动领取(会拉起浏览器);"
.. "自动签到已按参考把它排除。" }
end
local granted, failedMsg = 0, nil
for _, planId in ipairs(plans) do
local resp, err = __llmsproxy_http({
url = ZCODE_ORIGIN .. "/api/v1/zcode-plan/billing/claim", method = "POST",
headers = zcodeHeaders(cred, {
authorization = "Bearer " .. tostring(jwt),
captcha = { param = captcha.param, region = captcha.region or "cn" },
}),
body = json.encode({ plan_id = planId }),
})
if err or not resp then
failedMsg = "领取失败: " .. tostring(err)
else
local ok, rec = pcall(json.decode, resp.body or "")
if ok and type(rec) == "table" then
-- ⚠️ 业务码可能是**纯数字字符串**("3012"),tonumber 必须能认
local code = tonumber(rec.code) or tonumber(tostring(rec.code or ""))
local msg = tostring(rec.msg or rec.message or "")
if code == 3012 or msg:find("unusual activity", 1, true) then
failedMsg = "上游风控拦截(3012 unusual activity)—— 需重新获取验证码"
elseif code and code ~= 0 and code ~= 200 then
failedMsg = msg ~= "" and msg or ("业务码 " .. tostring(code))
else
granted = granted + 1
end
else
failedMsg = "响应不是 JSON(HTTP " .. tostring(resp.status) .. ")"
end
end
end
if granted == 0 then
return { done = true, kind = "failed", message = failedMsg or "全部计划领取失败" }
end
return { done = true, kind = "claimed", credit = granted, unit = "plans",
message = ("已领取 " .. granted .. "/" .. #plans .. " 个计划")
.. (failedMsg and ("(部分失败:" .. failedMsg .. ")") or "") }
end
-- raccoon 领取:桌面端登录奖励(照参考 raccoon-credits.ts:claimRaccoonLoginReward)
-- ============================================================================
-- 端点:POST {apiBase}/api/web/desktop/v1/login/points/grant
-- ⚠️ 幂等判据是响应里的 `granted` 字段:**重复领取同样返回 HTTP 200 但
-- granted=false**。据此映射成 already-claimed,而不是 claimed ——
-- 否则用户会以为每天都真的加了额度(谎报收益比报错更糟)。
-- 失败时若上游给了 message,用它(比通用文案有用)。
function claimRaccoon(plat, tok, args)
local cred = (args and args.credential) or nil
local base = (cred and (cred.api_base or cred.apiBase)) or plat.base_url or ""
-- base_url 在平台表里含 llm 前缀(.../api/web/llm/v2),领取端点不在这条路径上,
-- 故从凭据或平台根域名取。参考用 product.apiBase + RACCOON_DESKTOP_PREFIX。
local root = base:gsub("/api/web/llm/v2$", "")
local url = root .. "/api/web/desktop/v1/login/points/grant"
local h = {
["Authorization"] = "Bearer " .. tok,
["Accept"] = "application/json",
["Content-Type"] = "application/json",
["X-Org-Code"] = (cred and (cred.office_identity or "")) or "",
["X-Raccoon-Language"] = "zh",
}
if cred and cred.device_id and cred.device_id ~= "" then
h["X-Device-Id"] = cred.device_id
end
local resp, err = __llmsproxy_http({ url = url, method = "POST", headers = h, body = "{}" })
if err or not resp then
return { done = true, kind = "failed",
message = "领取请求失败: " .. tostring(err) }
end
local ok, rec = pcall(json.decode, resp.body or "")
if not ok or type(rec) ~= "table" then rec = {} end
-- 信封:{code, message, data:{granted, popup:{points}}}
local env = rec.data and rec or rec
if resp.status ~= 200 or (rec.code and rec.code ~= 0 and rec.code ~= 200) then
local msg = rec.message or rec.msg or ""
return { done = true, kind = "failed",
message = (msg ~= "" and tostring(msg)) or ("HTTP " .. tostring(resp.status)) }
end
local data = rec.data or env
if type(data) ~= "table" or data.granted ~= true then
-- 幂等:HTTP 200 但 granted 非 true = 已经领过(每号一次)
return { done = true, kind = "already-claimed",
message = "该账号已领取过桌面端登录奖励(每号一次)" }
end
local points = tonumber((data.popup or {}).points) or 3000
return { done = true, kind = "claimed", credit = points, unit = "points" }
end
-- installRaccoonAdapter
local function installTraeAdapter()
if not __llmsproxy_adapters then return false, "无适配器桥(旧内核?)" end
-- 本地预编译:装载失败只回"源码无法编译",不给行号;load 失败给行号。
@ -7638,6 +7974,40 @@ plugin.ui = {
},
}
-- plugin.credits_capability:只给测试用 —— 暴露能力表条目,让判据能断言
-- "表与参考真值一致"(表错了只会静默少领,没有任何报错)。
function plugin.credits_capability(args)
args = args or {}
local cap = CHANNEL_CREDITS[args.platform]
if not cap then return { json = { error = "无能力声明" } } end
local out = {}
for k, v in pairs(cap) do
if type(v) ~= "function" then out[k] = v end
end
return { json = { capability = out } }
end
-- plugin.claims_capable:只给测试用 —— 判断某平台**真的**能不能领
--(有无 checkin 表或 claim_fn)。这与"能力表声称能领"是两件事,
-- 两者不一致就会出现自相矛盾的文案。
function plugin.claims_capable(args)
args = args or {}
local plat = platformById(args.platform)
if not plat then return { json = { error = "未知平台" } } end
local has_checkin = plat.checkin ~= nil and plat.checkin.url ~= nil
local has_fn = plat.claim_fn ~= nil
local cap = CHANNEL_CREDITS[plat.id] or {}
return { json = {
capable = (has_checkin or has_fn),
has_checkin = has_checkin,
has_claim_fn = has_fn,
table_says_claim = cap.claim == true,
auto_exclude = cap.auto_exclude == true,
reason = (not (has_checkin or has_fn)) and
("既无 checkin 表也无 claim_fn(claim_fn=" .. tostring(plat.claim_fn ~= nil) .. ")") or "",
} }
end
-- plugin.__adapter_install_probe:只给测试/诊断用 —— 报告本插件自装适配器的
-- 装载结果(含语法错误详情)。"装了但没生效"这类问题没有它只能靠猜。
function plugin.__adapter_install_probe()
@ -7691,6 +8061,37 @@ function plugin.__sync_source_probe(args)
models_n = #models } }
end
-- 绑定 zcode 的领取钩子(同 raccoon/loomy:local 前向引用,必须放末尾)。
do
for _, p in ipairs(PLATFORMS) do
if p.id == "zcode" then
p.claim_fn = function(plat, tok, args) return claimZcode(plat, tok, args) end
break
end
end
end
-- 绑定 loomy 的领取钩子(同 raccoon:local 前向引用,必须放末尾)。
do
for _, p in ipairs(PLATFORMS) do
if p.id == "loomy" then
p.claim_fn = function(plat, tok, args) return claimLoomy(plat, tok, args) end
break
end
end
end
-- 绑定 raccoon 的领取钩子。必须放在**文件末尾**:claimRaccoon 是 local,
-- 只能从定义处向后可见(本文件已因 local 前向引用踩过 8 次)。
do
for _, p in ipairs(PLATFORMS) do
if p.id == "raccoon" then
p.claim_fn = function(plat, tok, args) return claimRaccoon(plat, tok, args) end
break
end
end
end
-- 装本插件自己的专用适配器。必须放在**文件末尾**(installCodeartsAdapter 是
-- local,只能从定义处向后可见 —— 本文件已因 local 前向引用踩过 7 次)。
-- 放在脚本执行末尾即可:适配器装载与源注册都发生在脚本执行完之后。