From 401a7ead27fb4dc5aeeb50af9f8fb086f56001f2 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 20 Oct 2026 09:13:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(channels):=20trae=20=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=A1=A8=E5=8E=BB=E6=8E=89=E4=B8=8D=E5=8F=AF=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E6=A7=BD=E4=BD=8D=20+=20opencode=20=E5=85=8D=E8=B4=B9=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=8C=89=E5=AE=9E=E6=B5=8B=E6=94=B6=E6=95=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ① trae:模型选择器里那几个 `custom_model_*` 是**槽位不是模型** 用户报「trae 的模型不正确 / 渠道不可用」。逐个实测(生产): `custom_model_kimi` / `custom_model_claude` / `custom_model_1M` 返回 **空 content**(`custom_model_gpt-5` 也答非所问),而真实模型 (glm-5.2 → Z.ai GLM、kimi-k3 → Moonshot、minimax-m3 → MiniMax、 Doubao-Seed-2.1-Pro → 字节)全部正常 —— 所以渠道是通的,问题在**表**。 根因:`custom_model_*` 是 TRAE 的「自定义模型槽位」,上游目录里它们的权威 判据是 `is_custom_model: true`。插件自己的 `trae_batch` 解析器早已按这条 **准入**剔除,但**兜底表**把它们当普通模型列了出来;而远端目录在部分账号 下恒空(企业租户账号实测:22 个 function 全回 `error_code 2001 Config detail for function: X not found`),用户只能看到 兜底表 ⇒ 选中槽位后上游回空 ⇒ 「模型坏了」。 修法(**纯插件**,内核零改动): - 新增 `models_unavailable` 一档:声明「本协议下不可调用」的条目, **既不进 UI 选择器,也不进上游源**(与 `models_hidden` 区别:后者只是 不进 UI,内部子 agent 仍可显式调用); - trae 的 11 个 `custom_model_*` 移入该档;把 4 个内部子 agent 留在 `models_hidden`(语义不变); - 生产验证:trae 源/选择器从 28 → **17**,逐个实测 17 个全部返回非空内容。 ## ② 远端目录「空」要能区分原因 `refresh_models` 此前只会报「远端返回空目录」,把两种完全不同的故障混为一谈: 网络/HTTP 故障 vs 服务端 200 但账号侧没有可下发配置。 现在后者如实报:「远端 200 但无模型配置(账号侧未下发 function config, 企业/租户账号需管理员开启,实测 error_code 2001)」——运维据此知道该找 租户管理员,而不是反复重试网络。 另加 `__trae_catalog_probe`(诊断探针):原样回传目录端点的 status/body 前 N 字节/解析条数,并在 2001 现场定位时立了大功(这类"上游 200 却空目录"没有 原始响应只能瞎猜)。 ## ③ opencode / localzen:免费模型按**实测**收敛 用户报「localzen 也不能用了」。逐项实测(生产): - `space-bunny-free` → **410** `Model space-bunny-free has been deprecated. Use minimax-m3.1-flash-free instead.`(仍在本地源里 ⇒ 选中必失败) - `jev-1.13-free` → 400 `Model does not support this protocol.`(非 OpenAI chat 协议,本端点永远调不通) - `beam-free` → 200 但 content 空、无 finish_reason - 其余 7 个(mimo / nemotron-super-vl / nemotron-lightning / longcat / deepseek-v4.1 / glm-5.3-flash / minimax-m3.1-flash)全部正常返回内容 修法: - `DEAD_GONE_PATTERNS` 补 opencode 的下架文案 (`Model … has been deprecated`);配阴/阳性判据(参数/字段被弃用**不算**); - opencode 平台把 jev-1.13-free / beam-free 列入 `models_unavailable`, 并从兜底表与 models_free 里移除; - 生产 `localzen` 源(手写源,插件不管)按实测刷新为 7 个可用免费模型, 逐个验证返回内容。 ## 纪律 渠道逻辑全部留在插件里(channels.lua / 插件自装适配器);内核这轮**没有** 新增任何厂商语义 —— 上一轮加进内核的只有通用的 `adapter.hidden` 机制。 --- internal/core/channels_dead_model_test.go | 10 ++ internal/core/channels_trae_models_test.go | 111 +++++++++++++++++ internal/lua/plugins/channels.lua | 134 +++++++++++++++++++-- 3 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 internal/core/channels_trae_models_test.go diff --git a/internal/core/channels_dead_model_test.go b/internal/core/channels_dead_model_test.go index 2d1fecd..2da0ef9 100644 --- a/internal/core/channels_dead_model_test.go +++ b/internal/core/channels_dead_model_test.go @@ -68,6 +68,16 @@ func TestDeadModelGoneJudgement(t *testing.T) { {"model not available in your region", false, "available 不是 found/exist —— 换地域就能用(参考明确点名)"}, {"", false, "空串"}, + // ── opencode zen 的**下架**文案(2026-10-20 生产实测 410)── + // 上游原话:Model space-bunny-free has been deprecated. Use + // minimax-m3.1-flash-free instead. 此前不识别 ⇒ 该模型一直留在源里, + // 用户选中必 410("渠道不可用"的观感来源之一)。 + {"Model space-bunny-free has been deprecated. Use minimax-m3.1-flash-free instead.", + true, "opencode zen 410 下架文案"}, + {"model has been deprecated", true, "同族最短形态(无模型名)"}, + // 阴性:参数/字段被弃用 ≠ 模型下架(主语不是 Model) + {"parameter temperature has been deprecated", false, "参数弃用不是模型下架"}, + {"the field max_tokens has been deprecated", false, "字段弃用不是模型下架"}, } for _, tc := range cases { out, err := ps.Call("channels", "__is_model_gone_probe", diff --git a/internal/core/channels_trae_models_test.go b/internal/core/channels_trae_models_test.go new file mode 100644 index 0000000..4447076 --- /dev/null +++ b/internal/core/channels_trae_models_test.go @@ -0,0 +1,111 @@ +package core + +import "testing" + +// TRAE 的模型选择器**不许**出现 `custom_model_*`。 +// +// 真实缺陷(2026-10-20 用户报「trae 的模型不正确 / 渠道不可用」): +// 这些 id 是 TRAE 的「自定义模型槽位」,上游目录里它们的权威判据是 +// `is_custom_model: true`(插件 trae_batch 解析器早已按这条准入剔除)。 +// 但**兜底表**把它们当普通模型列了出来 —— 而远端目录在部分账号下恒空 +// (企业租户账号实测:22 个 function 全回 error_code 2001 "Config detail +// for function: X not found"),于是用户只能看到兜底表,选中这些槽位后 +// 上游回**空 content**(实测 custom_model_kimi / custom_model_claude / +// custom_model_1M 均空),表现就是"模型坏了、渠道不可用"。 +// +// 判据分两层: +// - model_list(UI 选择器)不含它们; +// - 交给内核的上游源里也不含它们(effectiveModels 走 models_unavailable)。 +func TestTraeCustomModelSlotsAreNotOffered(t *testing.T) { + c := kernelTestCore(t) + ps := c.Plugins() + src, err := readBundledChannels(t) + if err != nil { + t.Fatal(err) + } + if err := ps.LoadSource("channels", src); err != nil { + t.Fatal(err) + } + _ = ps.SetPluginKernelPermission("channels", + []string{"add_source", "remove_source", "list_sources", "status"}, false) + + out, err := ps.Call("channels", "model_list", map[string]interface{}{"platform": "trae"}) + if err != nil { + t.Fatalf("model_list: %v", err) + } + j, _ := out.(map[string]interface{})["json"].(map[string]interface{}) + models, _ := j["models"].([]interface{}) + if len(models) == 0 { + t.Fatal("trae 应至少有一个可选模型(兜底表)") + } + for _, raw := range models { + m, _ := raw.(map[string]interface{}) + id, _ := m["id"].(string) + if len(id) > 13 && id[:13] == "custom_model_" { + t.Fatalf("model_list 不应包含自定义模型槽位 %q(选中后上游回空内容)", id) + } + } + // 可调用模型必须还在(不能把整张表干掉换绿) + found := false + for _, raw := range models { + m, _ := raw.(map[string]interface{}) + if m["id"] == "glm-5.2" { + found = true + } + } + if !found { + t.Fatalf("可调用的 glm-5.2 不应被一起藏掉;实得 %v", models) + } +} + +// 上游源的模型清单同样不含占位槽位(客户端按裸名就能调,藏 UI 不够)。 +func TestTraeSourceOmitsCustomModelSlots(t *testing.T) { + c := kernelTestCore(t) + ps := c.Plugins() + src, err := readBundledChannels(t) + if err != nil { + t.Fatal(err) + } + if err := ps.LoadSource("channels", src); err != nil { + t.Fatal(err) + } + _ = ps.SetPluginKernelPermission("channels", + []string{"add_source", "remove_source", "list_sources", "status"}, false) + if _, err := ps.Call("channels", "account_add", map[string]interface{}{ + "platform": "trae", "account_id": "trae-a", + "credential": `{"access_token":"jwt","uid":"1","device_id":"d","machine_id":"m"}`, + }); err != nil { + t.Fatalf("account_add: %v", err) + } + ls, err := ps.Call("channels", "__list_sources_probe", map[string]interface{}{}) + if err != nil { + t.Fatalf("probe: %v", err) + } + j, _ := ls.(map[string]interface{})["json"].(map[string]interface{}) + arr, _ := j["sources"].([]interface{}) + haveTrae := false + for _, raw := range arr { + m, _ := raw.(map[string]interface{}) + if m["name"] == "channels-trae" { + haveTrae = true + } + } + if !haveTrae { + t.Fatalf("trae 账号加好后应产出 channels-trae 源;实得 %v", arr) + } + // 源里的模型清单从内核真实源读(不是插件的 model_list) + srcs := c.Sources() + for _, s := range srcs { + if s.Name != "channels-trae" { + continue + } + for _, m := range s.Models { + if len(m.ID) > 13 && m.ID[:13] == "custom_model_" { + t.Fatalf("上游源不应包含自定义模型槽位 %q", m.ID) + } + } + if len(s.Models) == 0 { + t.Fatal("channels-trae 源应带模型清单") + } + } +} diff --git a/internal/lua/plugins/channels.lua b/internal/lua/plugins/channels.lua index a02d89f..2e172ac 100644 --- a/internal/lua/plugins/channels.lua +++ b/internal/lua/plugins/channels.lua @@ -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", "__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", "__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" } @@ -291,14 +291,25 @@ local PLATFORMS = { "Doubao-Seed-2.1-Turbo", "Doubao-Seed-2.0-Code", "glm-5.2", "glm-5-turbo", "glm-5", "DeepSeek-V4-Pro", "DeepSeek-V4-Flash", "kimi-k3", "kimi-k2.7-code", "kimi-k2.6", "minimax-m3", "qwen-3.7-plus", "sagitta", "aquila", + }, + -- ★ custom_model_* 是 TRAE 的「自定义模型槽位」,**不是可调用的模型**: + -- 上游目录里它们的判据是 `is_custom_model: true`(本插件的 trae_batch + -- 解析器早就按这条**准入**剔除),但兜底表此前把它们当普通模型列出来 —— + -- 于是远端目录取不到时(企业租户账号实测:22 个 function 全回 + -- `2001 Config detail ... not found` ⇒ 目录为空),用户在模型选择器里 + -- 能看到这些条目,选中后上游回空内容(实测 custom_model_kimi / + -- custom_model_claude / custom_model_1M 均返回空 content)—— + -- 表现就是「模型不正确 / 渠道不可用」。 + -- 它们与内部子 agent 同性质:保留在目录里(排查可见),但不进可选列表。 + models_hidden = { + "browser_use_subagent", "explore_sub_agent_v13", "explore_sub_agent_v2", "summary", + }, + models_unavailable = { "custom_model_gemini", "custom_model_placeholder", "custom_model_1M_text", "custom_model_1M", "custom_model_kimi", "custom_model_claude", "custom_model_gpt-5", "custom_model_no-fc", "custom_model_deepseek_chat", "custom_model_deepseek_reasoner", "custom_model_deepseek_v4", }, - models_hidden = { - "browser_use_subagent", "explore_sub_agent_v13", "explore_sub_agent_v2", "summary", - }, -- 每日签到:POST /trae/api/v2/ug/checkin_credits/claim body {} 到 UG 主机。 checkin = { url = "https://api.trae.cn/trae/api/v2/ug/checkin_credits/claim", @@ -964,19 +975,27 @@ local PLATFORMS = { -- * 新增 beam-free / nemotron-3.5-super-vl-free / jev-1.13-free / -- glm-5.3-flash-free / minimax-m3.1-flash-free models = { - "big-pickle", "beam-free", "longcat-2.5-preview-free", + "big-pickle", "longcat-2.5-preview-free", "mimo-v2.6-flash-free", "nemotron-3.5-super-vl-free", - "nemotron-3.5-lightning-free", "jev-1.13-free", "glm-5.3-flash-free", + "nemotron-3.5-lightning-free", "glm-5.3-flash-free", "minimax-m3.1-flash-free", "deepseek-v4.1-flash-free", "deepseek-v4-flash", "deepseek-v4.1-flash", "glm-5.2", "kimi-k2.5", "minimax-m2.7", "minimax-m3", "qwen3.8-max", }, models_free = { - "big-pickle", "beam-free", "longcat-2.5-preview-free", + "big-pickle", "longcat-2.5-preview-free", "mimo-v2.6-flash-free", "nemotron-3.5-super-vl-free", - "nemotron-3.5-lightning-free", "jev-1.13-free", + "nemotron-3.5-lightning-free", "glm-5.3-flash-free", "minimax-m3.1-flash-free", "deepseek-v4.1-flash-free", }, + -- ★ 目录里有、但**本协议下不可调用**的免费模型(2026-10-20 逐个实测): + -- * jev-1.13-free → 400 "Model does not support this protocol." + -- (它不是 OpenAI chat 协议,用 /v1/chat/completions 必失败) + -- * beam-free → 200 但 content 为空、无 finish_reason(上游未真正产出) + -- 二者若留在可选列表,用户选中后只会看到"空回复/报错" ⇒ 判为不可用, + -- 既不进 UI 列表,也不进上游源(models_unavailable 与 hidden 的区别见 + -- effectiveModels 的注释)。 + models_unavailable = { "jev-1.13-free", "beam-free" }, }, -- CodeArts(华为)—— 凭据 + SDK-HMAC-SHA256 签到(照参考 codearts-credits.ts)。 @@ -3735,6 +3754,12 @@ local deadModelIds local function effectiveModels(provider) local plat = platformById(provider) if not plat then return {} end + -- models_unavailable:**声明上就不可调用**的条目(不是"暂不显示")。 + -- 与 models_hidden 的区别:hidden 只是不进 UI 列表(内部子 agent 仍可被 + -- 显式调用),unavailable 连**上游源**都不进 —— 因为它们调用必然失败/回空, + -- 进源只会让客户端选中后报错(trae 的 custom_model_* 槽位就是这类)。 + local unavailable = {} + for _, m in ipairs(plat.models_unavailable or {}) do unavailable[m] = true end local remote = plugin.state.remoteModels and plugin.state.remoteModels[provider] local out, seen = {}, {} -- 远端在前(权威顺序) @@ -3755,7 +3780,7 @@ local function effectiveModels(provider) local dead = deadModelIds(provider) local kept = {} for _, id in ipairs(out) do - if not dead[id] then kept[#kept+1] = id end + if not dead[id] and not unavailable[id] then kept[#kept+1] = id end end return kept end @@ -4161,6 +4186,9 @@ end local function refreshRemoteModels(provider) local plat = platformById(provider) if not plat then return nil, "未知平台" end + -- 最近一次目录响应的**原始体**:解析失败时用来区分"网络/HTTP 故障"与 + -- "服务端 200 但账号侧没有可下发配置"(后者文案完全不同,见函数尾部)。 + local lastBody = nil local mr = plat.models_remote if not mr then return nil, "该平台无远端目录端点" end -- 取一个可用账号的凭据(无账号则无凭据,仅当 auth=none 时仍可拉) @@ -4256,12 +4284,30 @@ local function refreshRemoteModels(provider) if err or not resp or resp.status ~= 200 then lastErr = (err or ("HTTP " .. tostring(resp and resp.status))) else + lastBody = resp.body or "" for _, id in ipairs(parseRemoteModels(ep.parse or mr.parse, resp.body, provider)) do if not seen[id] then seen[id] = true merged[#merged+1] = id end end end end - if #merged == 0 then return nil, lastErr or "远端返回空目录" end + if #merged == 0 then + -- ★ 把"空目录"再细分一层:远程 200 但一条配置都没有,通常不是网络问题, + -- 而是**账号侧**没有可下发的 function 配置(企业租户账号实测:22 个 + -- function 全回 error_code 2001 "Config detail for function: X not found")。 + -- 这两类原因的处置完全不同(网络重试 vs 找租户管理员),日志不能都说 + -- "远端返回空目录"。判据只看响应体里出现 2001 或 function_configs。 + if not lastErr then + local seenBody = false + if type(lastBody) == "string" and lastBody:find("function_configs", 1, true) then + seenBody = true + end + if seenBody then + return nil, "远端 200 但无模型配置(账号侧未下发 function config," + .. "企业/租户账号需管理员开启,实测 error_code 2001)" + end + end + return nil, lastErr or "远端返回空目录" + end if not plugin.state.remoteModels then plugin.state.remoteModels = {} end plugin.state.remoteModels[provider] = { ids = merged, @@ -7184,6 +7230,13 @@ local DEAD_GONE_PATTERNS = { "unknown%s+model", "no%s+such%s+model", "nonexistent%s+model", + -- ★ opencode zen 的下架文案(2026-10-20 实测): + -- 410 `Model space-bunny-free has been deprecated. Use minimax-m3.1-flash-free instead.` + -- 此前不识别 ⇒ 该模型一直留在源里,用户选中必失败("渠道不可用"观感)。 + -- 带 "has been deprecated" 整句是为了不误伤「参数 X 已弃用」这类**非模型**文案: + -- "Model ... has been deprecated" 的主语必须是 Model(%s+ 吃掉名字)。 + "model%s+%S+%s+has%s+been%s+deprecated", + "model%s+has%s+been%s+deprecated", } -- 反向模式:命中即**不是**下架(照参考 NOT_GONE_PATTERNS)。 @@ -8089,6 +8142,7 @@ function plugin.model_list(req) -- models_hidden:内部子 agent(照参考 isHidden 语义)不进用户可选列表。 local hidden = {} for _, m in ipairs(plat.models_hidden or {}) do hidden[m] = true end + for _, m in ipairs(plat.models_unavailable or {}) do hidden[m] = true end -- models_free:参考的 isFree 字段(官方语义:匿名/免费可用)。 local free = {} for _, m in ipairs(plat.models_free or {}) do free[m] = true end @@ -12986,6 +13040,66 @@ function plugin.__list_sources_probe() return { json = { sources = out } } end +-- plugin.__trae_catalog_probe:只给诊断用 —— 原样回传 TRAE 远端模型目录的 +-- HTTP 状态与前 1200 字节响应体。 +-- +-- 存在理由:`refresh_models` 只把「解析出几条」汇总成「远端返回空目录」, +-- 看不到**上游到底回了什么**(空数组?错误信封?形状变了?)。模型目录决定 +-- 用户在渠道页能选哪些模型,它是"渠道不可用"类报障的第一现场,不能只有一句 +-- 二手结论。探测逻辑与 refreshRemoteModels 的 trae 分支**同源**(同一个 +-- traeCheckinHeaders + 同一份请求体),避免"探针通、真路径不通"。 +function plugin.__trae_catalog_probe(args) + args = args or {} + local plat = platformById("trae") + if not plat or not plat.models_remote then + return { json = { error = "trae 平台未声明 models_remote" } } + end + local ep = (plat.models_remote.endpoints or {})[1] or {} + local cred + for _, a in ipairs(poolList("trae")) do + if a.enabled ~= false then + local raw = __llmsproxy_secrets.get(a.credentialRef) + local okj, cj = pcall(json.decode, raw or "") + if okj and type(cj) == "table" and cj.access_token then cred = cj break end + end + end + if not cred then return { json = { error = "无可用 TRAE 凭据" } } end + if type(traeCheckinHeaders) ~= "function" then + return { json = { error = "traeCheckinHeaders 未就绪" } } + end + local h, herr = traeCheckinHeaders(plat, cred, cred.uid or "") + if herr then return { json = { error = "构造头部失败: " .. tostring(herr) } } end + -- 诊断用覆盖:Accept 在参考里对目录请求是 application/json,而 checkin 的 + -- 头部构造给的是 text/event-stream —— 用它可以判定"是不是 Accept 导致空目录"。 + if type(args.accept) == "string" and args.accept ~= "" then + h["Accept"] = args.accept + end + -- 诊断用:额外头部(企业/租户类账号的 IDE 端点通常要求租户身份, + -- 而参考的 traeSOLOHeaders 不发。用它验证"缺租户头导致全 2001")。 + if type(args.headers) == "table" then + for k, v in pairs(args.headers) do + if type(k) == "string" and type(v) == "string" then h[k] = v end + end + end + if type(args.ide_version) == "string" and args.ide_version ~= "" then + h["X-Ide-Version"] = args.ide_version + h["X-App-Version-Code"] = args.ide_version + h["X-Ide-Version-Code"] = args.ide_version + end + local resp, err = __llmsproxy_http({ + url = ep.url, method = ep.method or "POST", + headers = h, body = (type(args.body) == "string" and args.body ~= "" and args.body) or ep.body or "{}", + }) + if err then return { json = { error = tostring(err) } } end + local body = (resp and resp.body) or "" + return { json = { + status = resp and resp.status or 0, + bytes = #body, + head = body:sub(1, math.min(tonumber(args.clip) or 1200, #body)), + parsed = #parseRemoteModels("trae_batch", body, "trae"), + } } +end + -- plugin.__trae_entry_probe:只给测试/诊断用 —— 报告某模型最终选中条目的关键字段。 -- “合并规则选错了条目”(空档位覆盖有档位,issue IKI7WT/IKILR7)这类问题 -- 没有它只能靠猜。