Skip to content

[Bug] 渠道账号「测试」恒失败:Loomy 缺探测分支、TRAE 只测目录首个不可调用模型、客户端读错字段导致原因被吞 #681

Open

Description

@unifilitence

版本: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)」。

复现

  1. 装 2.7.2,登录一个 Loomy 账号(讯飞)→ 渠道页点「测试」→ 失败,原因为空。
  2. 登录一个 TRAE 账号(字节)→ 点「测试」→ 失败,提示 doubao-for-auto 的 3003。
  3. 同一 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 一处:

  1. 补 Loomy 分支(在 Gemini 分支前):
    } else if (entry.provider === LOOMY.id) {
      adapter = new LoomyAdapter({
        credentialRef: ref,
        resolveCredential: async () => credential,
        refresh: async () => {},
        product: LOOMY
      });
  2. account.test 改成候选链:显式传 modelId 就只测它;否则按「已有 429 标记 → 目录顺序」去重后取前 PROBE_CANDIDATES_MAX(4)个,逐个测、第一个成功即停,返回里附 tried 数组。单次尝试超时 PROBE_ATTEMPT_TIMEOUT_MS = 2e4(4 × 20s = 80s,留在客户端 90s RPC 超时内)。
  3. 探测返回带耗时:probeWithAdapter 里 const probeStartedAt = Date.now();,成功/失败返回都加 ms: Date.now() - probeStartedAt;testAccount 的返回里透出 ...outcome.ms === void 0 ? {} : { ms: outcome.ms },。
  4. 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

期望

  1. probeWithAdapter 补齐 loomy(以及任何已在 REF_PREFIX_TO_PROVIDER / modelAdapters 里注册但漏了探测分支的 provider —— 建议加一条「注册表 ↔ 探测分支」一致性测试锁死,和 REF_PREFIX_TO_PROVIDER 那条用例同款思路)。
  2. account.test 不要只信 catalog.ids[0],至少给个候选链或跳过已知的自动路由占位模型。
  3. 客户端失败分支统一取 value.message,并让服务端真的返回 ms。

Activity

  1. unifilitence commented on Oct 27, 2026

    @unifilitence
    Author

    补充:TRAE 那条根因不是「只测目录首个模型」,而是探测用的 TraeAdapter 没接远端目录,导致渠道判定必然回退默认值

    实测发现

    probeWithAdapter 的 TRAE 分支只传了三个 option:

    adapter = new TraeAdapter({
      credentialRef: ref,
      resolveCredential: async () => credential,
      refresh: async () => {}
    });

    没有 fetchRemoteModels。于是 TraeAdapter.ensureRemoteModels() 第一行就返回:

    async ensureRemoteModels() {
      const fetchRemote = this.options.fetchRemoteModels;
      if (this.remoteModels !== void 0 || fetchRemote === void 0) return;   // ← 这里
      ...
    }

    remoteMeta 永远是空 Map,channelFor(model) 对任何模型都回退 this.product.function:

    channelFor(model) {
      const channel = this.remoteMeta.get(model)?.function;
      return channel !== void 0 && channel.length > 0 ? channel : 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,于是只有它失败:

    补丁前(不接远端目录 → channelFor 恒 solo_work_lite):
      PASS  Doubao-Seed-2.1-Turbo          1904 ms
      PASS  Doubao-Seed-2.1-Pro            2790 ms
      PASS  Doubao-Seed-Evolving           2986 ms
      FAIL  Doubao-Seed-Code                188 ms   trae: ... (code=4001)
    
    补丁后(probeWithAdapter 接远端目录):
      PASS  Doubao-Seed-2.1-Turbo          2289 ms
      PASS  Doubao-Seed-2.1-Pro            2269 ms
      PASS  Doubao-Seed-Evolving           2160 ms
      PASS  Doubao-Seed-Code               2004 ms
    

    确定性 A/B(直接打上游,绕开插件)

    同一凭据、同一请求体,只改 function / config_name 的组合:

    config_name function 结果
    glm-5.1 solo_work_lite code=4001 param is invalid
    glm-5.1 solo_coder 200,回答「收到」
    glm-5.3 solo_work_lite 200
    glm-5.3 solo_coder code=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:

    adapter = new TraeAdapter({
      credentialRef: ref,
      resolveCredential: async () => credential,
      refresh: async () => {},
      fetchRemoteModels: () => fetchTraeModelsForProbe(credential)   // ← 补这一行
    });

    其中 helper 与 TraeAuth.fetchModelsUncached 同源,差别只有三点:凭据从参数来(探测不走账号池)、不缓存(一次探测只拉一次)、失败回 []:

    async function fetchTraeModelsForProbe(credential) {
      if (!credential || typeof credential.access_token !== "string" || credential.access_token.length === 0) return [];
      try {
        const response = await fetch(`${TRAE.agentHost}${TRAE_BATCH_MODELS_PATH}`, {
          method: "POST",
          headers: traeSOLOHeaders(credential, TRAE, false),
          body: JSON.stringify({
            functions: TRAE.channels,
            agent_type: "",
            current_config_info: { config_name: "", is_custom_model: false },
            mode_type: 0, access_type: 0,
            ab_force_vids: "", ab_autotest_advanced_mode: 0, show_custom_model: true
          }),
          signal: AbortSignal.timeout(TRAE_REQUEST_TIMEOUT_MS)
        });
        if (!response.ok) return [];
        return parseTraeBatchModelList(await response.json());
      } catch {
        return [];
      }
    }

    目录拉取失败时回 [],ensureRemoteModels 保持 remoteMeta 为空、退回默认渠道 —— 与现在的行为一致,不会引入新的失败模式(已实测:断网模拟下仍能用默认渠道完成探测)。

    顺带一个观察

    过滤后的可用目录只有 40 个(原始目录 87 个 config_name),doubao-for-auto / Doubao_1_6 已被 parseTraeBatchModelList 的 usage !== "chat_completion" / isEnabled === false / isHidden === true 条件剔除,所以它们不会出现在候选链里 —— 我最初怀疑它们,是拿原始目录而不是过滤后目录去测的,这点自我更正一下。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions