@ -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 " }
@ -3780,6 +3780,23 @@ local function effectiveModels(provider)
-- 进源只会让客户端选中后报错( trae 的 custom_model_* 槽位就是这类)。
local unavailable = { }
for _ , m in ipairs ( plat.models_unavailable or { } ) do unavailable [ m ] = true end
-- ★ admit_functions: 只放行**本适配器真的会发**的通道里的模型。
--
-- 白名单( parse 里的 rankOf) 只说明"该通道在本端点下可调用",不等于
-- "适配器会发它"。trae 适配器恒发 solo_work_lite, 别的通道的模型经我们
-- 这条路发出去要么回空内容、要么被上游悄悄回落到默认模型(实测
-- kimi-k2.8-preview / deepseek-V3 空回, sagitta 自称 Claude、aquila 自称
-- GPT) —— 那正是用户说的"模型不正确"。
--
-- 放在这里而不是解析器里,是因为解析器还要负责**元数据合并**:同一模型在
-- solo_agent 里有思考档位、在 solo_work_lite 里没有,合并必须跨通道进行
-- (否则模型显示"未提供推理等级")。策略只决定"哪些 id 对外露出"。
local admit = nil
local mrAdmit = plat.models_remote and plat.models_remote . admit_functions
if type ( mrAdmit ) == " table " then
admit = { }
for _ , f in ipairs ( mrAdmit ) do admit [ f ] = true end
end
local remote = plugin.state . remoteModels and plugin.state . remoteModels [ provider ]
local out , seen = { } , { }
-- 远端在前(权威顺序)
@ -3804,9 +3821,24 @@ local function effectiveModels(provider)
-- ⚠️ 只剔这里记录过的(真正 404 过的),**绝不**拿远端目录反推下架 ——
-- buddy/qoder 那类平台的远端目录残缺,反推会删掉可用模型。
local dead = deadModelIds ( provider )
local metaAll = plugin.state . modelMeta and plugin.state . modelMeta [ provider ] or { }
local kept = { }
for _ , id in ipairs ( out ) do
if not dead [ id ] and not unavailable [ id ] then kept [ # kept + 1 ] = id end
local keep = ( not dead [ id ] ) and ( not unavailable [ id ] )
if keep and admit then
local e = metaAll [ id ]
-- 有目录元数据 ⇒ 按它声明的通道判;没有元数据(兜底/手工源)⇒ 不拦,
-- 因为"目录没说"不等于"不可调用"。
if type ( e ) == " table " and type ( e.chans ) == " table " then
keep = false
for _ , c in ipairs ( e.chans ) do
if admit [ c ] then keep = true break end
end
elseif type ( e ) == " table " and type ( e.chan ) == " string " and e.chan ~= " " then
keep = admit [ e.chan ] == true
end
end
if keep then kept [ # kept + 1 ] = id end
end
return kept
end
@ -4103,22 +4135,20 @@ local function parseRemoteModels(kind, body, provider)
local groups = rec.function_configs or rec.functionConfigs
if type ( groups ) == " table " then
local byId , chosenRank , order = { } , { } , { }
-- chansOf[id]:该 id 在**哪些通道**里出现过(合并只留一条元数据,但
-- "在哪些通道可发"是策略判定要用的信息 —— admit_functions 不能只看
-- 合并后那条的 chan: 同模型常在有档位的 solo_agent 与我们要发的
-- solo_work_lite 同时出现,合并会选前者,导致误判"不可发")。
local chansOf = { }
for _ , g in ipairs ( groups ) do
if type ( g ) == " table " then
local chan = rstr ( g , " function " , " Function " )
-- 准入:未声明 function 或不在白名单 → 整组丢弃
-- ★ 追加一道「本适配器可发通道」闸门(声明式,见 models_remote.admit_functions) :
-- 白名单只说明"这个 通道在本适配器的推理端点下可调用",不代表
-- ** 适配器会发它** —— trae 适配器恒发 solo_work_lite, 别的通道 的
-- 模型即使可调用,经我们这条路发出去也会空回/回落默认模型。
local admitted = nil
do
local pl = platformById ( provider )
local mr2 = pl and pl.models_remote
local af = mr2 and mr2.admit_functions
if type ( af ) == " table " then admitted = { } for _ , f in ipairs ( af ) do admitted [ f ] = true end end
end
if admitted and chan and not admitted [ chan ] then chan = nil end
--
-- ⚠️ 「白名单」只回答"该 通道在本适配器的推理端点下** 可调用**";
-- " 适配器会不会发这条通道"是另一回事,由 effectiveModels 的
-- models_remote.admit_functions 过滤(那是产品策略,不该写进解析器:
-- 解析器的职责是把目录如实解出来,含元数据合并)。
if chan and rankOf [ chan ] then
local list = g.config_info_list or g.ConfigInfoList
if type ( list ) == " table " then
@ -4134,6 +4164,8 @@ local function parseRemoteModels(kind, body, provider)
-- is_custom_model: 仅可见但不可调用(上游流内 4001 拒绝)
if e.custom == true then ok = false end
if ok then
chansOf [ e.id ] = chansOf [ e.id ] or { }
chansOf [ e.id ] [ chan ] = true
local inc = byId [ e.id ]
if inc == nil then
byId [ e.id ] = e
@ -4169,7 +4201,14 @@ local function parseRemoteModels(kind, body, provider)
-- 上层只能显示"未提供推理等级")。
if not plugin.state . modelMeta then plugin.state . modelMeta = { } end
plugin.state . modelMeta [ provider ] = plugin.state . modelMeta [ provider ] or { }
plugin.state . modelMeta [ provider ] [ id ] = byId [ id ]
local entry = byId [ id ]
if type ( entry ) == " table " and chansOf [ id ] then
local list = { }
for c in pairs ( chansOf [ id ] ) do list [ # list + 1 ] = c end
table.sort ( list )
entry.chans = list
end
plugin.state . modelMeta [ provider ] [ id ] = entry
end
end
@ -9083,14 +9122,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 +9496,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 +13217,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) 而不是