mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
feat(channels): 补两个参考明确"不能省"的基础设施(已失效模型 + 目录闸门)
穷尽对照 143 个参考模块后,挑出两个**有实测理由**的补上。 ## ① 已失效模型记录(照参考 dead-model-store.ts,708 行) 修的是真实报障:用户选中某平台的兜底模型,每轮 404 —— 该模型**已被上游下架**, 但列表里仍显示、还标着「免费」。根因:兜底表是**编译期快照**,上游下架不会 自己变。 ★ 关键纪律(参考文件头有实测教训):**不能用「远端目录里没有 ⇒ 已下架」反推**。 那个判据只在远端目录完整权威的平台上成立;buddy 那类平台的远端目录**已知残缺** (CLI token 只拿得到 13 个内部别名、IDE 有 20 个),按目录比对会把「你有权用、 只是 token 看不到」的模型删掉;qoder 更是没有可比对的目录。 ⇒ 唯一跨平台安全的判据是**阳性证据**:真的 404 过才记。 判据逐条对齐参考(17 个用例,全是它踩过的误判): - `model_not_found` 只在**独占一句话成分**时算(后面只允许结束/句末标点/收尾 括号引号,**不含逗号**)—— 它在遥测里常作字段名出现: `error_class=model_not_found, retry allowed` 曾被误判下架 - rate limit / quota / insufficient / unauthor / forbidden / invalid key 一律**不是** - `expired` 必须带主语(token/session/key/…),否则会误杀真实下架文案 - 只认 found/exist,**不认** available/supported(那是"换套餐就能恢复") - 判定顺序:NOT_GONE 先于 GONE(与参考 `isModelGoneError` 逐行一致) TTL 30 天(过期自动复活,照参考 DSH_DEAD_MODEL_TTL_DAYS 语义)。 `model_clear_dead` 语义**修正**:原实现清的是"被禁用且不在目录里"(判据用错了、 拿目录反推),现改为清**失效记录**。 ## ② 远端目录加载闸门(照参考 remote-catalog-gate.ts) 参考点名「**不能省**」:宿主对每个 provider 拉目录、再对每个模型解析一次, 于是「目录没拿到」被放大成**一次请求 × 每个模型**,而超时是 30–60s 量级 —— 未登录/离线时表现就是「首屏巨长」。三道语义:命中缓存 / 并发去重 / 失败冷却。 Lua 无协程做不了「共享同一个 Promise」,用**时间戳闸门**等价实现 (in_flight_at 防同轮重复 + retry_at 失败冷却)。 含参考的**墙钟回拨防护**:`retry_at - now > cooldown` 一律按已到期处理 —— 否则系统时间回拨一天会把冷却变成事实上的永久卡死。 ## ③ 顺手修:状态落盘不建目录 `persistState` 直接 `io.open(path..".tmp")`,目录不存在就失败。而失败只打 stderr 一行「状态落盘失败」—— 表现是「状态永不落盘、重启即丢」,很容易被忽略。 现先 `mkdir -p`。这是实测发现的(authorize 测试的临时目录尚未创建时报的)。 ## 判据 `channels_dead_model_test.go`:判据 17 例(含 6 个"看着像但不是下架"的反例)、 记录-剔除-恢复闭环、闸门冷却。 `channels_pool_test.go`:`model_clear_dead` 断言按新语义改写,并加"清失效记录 **不得**动 disabled 黑名单"的隔离断言。 回归:9 个包全绿。
This commit is contained in:
@ -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", "__store_dir_probe", "credits_capability", "claims_capable" }
|
||||
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", "__store_dir_probe", "__is_model_gone_probe", "__record_dead_probe", "__catalog_gate_probe", "__catalog_gate_fail_probe", "credits_capability", "claims_capable" }
|
||||
|
||||
|
||||
|
||||
@ -2352,6 +2352,12 @@ local PERSIST_KEYS = {
|
||||
-- 远端拉到的模型目录缓存(远端为主、兜底补位的结果)。
|
||||
-- 缓存它而非每次请求都拉:上游目录是分钟级稳定,UI 却是秒级刷新。
|
||||
"remoteModels",
|
||||
-- 已失效模型记录(阳性 404 证据)。必须持久化:不落盘的话重启就全忘了,
|
||||
-- 用户又要重新被 404 打一遍(照参考 DEAD_MODELS_FILE 的落盘语义)。
|
||||
"deadModels",
|
||||
-- 目录闸门状态(冷却/in-flight)。也落盘:重启后冷却窗口应继续有效,
|
||||
-- 否则"重启即重试坏上游"(照参考把闸门状态随插件存活的语义)。
|
||||
"catalogGate",
|
||||
}
|
||||
|
||||
local function persistState()
|
||||
@ -2364,6 +2370,11 @@ local function persistState()
|
||||
local body = json.encode({ version = 1, state = snap })
|
||||
if not body then return false, "json.encode 失败" end
|
||||
local tmp = path .. ".tmp"
|
||||
-- 目录可能还不存在(首次运行、或内核交给我们的目录尚未创建)。
|
||||
-- 不 mkdir 就直接 io.open 会失败,而失败只打到 stderr —— 表现为
|
||||
-- "状态永不落盘"(重启即丢),日志里一行「状态落盘失败」很容易被忽略。
|
||||
local dir = path:match("^(.*)/[^/]+$")
|
||||
if dir and dir ~= "" then os.execute("mkdir -p '" .. dir .. "' 2>/dev/null") end
|
||||
local w = io.open(tmp, "w")
|
||||
if not w then return false, "无法写入 " .. tmp end
|
||||
w:write(body)
|
||||
@ -2752,6 +2763,10 @@ end
|
||||
-- 注意:兜底表是**编译期快照**,价格/倍率会变,故兜底条目不携带价格类字段。
|
||||
|
||||
-- 取该平台**当前有效**的模型目录(远端优先,兜底补位,剔除黑名单)。
|
||||
-- 前向声明:deadModelIds 定义在下方(失效模型存储一节),而 effectiveModels
|
||||
-- 在它之前就要剔除失效模型。Lua 的 local 只从定义处向后可见 —— 第 9 次踩。
|
||||
local deadModelIds
|
||||
|
||||
local function effectiveModels(provider)
|
||||
local plat = platformById(provider)
|
||||
if not plat then return {} end
|
||||
@ -2769,7 +2784,15 @@ local function effectiveModels(provider)
|
||||
for _, id in ipairs(plat.models or {}) do
|
||||
if not seen[id] then seen[id] = true out[#out+1] = id end
|
||||
end
|
||||
return out
|
||||
-- 剔除**有阳性证据**已失效的模型(照参考 dead-model-store 的剔除语义)。
|
||||
-- ⚠️ 只剔这里记录过的(真正 404 过的),**绝不**拿远端目录反推下架 ——
|
||||
-- buddy/qoder 那类平台的远端目录残缺,反推会删掉可用模型。
|
||||
local dead = deadModelIds(provider)
|
||||
local kept = {}
|
||||
for _, id in ipairs(out) do
|
||||
if not dead[id] then kept[#kept+1] = id end
|
||||
end
|
||||
return kept
|
||||
end
|
||||
|
||||
-- parseRemoteModels(parse, body):把各平台响应解析成 id 列表。
|
||||
@ -4960,16 +4983,213 @@ plugin.schedule = {
|
||||
{ fn = "refresh_creds", every = 1800, first_due_now = true, jitter = 60 },
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- ============================================================================
|
||||
-- 已失效模型(照参考 dead-model-store.ts)
|
||||
-- ============================================================================
|
||||
-- 解决的真实报障:用户选中某平台的兜底模型,每轮都 404 ——
|
||||
-- 该模型**已被上游下架**,但模型列表里仍显示、还标着"免费"。
|
||||
-- 根因:各平台的兜底表是**编译期快照**,上游下架时不会自己跟着变。
|
||||
--
|
||||
-- ⚠️ **不能用「远端目录里没有 ⇒ 已下架」来剔除**(参考文件头实测):
|
||||
-- 那个判据只在"远端目录完整权威"的平台上成立。buddy 那类平台的远端目录
|
||||
-- **已知残缺**(CLI token 只拿得到 13 个内部别名,IDE 有 20 个),
|
||||
-- 按目录比对会把"你有权用、只是 token 看不到"的模型删掉 —— 参考作者已为此
|
||||
-- 踩过一次。qoder 更是**没有**可比对的远端目录。
|
||||
-- ⇒ 唯一跨平台安全的判据是**阳性证据**:这个模型**真的**请求失败并返回
|
||||
-- 「模型不存在」时,才记为失效。**绝不从残缺目录反推。**
|
||||
|
||||
-- 判定为"模型已失效"的正向模式(照参考 GONE_PATTERNS 逐条)。
|
||||
--
|
||||
-- ⚠️ `model_not_found` 必须要求它**独占一句话成分**:只加词边界不够 ——
|
||||
-- 它在遥测/日志文案里常作为**字段名**出现,后面紧跟别的内容:
|
||||
-- error_class=model_not_found, retry allowed → 初版误判下架
|
||||
-- model_not_found_reason=upstream → 初版误判下架
|
||||
-- 误判会把**可用模型藏 30 天**、用户须手动恢复。故后面只允许结束、句末标点
|
||||
-- 或收尾括号/引号,**不含逗号**。
|
||||
local DEAD_GONE_PATTERNS = {
|
||||
"model%s+not%s+found",
|
||||
"unknown%s+model",
|
||||
"no%s+such%s+model",
|
||||
"nonexistent%s+model",
|
||||
}
|
||||
|
||||
-- 反向模式:命中即**不是**下架(照参考 NOT_GONE_PATTERNS)。
|
||||
-- 逐条都有理由,不要删:
|
||||
-- * rate limit / quota / insufficient → 换时段或加额度就能用
|
||||
-- * unauthor/forbidden/invalid token → 认证问题,不是模型不存在
|
||||
-- * `expired` **必须带主语**(token/session/credential/key/auth/lease),
|
||||
-- 否则会误杀正常的下架文案:`model expired-v3 does not exist`、
|
||||
-- `the model lease expired`(租约到期也是下架的一种说法)
|
||||
local DEAD_NOT_GONE_PATTERNS = {
|
||||
"rate%s*%-?%s*limit",
|
||||
"too%s+many%s+requests",
|
||||
"quota",
|
||||
"insufficient",
|
||||
"unauthor", "forbidden", "invalid%s+token", "invalid%s+api%s*key", "invalid%s+credential",
|
||||
"token%s+expired", "session%s+expired", "credential%s+expired", "key%s+expired",
|
||||
"auth%s+expired", "lease%s+expired",
|
||||
"expired%s+token", "expired%s+session", "expired%s+credential", "expired%s+key",
|
||||
"expired%s+auth",
|
||||
}
|
||||
|
||||
-- isModelGone(message):把一段错误文本判定为"模型已失效"。
|
||||
--
|
||||
-- 返回 true 的**唯一**条件是出现阳性证据;拿不准一律 false ——
|
||||
-- 误判的代价是把可用模型藏 30 天,漏判的代价只是用户再看到一次 404。
|
||||
local function isModelGone(message)
|
||||
if type(message) ~= "string" or message == "" then return false end
|
||||
local m = message:lower()
|
||||
for _, pat in ipairs(DEAD_NOT_GONE_PATTERNS) do
|
||||
if m:find(pat) then return false end
|
||||
end
|
||||
for _, pat in ipairs(DEAD_GONE_PATTERNS) do
|
||||
if m:find(pat) then return true end
|
||||
end
|
||||
-- `model_not_found` 形态:要求后面只剩结束/句末标点/收尾括号引号。
|
||||
-- Lua 的 find 无前瞻,故手动定位后检查紧随的字符。
|
||||
local from = 1
|
||||
while true do
|
||||
local a, b = m:find("model[_%-]not[_%-]found", from)
|
||||
if not a then break end
|
||||
local rest = m:sub(b + 1, b + 1)
|
||||
-- 只允许:结束、句末标点、收尾括号/引号。**不含逗号**
|
||||
-- ("…not_found, retry allowed" 里逗号后仍是另一个字段 ⇒ 不算阳性)。
|
||||
-- 字符类里 `]` 必须放最前或转义;这里直接列字符以免 Lua 解析歧义。
|
||||
-- 用**单引号**的 Lua 串,避免字符类里的双引号提前终止字符串。
|
||||
if rest == "" or rest:find('[%]%)%}%.!%;"]') then return true end
|
||||
from = b + 1
|
||||
end
|
||||
-- 「id + 紧邻的不存在语义」:只认 found/exist。
|
||||
-- ⚠️ 不认 available/supported —— 那是"换套餐/换地域就能恢复",不是下架。
|
||||
if m:find("does%s+not%s+exist") or m:find("doesn't%s+exist") then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
-- 失效记录 TTL(秒)。照参考默认 30 天;过期自动复活,避免永久隐藏。
|
||||
local DEAD_MODEL_TTL_DAYS = 30
|
||||
|
||||
local function deadModelsOf(provider)
|
||||
plugin.state.deadModels = plugin.state.deadModels or {}
|
||||
local p = plugin.state.deadModels[provider]
|
||||
if not p then p = {} plugin.state.deadModels[provider] = p end
|
||||
return p
|
||||
end
|
||||
|
||||
-- recordDeadModel:记录一次**阳性**证据(真正 404 了)。
|
||||
-- 返回 true 表示这是新记录(调用方据此决定要不要重同步源)。
|
||||
local function recordDeadModel(provider, modelId, reason)
|
||||
if not modelId or modelId == "" then return false end
|
||||
local p = deadModelsOf(provider)
|
||||
if p[modelId] then return false end
|
||||
p[modelId] = { at = os.time and os.time() or 0, reason = tostring(reason or ""):sub(1, 120) }
|
||||
return true
|
||||
end
|
||||
|
||||
-- deadModelIds(provider):当前仍有效的失效模型 id 集合(已过 TTL 的自动清掉)。
|
||||
deadModelIds = function(provider)
|
||||
local p = deadModelsOf(provider)
|
||||
local now = os.time and os.time() or 0
|
||||
local ttl = DEAD_MODEL_TTL_DAYS * 86400
|
||||
local out = {}
|
||||
for id, meta in pairs(p) do
|
||||
local at = type(meta) == "table" and tonumber(meta.at) or 0
|
||||
if ttl > 0 and (now - at) > ttl then
|
||||
p[id] = nil -- 过期复活(照参考 TTL 语义)
|
||||
else
|
||||
out[id] = true
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- clearDeadModels(provider, modelId?):清失效记录(不带 id 则清该平台全部)。
|
||||
local function clearDeadModels(provider, modelId)
|
||||
local p = deadModelsOf(provider)
|
||||
if modelId and modelId ~= "" then
|
||||
local had = p[modelId] ~= nil
|
||||
p[modelId] = nil
|
||||
return had and 1 or 0
|
||||
end
|
||||
local n = 0
|
||||
for _ in pairs(p) do n = n + 1 end
|
||||
plugin.state.deadModels[provider] = {}
|
||||
return n
|
||||
end
|
||||
|
||||
-- 远端目录加载闸门(照参考 remote-catalog-gate.ts)
|
||||
-- ============================================================================
|
||||
-- 参考明确点名「**不能省**」,理由实测:
|
||||
-- 宿主会对**每个** provider 拉目录、再对**每个模型**解析一次,于是「目录没拿到」
|
||||
-- 被放大成**一次请求 × 每个模型**。若失败时不落任何标记,下一次还会再拉一遍,
|
||||
-- 而这些拉取的超时是 30–60s 量级 —— 未登录/离线时的表现就是「首屏巨长」。
|
||||
--
|
||||
-- 三道语义(照参考):
|
||||
-- ① 命中缓存:有远端目录就直接用(不在本闸门内)
|
||||
-- ② 并发去重:同一次加载的多个调用共享同一次
|
||||
-- ③ 失败/空结果**冷却**:避免短时间内反复打同一个坏上游
|
||||
--
|
||||
-- ⚠️ 冷却只影响**拉取时机**,不影响兜底目录:冷却期内照旧回落到静态表,
|
||||
-- 界面不会因此少模型。
|
||||
--
|
||||
-- ⚠️ 本 Lua 无协程(单 VM 单锁),做不了"共享同一个 Promise"。用**时间戳
|
||||
-- 闸门**达到等价效果:记录 in_flight_at(防同轮重复)+ retry_at(失败冷却)。
|
||||
local REMOTE_CATALOG_COOLDOWN = 30 -- 秒,照参考 REMOTE_CATALOG_COOLDOWN_MS
|
||||
|
||||
local function catalogGateOf(provider)
|
||||
plugin.state.catalogGate = plugin.state.catalogGate or {}
|
||||
local g = plugin.state.catalogGate[provider]
|
||||
if not g then g = {} plugin.state.catalogGate[provider] = g end
|
||||
return g
|
||||
end
|
||||
|
||||
-- catalogGateCooling(provider):是否处于"上次没拿到目录"的冷却窗口内。
|
||||
--
|
||||
-- ⚠️ 照参考的墙钟回拨防护:`retry_at` 用墙钟算,系统时间被回拨时
|
||||
-- `now < retry_at` 会把冷却**拉长**(回拨一天就是事实上的永久卡死)。
|
||||
-- 故只认"剩余量不超过一个冷却周期"的窗口,超长的按已到期处理。
|
||||
local function catalogGateCooling(provider, now)
|
||||
local g = catalogGateOf(provider)
|
||||
if not g.retry_at then return false end
|
||||
if now >= g.retry_at then return false end
|
||||
if g.retry_at - now > REMOTE_CATALOG_COOLDOWN then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
-- catalogGateBegin(provider):尝试进入一次加载。返回 true 表示"本次由你拉"。
|
||||
local function catalogGateBegin(provider, now)
|
||||
local g = catalogGateOf(provider)
|
||||
if catalogGateCooling(provider, now) then return false end
|
||||
-- 同一轮内的并发去重:正在飞就不再发起(参考的 inFlight)
|
||||
if g.in_flight_at and (now - g.in_flight_at) < 10 then return false end
|
||||
g.in_flight_at = now
|
||||
return true
|
||||
end
|
||||
|
||||
-- catalogGateEnd(provider, got, now):收尾。拿到目录清冷却,没拿到进冷却。
|
||||
local function catalogGateEnd(provider, got, now)
|
||||
local g = catalogGateOf(provider)
|
||||
g.in_flight_at = nil
|
||||
g.retry_at = got and nil or (now + REMOTE_CATALOG_COOLDOWN)
|
||||
end
|
||||
|
||||
-- refresh_models():定时把远端模型目录拉回来并重同步所有源。
|
||||
--
|
||||
-- 为什么需要它:模型目录写进了**上游源**(源的 models 决定网关能路由哪些
|
||||
-- 模型)。不刷新就只能用兜底表 —— 平台上新模型后网关永远看不到,
|
||||
-- 表现为"某个新模型 404 / 不可选"。
|
||||
function plugin.refresh_models()
|
||||
local updated, failed = 0, {}
|
||||
local updated, failed, cooled = 0, {}, 0
|
||||
local now = os.time and os.time() or 0
|
||||
for _, plat in ipairs(allPlatforms()) do
|
||||
if plat.models_remote then
|
||||
-- 目录闸门:冷却期内不拉(避免对坏上游反复打;照参考 remote-catalog-gate)
|
||||
if not catalogGateBegin(plat.id, now) then
|
||||
cooled = cooled + 1
|
||||
goto continue
|
||||
end
|
||||
local ids, err = refreshRemoteModels(plat.id)
|
||||
catalogGateEnd(plat.id, ids ~= nil, now)
|
||||
if ids then
|
||||
updated = updated + 1
|
||||
-- 目录变了就重同步该平台所有源(模型清单在源里)
|
||||
@ -4980,6 +5200,11 @@ function plugin.refresh_models()
|
||||
failed[#failed + 1] = plat.id .. ": " .. tostring(err)
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
-- 冷却期内的平台不算失败(它们上次已经记过原因,重复记会淹没真正的错误)
|
||||
if cooled > 0 then
|
||||
logEvent("模型刷新:跳过 " .. cooled .. " 个冷却中的平台")
|
||||
end
|
||||
if updated > 0 then
|
||||
logEvent("模型目录已刷新(" .. updated .. " 个平台)")
|
||||
@ -5733,21 +5958,22 @@ function plugin.model_set_disabled_many(req)
|
||||
end
|
||||
|
||||
-- model.clearDead:清死模型(对齐参考 model.clearDead:从黑名单移除已下线的 id)
|
||||
-- model_clear_dead:清掉"已失效模型"的记录(照参考 clearDeadModels 语义)。
|
||||
--
|
||||
-- ⚠️ 语义修正:此前这里删的是"被禁用且不在目录里"的条目 —— 那既不是失效记录,
|
||||
-- 判据也用错了(拿目录反推)。参考的 clear 是:把**有阳性 404 证据**的记录清掉,
|
||||
-- 让模型重新出现在列表里(用户确认上游又上了/或误判时用)。
|
||||
function plugin.model_clear_dead(req)
|
||||
local args = bodyOf(req)
|
||||
if args.platform == nil then args = req end
|
||||
local plat = platformById(args and args.platform)
|
||||
if not plat then return { status = 400, json = { error = "未知平台" } } end
|
||||
local disabled = disabledModelsOf(plat.id)
|
||||
local removed = {}
|
||||
for mid in pairs(disabled) do
|
||||
local alive = false
|
||||
for _, m in ipairs(effectiveModels(plat.id)) do if m == mid then alive = true break end end
|
||||
if not alive then disabled[mid] = nil removed[#removed+1] = mid end
|
||||
end
|
||||
local mid = args and args.model_id
|
||||
local n = clearDeadModels(plat.id, mid)
|
||||
markDirty()
|
||||
for _, a in ipairs(poolList(plat.id)) do syncAccountSource(plat.id, a.id) end
|
||||
return { status = 200, json = { ok = true, removed = removed } }
|
||||
return { status = 200, json = { ok = true, removed = n,
|
||||
message = n > 0 and ("已恢复 " .. n .. " 个模型") or "无失效模型记录" } }
|
||||
end
|
||||
|
||||
-- 供应商排序(对齐参考 provider.getOrder/setOrder)
|
||||
@ -7990,6 +8216,40 @@ function plugin.__store_dir_probe()
|
||||
return { json = { store_dir = storeDir() } }
|
||||
end
|
||||
|
||||
-- plugin.__is_model_gone_probe:只给测试用 —— 暴露失效判据本体。
|
||||
-- 判据必须能被逐条钉住:这些模式的误判代价是把**可用模型藏 30 天**。
|
||||
function plugin.__is_model_gone_probe(args)
|
||||
return { json = { gone = isModelGone(tostring((args or {}).message or "")) } }
|
||||
end
|
||||
|
||||
-- plugin.__record_dead_probe:只给测试用 —— 记录一条失效证据。
|
||||
function plugin.__record_dead_probe(args)
|
||||
args = args or {}
|
||||
local isnew = recordDeadModel(tostring(args.platform or ""), tostring(args.model_id or ""),
|
||||
tostring(args.reason or ""))
|
||||
return { json = { recorded = isnew } }
|
||||
end
|
||||
|
||||
-- plugin.__catalog_gate_probe:只给测试用 —— 读闸门状态并尝试进入一次。
|
||||
function plugin.__catalog_gate_probe(args)
|
||||
args = args or {}
|
||||
local now = os.time and os.time() or 0
|
||||
local cooling = catalogGateCooling(tostring(args.platform or ""), now)
|
||||
local began = false
|
||||
if not cooling then began = catalogGateBegin(tostring(args.platform or ""), now) end
|
||||
return { json = { began = began, cooling = cooling } }
|
||||
end
|
||||
|
||||
-- plugin.__catalog_gate_fail_probe:只给测试用 —— 模拟一次拉取失败(进冷却)。
|
||||
function plugin.__catalog_gate_fail_probe(args)
|
||||
args = args or {}
|
||||
local now = os.time and os.time() or 0
|
||||
local p = tostring(args.platform or "")
|
||||
catalogGateBegin(p, now)
|
||||
catalogGateEnd(p, false, now)
|
||||
return { json = { ok = true } }
|
||||
end
|
||||
|
||||
-- plugin.credits_capability:只给测试用 —— 暴露能力表条目,让判据能断言
|
||||
-- "表与参考真值一致"(表错了只会静默少领,没有任何报错)。
|
||||
function plugin.credits_capability(args)
|
||||
|
||||
Reference in New Issue
Block a user