From 47fe759025cf211e0146d8660f2d4eff4e055999 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 16 Oct 2026 14:06:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(channels):=20buddy=20=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E4=B8=8D=E5=8F=AF=E8=B0=83=E7=94=A8=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=EF=BC=88=E7=85=A7=E5=8F=82=E8=80=83=20isChatModel=20?= =?UTF-8?q?=E5=9B=9B=E7=B1=BB=E5=88=A4=E6=8D=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 真实缺陷 `/v3/config` 的 `data.models` 混着**不是对话模型**的条目,我们原样全收 ⇒ 生产源里 24 个模型,其中 `nes-gf` / `codewise-completions` / `codewise-rewrite` / `codewise-jump` / `codewise-default-model-v2` / `hunyuan-image-alpha` / `auto` 全部调不通(`glm-5.1` 也 404)。用户选中报错,而错误文案指向**模型** ⇒ 极易被误判成"这个模型坏了"。 参考实现(buddy.ts `isChatModel` / `isAutoSelectAlias`)逐条给出四类判据, 且都有实测依据;本轮逐条移植: ① 自动选择别名 `auto` / `default` ⚠️ **只按这两个字面量**过滤,不做前缀匹配 —— 前缀会误伤 `default-model` / `fast-model`(国际版抽象别名,但被 craft agent 引用, 是官方推荐入口)。参考注释专门警告过这点。 ② 补全/NES 专用:前缀 `nes-` / `completion-` / `codewise-`,或 `supportsExtra` (参考实测 codewise-default-model-v2 → `code 11102 service info not found`) ③ `maxOutputTokens > 0 且 ≤ 256`(补全用途;对话模型普遍 ≥24000) ④ 带 `text-to-image` 标签(如 hunyuan-image-alpha) ⇒ 故 `models` 先建成 id→meta 映射(③④ 需要元数据),再逐条判定。 同时收 `data.agents[].models` 里**被 agent 引用**的清单并合并 —— 参考据此给 `agentReferenced` 打标(`hy4-preview-f` 只由本端点下发且被 agent 引用, 白名单式重建会把它丢掉)。 ## 判据(1 条 + 变异 1/1) `TestBuddyCatalogFilterDropsNonChatModels`:断言四类规则**都在**源码里 ("有人精简掉一条"就会把不可调用模型放回选择器),并**反向**断言 `default` 没被写成前缀匹配(那会误伤 `default-model`)。 变异:删掉 `nes-` 规则 → 报红。 ## 说明 这是上一提交(v2.0.30)同一轮的第二个发现:v2.0.30 修掉 URL 拼接后, buddy 通道**才真正可达**,于是"可达之后目录里哪些真能调"这件事第一次暴露出来。 两个缺陷是串联的 —— 不先修 URL,这个目录问题无法被观察到。 --- .../channels_buddy_catalog_filter_test.go | 57 +++++++++++++ internal/lua/plugins/channels.lua | 83 ++++++++++++++++--- 2 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 internal/core/channels_buddy_catalog_filter_test.go diff --git a/internal/core/channels_buddy_catalog_filter_test.go b/internal/core/channels_buddy_catalog_filter_test.go new file mode 100644 index 0000000..b8bc5c7 --- /dev/null +++ b/internal/core/channels_buddy_catalog_filter_test.go @@ -0,0 +1,57 @@ +package core + +import ( + "strings" + "testing" +) + +// ★ 远端模型目录混着「不可调用」的条目,必须过滤掉。 +// +// 真实缺陷(生产实测):buddy 源声明了 24 个模型,其中 +// +// nes-gf / codewise-completions / codewise-rewrite / codewise-jump / +// codewise-default-model-v2 / hunyuan-image-alpha / auto +// +// 全部调不通(`glm-5.1` 也是 404)。用户选中就报错,而错误文案指向**模型**, +// 极易被误判成"这个模型坏了"。 +// +// 参考实现(buddy.ts 的 isChatModel / isAutoSelectAlias)记录了**四类**判据, +// 且都是实测依据: +// +// ① 自动选择别名 auto/default(**只按字面量**,前缀匹配会误伤 default-model) +// ② 补全/NES 专用前缀 nes- / completion- / codewise-,或 supportsExtra=true +// (参考实测 codewise-default-model-v2 → `code 11102 service info not found`) +// ③ maxOutputTokens >0 且 ≤256(补全用途;对话模型普遍 ≥24000) +// ④ 带 text-to-image 标签(如 hunyuan-image-alpha) +func TestBuddyCatalogFilterDropsNonChatModels(t *testing.T) { + src, err := readBundledChannels(t) + if err != nil { + t.Fatal(err) + } + s := string(src) + + // 判据一:四类规则必须都在源码里(防止有人"精简"掉其中一条) + for _, rule := range []string{ + `id == "auto" or id == "default"`, // ① + `id:sub(1, 4) == "nes-"`, // ② + `id:sub(1, 11) == "completion-"`, // ② + `id:sub(1, 9) == "codewise-"`, // ② + `m.supportsExtra == true`, // ② + `mo > 0 and mo <= 256`, // ③ + `t == "text-to-image"`, // ④ + } { + if !strings.Contains(s, rule) { + t.Errorf("buddy 目录过滤缺少规则:%s\n"+ + "(这四类都是参考用真实请求验证过的「列出来就会让用户报错」的条目,"+ + "删任何一条都会把不可调用模型放回选择器)", rule) + } + } + + // 判据二:`default-model` / `fast-model` 必须**不在**被过滤的形态里。 + // 参考注释明确警告:只过滤 auto/default 两个字面量,不做前缀匹配 —— + // 前缀会误伤这两个国际版别名(它们被 craft agent 引用,是官方推荐入口)。 + if strings.Contains(s, `id:sub(1, 7) == "default"`) { + t.Error("buddy 过滤把 default 做成了**前缀**匹配 ⇒ 会误伤 default-model(参考点名警告)") + } + t.Log("✓ buddy 目录过滤四类规则齐备,且未误伤 default-model 类别名") +} diff --git a/internal/lua/plugins/channels.lua b/internal/lua/plugins/channels.lua index d990bd8..09831da 100644 --- a/internal/lua/plugins/channels.lua +++ b/internal/lua/plugins/channels.lua @@ -3216,17 +3216,80 @@ local function parseRemoteModels(kind, body, provider) end end - elseif kind == "buddy_config" then - -- 照参考 parseModelsFromConfig:结果在 data.models / data.agents 结构里。 - -- 多形态兜底(顶层 data / 直接 models),因为该端点历史上变过形状。 - local d = rec.data or rec - local arr = d.models or d.agents or rec.models - if type(arr) == "table" then - for _, m in ipairs(arr) do - local id = type(m) == "table" and (m.id or m.modelId or m.name) - if type(id) == "string" and id ~= "" then out[#out+1] = id end + elseif kind == "buddy_config" then + -- 照参考 parseModelsFromConfig:结果在 data.models / data.agents 结构里。 + -- 多形态兜底(顶层 data / 直接 models),因为该端点历史上变过形状。 + -- + -- ★★ 必须过滤「不可调用」的条目 —— 远端 data.models 里混着三类**不是对话 + -- 模型**的东西,列进模型选择器只会让用户选中后报错(实测生产:glm-5.1 + -- 与 glm-5.2 之外,codewise-* / nes-gf / hunyuan-image-alpha 同样调不通)。 + -- 判据与命名逐条照参考 buddy.ts 的 isChatModel / isAutoSelectAlias: + -- ① 自动选择类内部别名:`auto` / `default` + -- ⚠️ 只按**这两个字面量**过滤,不做前缀匹配 —— 前缀会误伤 + -- `default-model` / `fast-model`(国际版抽象别名,但被 craft agent + -- 引用,是官方推荐入口)。 + -- ② 补全/NES 专用:前缀 `nes-` / `completion-` / `codewise-`, + -- 或带 `supportsExtra` 标记。参考实测 codewise-default-model-v2 + -- 返回 `code 11102 model service info not found`(后端未开放)。 + -- ③ 输出上限过小(>0 且 ≤256):补全用途,对话模型普遍 ≥24000。 + -- ④ 带 `text-to-image` 标签的生成式模型(如 hunyuan-image-alpha)。 + -- ⚠️ ③④ 需要元数据,故先把 models 建成 id→meta 映射。 + local d = rec.data or rec + local arr = d.models or d.agents or rec.models + local metaById = {} + if type(arr) == "table" then + for _, m in ipairs(arr) do + if type(m) == "table" and type(m.id) == "string" and m.id ~= "" then + metaById[m.id] = m + end + end end - end + -- 被 agent 引用的模型(craft/ask/plan/cli)。参考据此给 `agentReferenced` + -- 打标:`hy4-preview-f` 只由本端点下发且被 agent 引用,白名单重建会丢掉它。 + local agentRef = {} + if type(d.agents) == "table" then + for _, ag in ipairs(d.agents) do + if type(ag) == "table" and type(ag.models) == "table" then + for _, mid in ipairs(ag.models) do + if type(mid) == "string" then agentRef[mid] = true end + end + end + end + end + local function buddyIsChatModel(id) + if id == "auto" or id == "default" then return false end + if id:sub(1, 4) == "nes-" or id:sub(1, 11) == "completion-" + or id:sub(1, 9) == "codewise-" then return false end + local m = metaById[id] + if type(m) == "table" then + if m.supportsExtra == true then return false end + local mo = tonumber(m.maxOutputTokens) + if mo and mo > 0 and mo <= 256 then return false end + if type(m.tags) == "table" then + for _, t in ipairs(m.tags) do + if t == "text-to-image" then return false end + end + end + end + return true + end + local seen = {} + local function pushId(id) + if type(id) ~= "string" or id == "" then return end + if seen[id] or not buddyIsChatModel(id) then return end + seen[id] = true + out[#out + 1] = id + end + -- models 与 agents 两处都收(agents 里是被引用的清单,可能含远端 + -- data.models 没有的新变体)。models 优先,保持顺序权威。 + if type(arr) == "table" then + for _, m in ipairs(arr) do + local id = type(m) == "table" and (m.id or m.modelId or m.name) or nil + if type(id) == "string" then pushId(id) end + end + end + for id in pairs(agentRef) do pushId(id) end + elseif kind == "codearts_builtin" then -- 照参考 fetchCodeArtsRemoteModels 的第 2 个端点:字段是 builtinModels