fix(channels): 渠道卡片 logged_in 改成看账号池(此前所有池接入的平台都显示未登录)

## 真因:logged_in 判的是旧单账号键
用户报「codearts 还是不可用」。查下来渠道**本身是通的**(9 个模型逐个实测都返回
内容),不可用的是**UI 的登录态判断**:

`list_platforms` 写的是 `credentialOf("channels:" .. platform)` —— 那是**旧版
单账号**的保险库键。现网凭据键是 `channels:<platform>:<accountId>`(一平台多
账号),于是**所有**通过账号池接入的平台都被判成"未登录":

    修复前(生产):trae=False minimax=False codearts=False qoder=False zcode=False
                    (只有 buddy=True —— 它还留着一条旧格式凭据)
    修复后:          trae=True  minimax=True  codearts=True qoder=True  zcode=True

UI 据此把渠道卡片渲染成"未接入",用户看到的就是"渠道不可用"。底层模型完全能用 ——
这是最容易被误判成"上游挂了"的一类假信号。

## 修法
- `list_platforms`:logged_in 改为遍历账号池(`poolList` + 凭据存在 + enabled≠false),
  旧单账号仅作兜底;顺带回 `account_id` / `accounts` 计数,让 UI 能区分同平台多账号;
- `status_page`:`accounts` 同样合并账号池(此前只回旧表,状态页里池接入的平台
  是空的 —— 同一根因的第二个出口);
- 新增 `__platforms_probe`(`list_platforms` 是 HTTP 路由处理器、不在 exports 里,
  判据走探针,与仓库既有做法一致);
- 回归判据 `TestPlatformsLoggedInFromAccountPool`:池里有凭据账号 ⇒ logged_in=true,
  且 account_id / account / accounts 回池数据。

## 顺带核实(不是代码缺陷)
- codearts 目录里 4 个模型(`deepseek-v4-flash-0731` / `glm-5.3-flash` /
  `deepseek-v4-pro-0813` / `CodeArts-Pangu`)实测上游 404
  `InferHub.002002009.404: The model is not registered` ⇒ 之前被手工停用是对的,
  已按死模型流程收口(不再回到可选列表);
- `GLM-5.3` 同样 404,已标失效并自动从源里剔除(日志:
  「模型已失效,已标记 codearts/GLM-5.3」);
- `deepseek-v4-flash` 早前的 429 是 TPM 限流(我自己连续压测造成),现已恢复 OK。
- 最终 codearts:**9 个模型,9/9 实测返回内容**。
This commit is contained in:
root
2026-10-20 09:54:06 +08:00
parent b5c89fd3d7
commit 9feef8f4cb
2 changed files with 126 additions and 7 deletions

View File

