Repository navigation
[Bug] 渠道账号「测试」恒失败:Loomy 缺探测分支、TRAE 只测目录首个不可调用模型、客户端读错字段导致原因被吞 #681
Description
版本:2.7.2(dsh-our-free-model 2.7.2,DSH Desktop 0.2.1-rc.2,Windows 11,node v24.19.0)
现象
在插件「渠道」页点某个账号的**「测试」**按钮,两个渠道表现不同但都失败:
| 渠道 | 表现 |
|---|---|
| Loomy(讯飞) | 测试失败: 后面空白(原因是 Unsupported probe adapter: loomy,被客户端吞掉) |
| TRAE(字节) | 明明是通的,却报 无法确认:... —— 因为只测了目录第一个模型 doubao-for-auto,上游回 {"code":3003,"message":"model service is unavailable..."} |
两个账号本身的凭据、额度、上游线路都是好的(下节有实测)。
根因一:probeWithAdapter 没有 loomy 分支
vendor/channel-pack/pack.js 的 probeWithAdapter() 里是一条 else-if 链:
if (lobsteraiProduct !== void 0) { ... } // lobsterai
else if (qoderProduct !== void 0) { ... } // qoder / qodercn
else if (clineProduct !== void 0) { ... } // cline
else if (zcodeProduct !== void 0) { ... } // zcode
else if (entry.provider === TRAE_PROVIDER_ID) { ... } // trae
else if (entry.provider === GEMINI.id) { ... } // gemini
else { throw new Error(`Unsupported probe adapter: ${entry.provider}`); }没有 loomy(该段内 loomy 出现 0 次),而 loomy 在 REF_PREFIX_TO_PROVIDER 里是受支持的 provider、LoomyAdapter 类也在(loomy: pruned(LOOMY.id, loomyAdapter) 也注册进了 modelAdapters)。
于是 Loomy 账号点「测试」必然走到 else 抛 Unsupported probe adapter: loomy。
注:
LoomyAdapter的构造只依赖product、fetchImpl、resolveCredential;fetchRemoteModels/accountPool在探测场景下都可缺省(loadModels()会退回product.fallbackModels,providerCatalogVisible在accountPool === undefined时直接返回 true)。所以补一条分支即可,不需要额外依赖。
根因二:account.test 只测目录里的第一个模型
case "account.test" 里:
let modelId = pickTestModel(entry, [], req.modelId); // 前端不传 modelId → ""
if (modelId === "") {
const catalog = await fullCatalogIds(ctx, modelAdapters, entry.provider);
...
modelId = pickTestModel(entry, catalog.ids, req.modelId); // → catalog.ids[0]
}
const value = await testAccount(pool, accountId, modelId); // 只测这一个pickTestModel 的兜底就是 catalogIds[0]。
而 TRAE 的远端目录第一个模型是 doubao-for-auto(channel = solo_coder,自动路由占位模型),实测对它发真实请求回:
{"code":3003,"message":"model service is unavailable, please contact the administrator"}用同样的凭据、同样的头、按每个模型自己的 channel 测前 12 个:
| # | 模型 | 结果 |
|---|---|---|
| 0 | doubao-for-auto |
❌ 3003 model service is unavailable |
| 1 | Doubao-Seed-2.0-Code-auto |
✅ |
| 2 | Doubao_1_6 |
❌ |
| 3 | Doubao-Seed-Code |
✅ |
| 4 | Doubao-Seed-2.0-Code |
✅ |
| 5 | search_agent_qwen_fast |
✅ |
| 6 | glm-5.1 |
✅ |
| 7 | glm-5v-turbo |
✅ |
| 8 | glm-5 |
✅ |
| 9 | minimax-m2.1-auto |
✅ |
| 10 | glm-4.7 |
✅ |
| 11 | minimax-m2-auto |
✅ |
前 12 个里 10 个通,只有两个自动路由占位模型不可调用 —— 而插件偏偏只测第 0 个。
这也解释了「有时能过有时不能」:远端目录拉取失败时走 staticFallbackModels(),首个是 DeepSeek-V4-Flash-Official(实测 200 通)。
根因三:客户端读错字段名,原因被吞成空白
client.js:
const value = await rpc('account.test', { accountId: row.id }, 90_000)
setNotice(value?.ok === false
? t('chan.test.failed').replace('{reason}', value?.error ?? '') // ← 读 error
: t('chan.test.ok').replace('{ms}', String(value?.ms ?? value?.latencyMs ?? '—'))) // ← 读 ms但 account.test 成功时返回 { ok: true, value },而 rpc() 在 ok === false 时是直接 throw 的 —— 所以 value?.ok === false 永远不成立,失败分支走不到;真正的原因文字在 value.message 里,字段名不是 error。
另外 ms 服务端从来没返回过,所以即使测试成功,界面显示的是「连接正常(— ms)」。
复现
- 装 2.7.2,登录一个 Loomy 账号(讯飞)→ 渠道页点「测试」→ 失败,原因为空。
- 登录一个 TRAE 账号(字节)→ 点「测试」→ 失败,提示
doubao-for-auto的 3003。 - 同一 TRAE 凭据手工调
POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat(Authorization: Cloud-IDE-JWT <token>+X-Machine-Id+X-Device-Id,body 带"function": "solo_work_lite",content用 parts 数组)并指定glm-5.1→ 200 正常。
本地修复(已在本机验证,供参考)
pack.js 四处、client.js 一处:
- 补 Loomy 分支(在 Gemini 分支前):
} else if (entry.provider === LOOMY.id) { adapter = new LoomyAdapter({ credentialRef: ref, resolveCredential: async () => credential, refresh: async () => {}, product: LOOMY });
account.test改成候选链:显式传modelId就只测它;否则按「已有 429 标记 → 目录顺序」去重后取前PROBE_CANDIDATES_MAX(4)个,逐个测、第一个成功即停,返回里附tried数组。单次尝试超时PROBE_ATTEMPT_TIMEOUT_MS = 2e4(4 × 20s = 80s,留在客户端 90s RPC 超时内)。- 探测返回带耗时:
probeWithAdapter里const probeStartedAt = Date.now();,成功/失败返回都加ms: Date.now() - probeStartedAt;testAccount的返回里透出...outcome.ms === void 0 ? {} : { ms: outcome.ms },。 client.js:value?.error ?? ''→value?.error ?? value?.message ?? ''。
修完本机实测:Loomy「测试」通过、TRAE「测试」通过(候选链跳过 doubao-for-auto 命中第 2 个),文案正常显示耗时。
补充:Loomy 线路本身没问题
同一凭据直接打 https://loomyad.xunfei.cn/api/v1/chat/completions(Accept: text/event-stream + Authorization: Bearer <token> + token: <token>),兜底表 8 个模型全部 HTTP 200:
deepseek-v4-flash-0731 2246ms / MiniMax-M3 2522ms / Kimi-k2.6 614ms / qwen-3.8-max 1392ms / GLM-5.3-Flash 4031ms / qwen3.8-flash 762ms / spark-x 2619ms / mimo-v2.5 3839ms
期望
probeWithAdapter补齐 loomy(以及任何已在REF_PREFIX_TO_PROVIDER/modelAdapters里注册但漏了探测分支的 provider —— 建议加一条「注册表 ↔ 探测分支」一致性测试锁死,和REF_PREFIX_TO_PROVIDER那条用例同款思路)。account.test不要只信catalog.ids[0],至少给个候选链或跳过已知的自动路由占位模型。- 客户端失败分支统一取
value.message,并让服务端真的返回ms。
Activity
unifilitence commentedon Oct 27, 2026
补充:TRAE 那条根因不是「只测目录首个模型」,而是探测用的 TraeAdapter 没接远端目录,导致渠道判定必然回退默认值
实测发现
probeWithAdapter的 TRAE 分支只传了三个 option:没有
fetchRemoteModels。于是TraeAdapter.ensureRemoteModels()第一行就返回:remoteMeta永远是空 Map,channelFor(model)对任何模型都回退this.product.function:product.function="solo_work_lite"。而 TRAE 上游要求config_name必须属于所声明的function渠道,跨渠道一律4001 param is invalid。结论:不是「第一个模型恰好不可用」,而是「任何不属于 solo_work_lite 的模型都必然失败」
原 issue 里写的「TRAE 只测目录首个模型」只描述了一半现象。补上候选链后我发现真正原因:候选链前 4 个模型(
Doubao-Seed-2.1-Turbo/Doubao-Seed-2.1-Pro/Doubao-Seed-Evolving/Doubao-Seed-Code)里,前三个恰好都属于solo_work_lite,第四个Doubao-Seed-Code属于solo_agent,于是只有它失败:确定性 A/B(直接打上游,绕开插件)
同一凭据、同一请求体,只改
function/config_name的组合:glm-5.1solo_work_litecode=4001 param is invalidglm-5.1solo_coderglm-5.3solo_work_liteglm-5.3solo_codercode=4001 param is invalid远端目录里
glm-5.1属于chat_v3/solo_coder/solo_agent/solo_agent_lite,不含solo_work_lite—— 与失败完全吻合。另外:
config_name传空串也能 200(上游自选模型),但那样探测不出指定模型是否可用,所以不建议用空串兜底。建议修法(我本地验证通过的最小改动)
给 TRAE 探测分支补一个探测专用的目录拉取,复用现有的
traeSOLOHeaders+parseTraeBatchModelList:其中 helper 与
TraeAuth.fetchModelsUncached同源,差别只有三点:凭据从参数来(探测不走账号池)、不缓存(一次探测只拉一次)、失败回[]:目录拉取失败时回
[],ensureRemoteModels保持remoteMeta为空、退回默认渠道 —— 与现在的行为一致,不会引入新的失败模式(已实测:断网模拟下仍能用默认渠道完成探测)。顺带一个观察
过滤后的可用目录只有 40 个(原始目录 87 个
config_name),doubao-for-auto/Doubao_1_6已被parseTraeBatchModelList的usage !== "chat_completion"/isEnabled === false/isHidden === true条件剔除,所以它们不会出现在候选链里 —— 我最初怀疑它们,是拿原始目录而不是过滤后目录去测的,这点自我更正一下。