@ -140,7 +140,7 @@ plugin.kernel = {
-- WebUI 可调用的导出函数(POST /api/plugins/channels/call/{fn})。
-- set_endpoints 让运维把渠道端点指向自建代理/镜像;测试用它打假上游。
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_clear_credentialless", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "account_adopt", "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_pool_reason", "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", "__set_loopback_probe", "reap_loopbacks", "reap_orphan_sources", "__add_source_for_test", "__forget_account_probe", "__set_pool_cred_probe", "__cred_headers_probe", "__b64_probe", "__codearts_jwk_pem_probe", "__platform_def_probe", "__zcode_diag_probe", "__trae_entry_probe", "__trae_catalog_probe", "__trae_headers_probe", "__list_sources_probe", "__clear_priorities_probe", "__resync_sources_probe", "__utf8_clip_probe", "__event_log_probe", "__is_model_gone_probe", "__rank_accounts_probe", "__chosen_account_probe", "__inject_refresh_platform_probe", "__clear_capabilities_probe", "__qoder_claim_outcome_probe", "__is_free_probe", "__claim_covers_probe", "__schedule_probe", "__parse_capability_probe", "__inject_capability_probe", "__ttl_reset_probe", "__ttl_probe", "__ttl_invalidate_probe", "__serial_probe", "__serial_release_probe", "__set_legacy_cred_probe", "__read_cred_probe", "__clear_all_creds_probe", "credits_tier_probe", "__set_lock_permanent_probe", "__set_expiring_window_probe", "__expiring_window_probe", "__record_dead_probe", "__catalog_gate_probe", "__catalog_gate_fail_probe", "credits_capability", "claims_capable" }
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_clear_credentialless", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "account_adopt", "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_pool_reason", "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", "__set_loopback_probe", "reap_loopbacks", "reap_orphan_sources", "__add_source_for_test", "__forget_account_probe", "__set_pool_cred_probe", "__cred_headers_probe", "__b64_probe", "__codearts_jwk_pem_probe", "__platform_def_probe", "__zcode_diag_probe", "__trae_entry_probe", "__trae_catalog_probe", "__trae_headers_probe", "__platforms_probe", "__list_sources_probe", "__clear_priorities_probe", "__resync_sources_probe", "__utf8_clip_probe", "__event_log_probe", "__is_model_gone_probe", "__rank_accounts_probe", "__chosen_account_probe", "__inject_refresh_platform_probe", "__clear_capabilities_probe", "__qoder_claim_outcome_probe", "__is_free_probe", "__claim_covers_probe", "__schedule_probe", "__parse_capability_probe", "__inject_capability_probe", "__ttl_reset_probe", "__ttl_probe", "__ttl_invalidate_probe", "__serial_probe", "__serial_release_probe", "__set_legacy_cred_probe", "__read_cred_probe", "__clear_all_creds_probe", "credits_tier_probe", "__set_lock_permanent_probe", "__set_expiring_window_probe", "__expiring_window_probe", "__record_dead_probe", "__catalog_gate_probe", "__catalog_gate_fail_probe", "credits_capability", "claims_capable" }
@ -9083,14 +9083,42 @@ end
function plugin.list_platforms(req)
local out = {}
for _, p in ipairs(allPlatforms()) do
-- ★ logged_in 必须看**账号池**,不能只看旧的单账号键。
--
-- 真实缺陷(2026-10-20 用户报「codearts 还是不可用」):这里原本
-- `credentialOf("channels:" .. id)` —— 那是**旧版单账号**的保险库键;
-- 现网凭据键是 `channels:<platform>:<accountId>`(一平台多账号),
-- 于是**所有**通过账号池接入的平台都被判成未登录(buddy 之所以显示已登录,
-- 是因为它还留着一条旧格式凭据)。UI 据此把渠道卡片渲染成"未接入",
-- 用户看到的就是"渠道不可用",而底层模型其实完全能用。
local logged, first, claimed = false, nil, nil
for _, a in ipairs(poolList(p.id)) do
if a.enabled ~= false and credentialOf(a.credentialRef) ~= nil then
logged = true
if first == nil then first = a end
if claimed == nil and a.last_claim ~= nil then claimed = a.last_claim end
end
end
-- 兼容旧单账号:池里没有时回落到 state.accounts + 旧凭据键
local acct = accountOf(p.id) or {}
local has = credentialOf("channels:" .. p.id)
local hasLegacy = credentialOf("channels:" .. p.id) ~= nil
if not logged and hasLegacy then logged = true end
if first == nil and (hasLegacy or acct.account ~= nil) then
first = { id = acct.id, nickname = acct.account, enabled = acct.enabled }
end
out[#out + 1] = {
id = p.id, name = p.name, flow = p.flow,
logged_in = has ~= nil,
account = acct.account,
enabled = acct.enabled,
last_claim = acct.last_claim,
logged_in = logged,
-- 账号展示:池里第一个可用账号优先(id 是 12 位 hex,UI 用来区分同平台多账号)
account_id = first and first.id or nil,
account = (first and first.nickname) or acct.account,
enabled = first and first.enabled ~= false or acct.enabled,
last_claim = claimed or acct.last_claim,
accounts = (function()
local n = 0
for _, a in ipairs(poolList(p.id)) do if a.enabled ~= false then n = n + 1 end end
return n
end)(),
}
end
return { status = 200, json = { platforms = out } }
@ -9429,8 +9457,32 @@ end
function plugin.status_page(req)
local r = __llmsproxy_kernel.call("status", {})
-- accounts 必须**合并账号池**:只回 state.accounts(旧单账号表)时,
-- 通过账号池接入的平台在这里是空的,UI 的状态页于是显示"没有账号",
-- 与 platforms 的 logged_in 一样会让人误判成"渠道不可用"
-- (同一根因的两个出口,2026-10-20 一起修)。
local accounts = {}
for k, v in pairs(plugin.state.accounts or {}) do accounts[k] = v end
for _, p in ipairs(allPlatforms()) do
local list = {}
for _, a in ipairs(poolList(p.id)) do
list[#list + 1] = {
id = a.id, nickname = a.nickname, enabled = a.enabled ~= false,
expiresAt = a.expiresAt, last_claim = a.last_claim,
has_credential = credentialOf(a.credentialRef) ~= nil,
}
end
if #list > 0 then
local base = accounts[p.id] or {}
local merged = {}
for k, v in pairs(base) do merged[k] = v end
merged.accounts = list
merged.count = #list
accounts[p.id] = merged
end
end
return { status = 200, json = {
accounts = plugin.state.accounts,
accounts = accounts,
log = plugin.state.log,
kernel_ready = r and r.ready or false,
} }
@ -13126,6 +13178,17 @@ function plugin.__list_sources_probe()
return { json = { sources = out } }
end
-- plugin.__platforms_probe:只给判据用 —— 原样返回 `platforms` 路由的数据。
--
-- 为什么需要它:`list_platforms` 是 plugin.http 的路由处理器,**不在**
-- plugin.exports 里(exports 是给 WebUI 直接调用的白名单),判据无法直接
-- Call 到它。走探针是仓库既有做法(见 channels_remove_source_perm_test.go
-- 对 `status` 的同类处理),避免为了测试把只读状态处理器塞进 exports。
function plugin.__platforms_probe(args)
local r = plugin.list_platforms(nil) or {}
return { json = r.json or {} }
end
-- plugin.__trae_headers_probe:只给判据用 —— 返回 trae 两套头各自的键集合。
--
-- 存在理由:2026-10-20 事故是「目录刷新用了签到头(traeCheckinHeaders)而不是