@luchenglong/cli-bridge 0.2.9 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/PROVIDER-CONTRACT.md +34 -1
- package/docs/HANDOVER.md +109 -15
- package/dynamic/claude.host.js +138 -81
- package/dynamic/codebuddy.host.js +1426 -1378
- package/dynamic/codex.host.js +78 -103
- package/dynamic/mimo.host.js +720 -514
- package/dynamic/mimo.host.template.mjs +2 -0
- package/dynamic/opencode.host.js +740 -584
- package/dynamic/opencode.host.template.mjs +2 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +590 -600
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +1 -1
- package/mcp/cli-bridge-mcp.mjs +3 -3
- package/mcp/providers/codebuddy.mjs +427 -427
- package/package.json +3 -3
- package/providers/claude-models.mjs +80 -76
- package/providers/claude.mjs +59 -6
- package/providers/codebuddy-catalog.mjs +367 -599
- package/providers/codebuddy-consts.mjs +72 -72
- package/providers/codebuddy-models-source.mjs +254 -0
- package/providers/codebuddy-models-tool.mjs +106 -106
- package/providers/codebuddy-schemas.mjs +46 -46
- package/providers/codebuddy.mjs +600 -578
- package/providers/codex-cli.mjs +33 -99
- package/providers/codex.mjs +45 -4
- package/providers/mimo-pure.mjs +270 -111
- package/providers/mimo.mjs +453 -517
- package/providers/opencode-exe.mjs +190 -63
- package/providers/opencode.mjs +553 -587
- package/scripts/build.mjs +261 -261
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,48 @@ GitHub Release(`scripts/verify.mjs` 在 CI 与 prepack 双重锁一致)。
|
|
|
9
9
|
> - CodeBuddy 线:[docs/history/codebuddy/CHANGELOG.md](docs/history/codebuddy/CHANGELOG.md)(终版 1.6.2)
|
|
10
10
|
> - MiMo 线:[docs/history/mimo/CHANGELOG.md](docs/history/mimo/CHANGELOG.md)(终版 1.2.9)
|
|
11
11
|
|
|
12
|
+
## [0.2.10] - 2026-10-20
|
|
13
|
+
|
|
14
|
+
> ⚠️ **0.2.9 的 dynamic 沙箱形态在 opencode / mimo 两家是坏的**,本版修复。若你在用这两家的
|
|
15
|
+
> dynamic 形态,请升级。
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **`dynamic/opencode.host.js` 与 `dynamic/mimo.host.js` 运行即 `ReferenceError`**(0.2.9 已发布)。
|
|
19
|
+
根因:这两家的代码被拆进片文件(`opencode-exe.mjs` / `mimo-pure.mjs`)后,它们的 dynamic
|
|
20
|
+
**模板里漏了片文件占位标记**,build 只在模板声明占位时才内联片文件 —— 于是生成物里
|
|
21
|
+
`candidateExePaths` / `parseMimoJson` 只有引用、没有定义。实测证据:修前 `apply()` 直接抛
|
|
22
|
+
`ReferenceError: candidateExePaths is not defined`;修后两家 `apply()` 正常。
|
|
23
|
+
已补占位并重建,生成物内符号定义数 0 → 1。
|
|
24
|
+
- **新增永久防线**:`tests/{opencode,mimo}/dynamic-template.test.mjs` 断言生成物里片文件符号
|
|
25
|
+
**"定义数 = 1"**(而不是"出现过")—— 纯求值检查抓不到这类缺陷,因为未定义标识符只在函数
|
|
26
|
+
被调用时才炸。
|
|
27
|
+
- **EOL 与 `.gitattributes` 不一致**:`providers/codebuddy*.mjs` 等工作区曾被写成 CRLF
|
|
28
|
+
(PowerShell 默认行为),而仓库要求 `eol=lf`,导致生成物按 CRLF 源拼出、别的机器 clone 后
|
|
29
|
+
`build --check` 必然 drift(`dynamic/codebuddy.host.js` 甚至被 git 当二进制)。
|
|
30
|
+
已规范化全部源码为 LF 并重建;`git ls-files --eol` 复核全部 `w/lf`。
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- **模型下拉改为「手动拉取」(契约 §3.2)** —— 用户反馈 codebuddy 各产品面都自动拿不到可用
|
|
34
|
+
清单,希望加刷新按钮、每次手动拉取:
|
|
35
|
+
- `settings.meta()` **不再自动探测**(面板同步回调,探测会拖慢首屏;且探不到时下拉空着,
|
|
36
|
+
用户分不清"没模型"与"没拉到")。现在只读缓存,字段声明 `refresh: { label }` 即渲染按钮。
|
|
37
|
+
- 新增可选契约 `settings.refreshModels(settings, env) → { ok, ids, labels?, source, error? }`,
|
|
38
|
+
已由 5 家实现(codebuddy / opencode / mimo / claude / codex);agy 无模型字段故不适用。
|
|
39
|
+
- 新端点 `POST /cli-bridge/settings/models`(body `{provider}`):**业务失败恒返回 HTTP 200**
|
|
40
|
+
+ `{ok:false, error}`,不用 4xx/5xx —— 否则前端容易把"没拉到"当网络故障静默吞掉。
|
|
41
|
+
- `client.js` 渲染「拉取可用模型」按钮 + 进行中态 + 结果/错误文本;拿到 options 只更新展示态,
|
|
42
|
+
用户选中仍走原有 POST 保存。
|
|
43
|
+
- 各家的 `source` 如实标注(`account-live` / `live` / `opencode-models` / `mimo-models`)。
|
|
44
|
+
|
|
45
|
+
### 说明(各团队实测发现并修掉的真缺陷)
|
|
46
|
+
- claude:异步探测在 spawn 失败时 `error` 与 `close` 各推进一次候选游标,会**跳过后面能跑的
|
|
47
|
+
候选**(用户填错 binPath 就再也走不到有效 exe);已加 per-attempt 守卫 + 回归钉子。
|
|
48
|
+
- codex:live `GET /models` 无超时,端点卡住会让面板一直转圈;已加 AbortController 上界(20s)。
|
|
49
|
+
- codebuddy/claude:`meta()` 与 `refreshModels` 的"产品面/缓存键"口径必须同源,否则会出现
|
|
50
|
+
「刷新成功但下拉仍空」。已统一取值来源并各加用例。
|
|
51
|
+
- codebuddy:探测进程的环境曾硬编码 `process.env`,不尊重调用者传入的 env,导致测试无法隔离
|
|
52
|
+
(会回落到本机真实 CLI 并"假装成功")。现传入 env 可覆盖。
|
|
53
|
+
|
|
12
54
|
## [0.2.9] - 2026-10-20
|
|
13
55
|
|
|
14
56
|
### Added
|
package/PROVIDER-CONTRACT.md
CHANGED
|
@@ -134,13 +134,46 @@ meta(env) → {
|
|
|
134
134
|
allowCustom?: boolean, // select 允许自定义输入(defaultModel 就是它)
|
|
135
135
|
placeholder?: string,
|
|
136
136
|
note?: string, // 字段下方说明
|
|
137
|
-
secret?: boolean
|
|
137
|
+
secret?: boolean, // 凭据字段:必须同时列入 secretKeys
|
|
138
|
+
refresh?: { // 可选:给 select 加「手动拉取」按钮(见 §3.2)
|
|
139
|
+
label?: string // 按钮文案,缺省「拉取可用项」
|
|
140
|
+
}
|
|
138
141
|
} ]
|
|
139
142
|
}
|
|
140
143
|
```
|
|
141
144
|
|
|
142
145
|
diagnose(settings, env) 返回一行文本(卡片底部「诊断」区);无诊断给 ''。
|
|
143
146
|
|
|
147
|
+
### 3.2 手动拉取(`refreshModels`)—— 面板不自动探测
|
|
148
|
+
|
|
149
|
+
**背景**:模型可用列表只能问服务端/端点,而"问"要起子进程、可能联网、可能因为某个
|
|
150
|
+
产品面没登录而失败。早期做法是在 `meta()` 里同步探针(`spawnSync` + 缓存),带来两个问题:
|
|
151
|
+
① 面板首屏可能被拖慢;② 探不到时下拉空着,用户无法区分"没模型"与"没拉到"。
|
|
152
|
+
|
|
153
|
+
**现在**:`meta()` **只读缓存、绝不主动探测**;探测改由用户点按钮显式触发。
|
|
154
|
+
provider 若希望自己的某个 select 字段支持手动拉取,则:
|
|
155
|
+
|
|
156
|
+
1. 在 `meta()` 的该字段上加 `refresh: { label: '拉取可用模型' }`;
|
|
157
|
+
2. 实现 `settings.refreshModels(settings, env)`(**可选**,async):
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
refreshModels(settings, env) → Promise<{
|
|
161
|
+
ok: boolean,
|
|
162
|
+
ids?: string[], // 可用项 id(keystone 转成 options:[{value,label}])
|
|
163
|
+
labels?: object, // 可选:id → 展示文案
|
|
164
|
+
source?: string, // 数据来源标注,面板会显示(如 'account-live' / 'live')
|
|
165
|
+
error?: string // ok:false 时必填且要可行动(告诉用户下一步怎么做)
|
|
166
|
+
}>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**铁律**:探测失败必须 `ok:false` + 可操作错误,**绝不**返回 `{ok:true, ids:[]}`
|
|
170
|
+
或把静态表冒充成权威清单(这是本项目反复踩过的坑,0.2.6~0.2.9 都在修它)。
|
|
171
|
+
|
|
172
|
+
keystone 提供端点 `POST /cli-bridge/settings/models`(body `{ provider: '<id>' }`),
|
|
173
|
+
固定返回 HTTP 200 + `{ ok, provider, options, source, error }` —— 用 `ok` 表达业务失败,
|
|
174
|
+
不用 HTTP 状态码,以免前端把"没拉到"当成网络错误而静默。client.js 渲染按钮、处理 loading
|
|
175
|
+
与错误展示,并把拿到的 options 合并进当前字段(不落盘,纯展示态;用户选中后照常走 POST 保存)。
|
|
176
|
+
|
|
144
177
|
### 3.1 调度区(顶层 `_scheduling`,非 provider 分区 —— 新增 provider 零改动接入)
|
|
145
178
|
|
|
146
179
|
`{DSH_HOME}/cli-bridge-settings.json` 顶层另有 `_scheduling` 区(下划线前缀 = 非 provider
|
package/docs/HANDOVER.md
CHANGED
|
@@ -2,30 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
> 面向接手这个仓库/这台机器的下一个人(或下一个 Agent)。
|
|
4
4
|
> 目标:**不读完所有历史也能安全改动、发布、排障**。历史事故与红线集中在 §5 与 §10。
|
|
5
|
-
> 最近一次更新:2026-10-
|
|
6
|
-
>
|
|
5
|
+
> 最近一次更新:2026-10-20(**0.2.10:模型下拉改为「手动拉取」+ 修掉 0.2.9 里
|
|
6
|
+
> opencode/mimo 的 dynamic 形态 ReferenceError(模板漏片文件占位)+ 源码 EOL 规范化为 LF**)。
|
|
7
|
+
> 此前的 claude/codex 接入见 §14,OpenCode 见 §13。
|
|
7
8
|
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
## 1. 这是什么
|
|
11
12
|
|
|
12
|
-
`@luchenglong/cli-bridge`
|
|
13
|
-
|
|
13
|
+
`@luchenglong/cli-bridge` 把**本机六个编码 CLI**(agy / CodeBuddy / MiMo / OpenCode /
|
|
14
|
+
**Claude Code** / **OpenAI Codex**)以同一套 provider 描述符接入 DSH,并有四种落地形态
|
|
15
|
+
(preset / 动态宿主 / 家级宿主插件+指示灯 / MCP stdio server)。
|
|
14
16
|
它由三个旧包(`agy-first-bridge` / `codebuddy-first-bridge` / `mimo-first-bridge`)合并而来,
|
|
15
|
-
旧包的工具名、预设 id、策略文案**逐字保留**(兼容承诺,见 §5
|
|
16
|
-
provider(无旧包可迁,无兼容包袱),见 §13。
|
|
17
|
+
旧包的工具名、预设 id、策略文案**逐字保留**(兼容承诺,见 §5)。`opencode` / `claude` / `codex`
|
|
18
|
+
是统一包**新增**的 provider(无旧包可迁,无兼容包袱),见 §13 与 §14。
|
|
17
19
|
|
|
18
20
|
## 2. 当前状态快照
|
|
19
21
|
|
|
20
22
|
| 项 | 值 |
|
|
21
23
|
| --- | --- |
|
|
22
|
-
| 版本 | **0.2.
|
|
23
|
-
| npm | `@luchenglong/cli-bridge@0.2.
|
|
24
|
-
| GitHub | `new-256/cli-bridge`,`main`
|
|
24
|
+
| 版本 | **0.2.10**(三处版本锁:`package.json` / `mcp/cli-bridge-mcp.mjs` 的 `SERVER_VERSION` / `CHANGELOG.md` 顶部;`verify.mjs` 机器比对) |
|
|
25
|
+
| npm | `@luchenglong/cli-bridge@0.2.10`(`latest`;shasum 以 `npm view` 为准,100+ files) |
|
|
26
|
+
| GitHub | `new-256/cli-bridge`,`main` 见 `git log`;tag `v0.2.1` … `v0.2.10` |
|
|
25
27
|
| 旧包 | 三个都已在 npm 标 **deprecated**(提示迁移到统一包):`agy-first-bridge@1.7.1`、`codebuddy-first-bridge@1.6.2`、`mimo-first-bridge@1.2.9` |
|
|
26
28
|
| 旧仓 | `new-256/agy-first-bridge`、`new-256/mimo-first-bridge`:README 顶部已加迁移横幅且 **archived(只读)**;`codebuddy-first-bridge` 在 GitHub 上不存在 |
|
|
27
|
-
| 测试 | **
|
|
28
|
-
| 本机运行时 | DSH Desktop;`dsh-home/profiles/web` 以 **npm registry 标准依赖**
|
|
29
|
+
| 测试 | **600 tests / 599 pass / 0 fail / 1 skip**(既有跳过),四闸门 `npm run check` 全绿 |
|
|
30
|
+
| 本机运行时 | DSH Desktop;`dsh-home/profiles/web` 以 **npm registry 标准依赖** + pnpm 快照部署(不用 `file:`)。⚠ 已发布 ≠ 运行时已切,切版本要用户自己做 |
|
|
29
31
|
|
|
30
32
|
**已知遗留 / 未处理项**(均有意为之,非故障):
|
|
31
33
|
- 家级补丁 `dsh-home\cordis.patch.yml` 里四条 MCP 行**仍是注释状态** —— 与旧版本交接文档
|
|
@@ -375,11 +377,103 @@ npm whoami # 应输出 luchenglong
|
|
|
375
377
|
|
|
376
378
|
---
|
|
377
379
|
|
|
380
|
+
## 14. Claude Code / OpenAI Codex provider(0.2.9 新增)
|
|
381
|
+
|
|
382
|
+
两家都是**在真机上探测**后实现的,不要凭直觉改 argv —— 有一条错就会被 CLI 直接拒(`unexpected argument`)。
|
|
383
|
+
完整实测事实见 `PROVIDER-CONTRACT.md` §10,这里只记要点与坑。
|
|
384
|
+
|
|
385
|
+
### 文件与命名
|
|
386
|
+
- `providers/claude.mjs`(508) + `claude-exe.mjs`(112) / `claude-models.mjs`(334) / `claude-policy.mjs`(37)
|
|
387
|
+
- `providers/codex.mjs`(544) + `codex-cli.mjs`(471) / `codex-models.mjs`(107)
|
|
388
|
+
- preset `preset/{claude,codex}-first/`;dynamic `dynamic/{claude,codex}.host.template.mjs`;MCP `mcp/providers/{claude,codex}.mjs`
|
|
389
|
+
- 工具:`claude_run/_continue/_status/_models`、`codex_run/_continue/_status/_models`
|
|
390
|
+
|
|
391
|
+
### 关键差异(与 agy/codebuddy/mimo/opencode 比)
|
|
392
|
+
| 维度 | claude | codex |
|
|
393
|
+
| --- | --- | --- |
|
|
394
|
+
| 非交互 | `-p --output-format stream-json --verbose` | `exec --json` |
|
|
395
|
+
| sessionId | `result.session_id` | `thread.started.thread_id` |
|
|
396
|
+
| 续聊 | `--resume <id>` / `-c` | `exec resume <id>` / `--last` |
|
|
397
|
+
| 成功判定 | **`is_error===false && exit===0`**(subtype 不可靠!) | exit 0 + `turn.completed` + 无顶层 error |
|
|
398
|
+
| 模型清单 | 无效模型触发 400 报文 | `GET {base_url}/models`(live) |
|
|
399
|
+
|
|
400
|
+
### 三个必须知道的坑
|
|
401
|
+
1. **claude 的成功判定不能看 subtype**:无效模型时 `is_error=true` 但 `subtype` 仍是 `"success"`、
|
|
402
|
+
退出码可能是 0 也可能是 1。只看 subtype 或只看 exit code 都会把失败当成功。
|
|
403
|
+
2. **`codex exec` 不吃根命令的旗**:`-a/--ask-for-approval` 只属于 `codex`,`codex exec` 会报
|
|
404
|
+
unexpected argument;审批走 `-c approval_policy="…"`。同理 `exec resume` 没有 `-C/-s/-o/--add-dir`。
|
|
405
|
+
3. **`item.completed` 的 `item.type==="error"` 是告警不是致命错**(`resume --last` 会带一条
|
|
406
|
+
「此模型与原会话不同」)。把它当致命错会让正常续聊全判失败。
|
|
407
|
+
|
|
408
|
+
### 配置/凭据(本机实际布局,都不是官方直连)
|
|
409
|
+
- claude 走了 **DeepSeek 的 Anthropic 兼容端点**:`~/.claude/settings.json` 的 env 块里
|
|
410
|
+
`ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` + `ANTHROPIC_AUTH_TOKEN` +
|
|
411
|
+
`ANTHROPIC_DEFAULT_*_MODEL=deepseek-v4-pro[1M]`。所以实测可用模型是 `deepseek-flash,
|
|
412
|
+
deepseek-v4-pro`,**不是** Claude 官方模型名。
|
|
413
|
+
- codex 走了 **OpenRouter**:`~/.codex/config.toml` 里 `model_provider="custom"`、
|
|
414
|
+
`base_url=https://openrouter.ai/api/v1`、`experimental_bearer_token`。实时 `/models` 返回 469 项。
|
|
415
|
+
- 因此两家的"可用模型"完全取决于用户配的端点,**不能硬编码模型名**;只能实时问端点。
|
|
416
|
+
|
|
417
|
+
### 手动验收脚本(会真联网/真花钱,故不进 `node --test`)
|
|
418
|
+
```
|
|
419
|
+
node tests/e2e/lead-live-smoke.mjs # 走描述符的 buildArgv→真 CLI→parseJson→buildResult 全链路
|
|
420
|
+
node tests/codex/helpers/live-cli-smoke.mjs
|
|
421
|
+
node tests/claude/helpers/verify-real-cli.mjs
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 15. 模型下拉的「手动拉取」与两条新坑(0.2.10)
|
|
427
|
+
|
|
428
|
+
### 15.1 机制(契约 §3.2)
|
|
429
|
+
面板**不再自动探测**模型清单。原因:探测要起子进程/联网,面板是同步回调 —— 拖慢首屏;
|
|
430
|
+
且探不到时下拉空着,用户分不清"这家没模型"与"没拉到"。
|
|
431
|
+
|
|
432
|
+
现在:`meta()` 只读缓存 → 字段声明 `refresh: { label: '拉取可用模型' }` 即渲染按钮 →
|
|
433
|
+
点击 `POST /cli-bridge/settings/models {provider}` → keystone 调该 provider 的
|
|
434
|
+
`settings.refreshModels(settings, env)` → 返回 `{ok, ids, labels?, source, error?}` →
|
|
435
|
+
前端把 `ids` 变成下拉选项(只更新展示态,选中后照常 POST 保存)。
|
|
436
|
+
|
|
437
|
+
**铁律**:失败必须 `ok:false` + 可操作错误,端点恒返回 **HTTP 200**(业务失败不用 4xx,
|
|
438
|
+
否则前端会当网络错误静默吞掉)。`source` 会显示给用户(`account-live` / `live` / …)。
|
|
439
|
+
|
|
440
|
+
已实现:codebuddy / opencode / mimo / claude / codex(agy 无模型字段)。
|
|
441
|
+
各家的探测来源不同,别混:codebuddy=无效模型触发 400 报文;claude=同机制(Anthropic 兼容端点);
|
|
442
|
+
codex=`GET {base_url}/models`;opencode=`opencode models`;mimo=`mimo models`。
|
|
443
|
+
|
|
444
|
+
### 15.2 坑一:dynamic 模板的片文件占位(**0.2.9 真实事故**)
|
|
445
|
+
把 `providers/<id>.mjs` 拆成片文件(`<id>-*.mjs`)后,**必须确认 `dynamic/<id>.host.template.mjs`
|
|
446
|
+
里有片文件占位行**,否则 build 不会内联那些片 —— 生成物里符号只有引用没有定义,运行时
|
|
447
|
+
`ReferenceError`(且**纯语法检查/new Function 求值都抓不到**,因为未定义标识符只在函数被
|
|
448
|
+
调用时才炸)。0.2.9 就是这样让 opencode/mimo 的 dynamic 形态坏掉的。
|
|
449
|
+
|
|
450
|
+
现在有永久防线:`tests/{opencode,mimo,claude,codex}/dynamic-template.test.mjs` 断言生成物里
|
|
451
|
+
片文件符号的**定义数 = 1**(不是"出现过")。加新片文件时请照抄这个断言。
|
|
452
|
+
|
|
453
|
+
排查手法:`node tests/e2e/sandbox-smoke.mjs [id]` —— 求值 + 逐片符号定义数一把过。
|
|
454
|
+
|
|
455
|
+
### 15.3 坑二:EOL 必须 LF(否则别的机器 build --check 必然 drift)
|
|
456
|
+
`.gitattributes` 要求 `eol=lf`,但 **PowerShell 的 `Set-Content` / `[IO.File]::WriteAllLines`
|
|
457
|
+
在 Windows 默认写 CRLF**。一旦源文件是 CRLF,build 按它拼出的生成物也是 CRLF,于是:
|
|
458
|
+
- 本机 `build --check` 可能通过(源和产物一起是 CRLF);
|
|
459
|
+
- 但别人 clone(git 按 eol=lf 检出)+ 跑 `build --check` → **必然 drift**;
|
|
460
|
+
- 严重时 git 会把生成物当二进制(`git ls-files --eol` 显示 `i/-text w/-text`)。
|
|
461
|
+
|
|
462
|
+
用法:`node tests/e2e/normalize-eol.mjs`(干跑)/ `--apply`(规范化)。
|
|
463
|
+
发版前建议:规范化 EOL → `node scripts/build.mjs` → `build --check` 必须 `up to date` →
|
|
464
|
+
`git ls-files --eol providers/ dynamic/` 全部 `w/lf`。
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
378
468
|
### 交接最小检查单
|
|
379
469
|
|
|
380
|
-
- [ ] `npm run check` 全绿(
|
|
470
|
+
- [ ] `npm run check` 全绿(600 测试)
|
|
381
471
|
- [ ] `node mcp/cli-bridge-mcp.mjs --check` 显示 `scheduling.source` 且版本与 package.json 一致
|
|
382
|
-
- [ ] `GET $env:DSH_WEB_URL/cli-bridge/status`
|
|
383
|
-
- [ ] 设置面板有 `总设置 | AGY | CB | MIMO | OPENCODE`
|
|
472
|
+
- [ ] `GET $env:DSH_WEB_URL/cli-bridge/status` 返回**六家** + `scheduling`
|
|
473
|
+
- [ ] 设置面板有 `总设置 | AGY | CB | MIMO | OPENCODE | CLAUDE | CODEX` 七类卡片;
|
|
474
|
+
有「默认模型」的那几张,点「拉取可用模型」能拿到清单(拿不到会显示具体原因)
|
|
384
475
|
- [ ] 起一个 opencode 任务:灯亮 → 完成后 **60 秒内灭**(§6.1)
|
|
385
|
-
- [ ] 知道红线在 §5、生效时机在 §7.2、排障在 §10、OpenCode 在 §13
|
|
476
|
+
- [ ] 知道红线在 §5、生效时机在 §7.2、排障在 §10、OpenCode 在 §13、claude/codex 在 §14、
|
|
477
|
+
手动拉取与两条新踩的坑在 §15
|
|
478
|
+
- [ ] **`PRESET_DIRS` 只在 `providers/registry.mjs` 一处** —— 加 provider 别再在
|
|
479
|
+
`build.mjs` / `preset-declarations.mjs` / `verify.mjs` 里各写一份(0.2.9 收敛的坑)
|
package/dynamic/claude.host.js
CHANGED
|
@@ -585,7 +585,7 @@ function createRunner(o) {
|
|
|
585
585
|
// - 模型清单只认服务端权威探测;拿不到 = ok:false + 可操作错误(绝不静默空数组/配置冒充)。
|
|
586
586
|
// import { createStatusEngine, createRunner, clampInt, isLimited } from '../core/bridge-kit.mjs'
|
|
587
587
|
// import { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, CLAUDE_BIN_ENV_VARS } from './claude-exe.mjs'
|
|
588
|
-
// import { PROBE_MODEL, parseSupportedModels, probeSupportedModels,
|
|
588
|
+
// import { PROBE_MODEL, parseSupportedModels, probeSupportedModels, readCachedModels, readConfiguredModels, listAccountModels, resetModelsCache } from './claude-models.mjs'
|
|
589
589
|
// 单行 re-export(build 内联时整行剥离;多行形态风险见 build.mjs 的 stripModuleSyntax 注释)。
|
|
590
590
|
|
|
591
591
|
function nowIso() { return new Date().toISOString() }
|
|
@@ -593,6 +593,19 @@ function nowIso() { return new Date().toISOString() }
|
|
|
593
593
|
// 权限模式的用户可见取值(auto 由 DSH plan 状态决定;其余直通 CLI)。
|
|
594
594
|
const MODE_ENUM = ['auto', 'plan', 'accept-edits', 'bypass']
|
|
595
595
|
|
|
596
|
+
// 手动拉取(契约 §3.2)的探测时限与失败指引。
|
|
597
|
+
// 失败文案必须「可操作」:面板只会把 error 原样显示给用户,没写下一步等于没帮上忙。
|
|
598
|
+
const REFRESH_TIMEOUT_MS = 30000
|
|
599
|
+
const REFRESH_HINT = '排查顺序:① 确认设置面板的「claude 可执行文件路径」或 CLAUDE_BIN 指向真实 claude(原生 .exe 优先);② 确认 ~/.claude/settings.json 的 env 里 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN 指向可用端点(该面若未登录,先在终端跑一次 claude 完成登录);③ 再点一次「拉取可用模型」。'
|
|
600
|
+
|
|
601
|
+
/** 缓存年龄的短文案(面板 note 用;只描述事实,不假装新鲜)。 */
|
|
602
|
+
function ageText(ms) {
|
|
603
|
+
const min = Math.floor(Math.max(0, Number(ms) || 0) / 60000)
|
|
604
|
+
if (min < 1) return '刚刚拉取'
|
|
605
|
+
if (min < 60) return min + ' 分钟前拉取'
|
|
606
|
+
return Math.floor(min / 60) + ' 小时前拉取'
|
|
607
|
+
}
|
|
608
|
+
|
|
596
609
|
// ── 三工具 schema ─────────────────────────────────────────────────────────────
|
|
597
610
|
const RUN_PARAMS = {
|
|
598
611
|
type: 'object',
|
|
@@ -967,11 +980,17 @@ function defineProvider (deps = {}) {
|
|
|
967
980
|
}
|
|
968
981
|
return out
|
|
969
982
|
},
|
|
983
|
+
/**
|
|
984
|
+
* 设置面板元数据(契约 §3.2):**只读缓存、绝不探测**。
|
|
985
|
+
* 面板是同步回调,而探测要起子进程打网络(3-5s)——放在这里会把首屏拖慢,且探不到时
|
|
986
|
+
* 用户无法区分「没模型」与「没拉到」。故探测改为用户点「拉取可用模型」→
|
|
987
|
+
* settings.refreshModels() 显式触发;成功后写回同一份缓存,这里下次渲染直接命中。
|
|
988
|
+
*/
|
|
970
989
|
meta: (env2) => {
|
|
971
990
|
const e = env2 && typeof env2 === 'object' ? env2 : {}
|
|
972
991
|
const s = currentSettings()
|
|
973
|
-
const
|
|
974
|
-
const options = (
|
|
992
|
+
const cached = readCachedModels(e, { binPath: s.binPath })
|
|
993
|
+
const options = (cached ? cached.ids : []).map((id) => ({ value: id, label: id }))
|
|
975
994
|
const exeCandidates = claudeExeCandidates(e, '')
|
|
976
995
|
return {
|
|
977
996
|
titleSuffix: 'Claude Code',
|
|
@@ -985,10 +1004,12 @@ function defineProvider (deps = {}) {
|
|
|
985
1004
|
{
|
|
986
1005
|
key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
|
|
987
1006
|
options,
|
|
1007
|
+
// 面板据此渲染「手动拉取」按钮(契约 §3.2);meta() 自身永不探测。
|
|
1008
|
+
refresh: { label: '拉取可用模型' },
|
|
988
1009
|
placeholder: '留空 = 由 claude 自身配置(~/.claude/settings.json)决定',
|
|
989
1010
|
note: options.length
|
|
990
|
-
? '共 ' + options.length + '
|
|
991
|
-
: '
|
|
1011
|
+
? '共 ' + options.length + ' 个服务端权威可用模型(' + ageText(cached.ageMs) + ',源:' + cached.mode + ')。点「拉取可用模型」重新获取;调用时传 model 可覆盖本项。'
|
|
1012
|
+
: '还没拉取过(或上次没拉到):点「拉取可用模型」向端点取权威清单;空表示尚未拉取 / 该面未登录。可直接手动输入模型名 —— 配置里的 ANTHROPIC_*_MODEL 只是别名映射,不作为候选展示。'
|
|
992
1013
|
},
|
|
993
1014
|
{
|
|
994
1015
|
key: 'maxBudgetUsd', label: '单次最大花费(美元)', type: 'text',
|
|
@@ -998,6 +1019,38 @@ function defineProvider (deps = {}) {
|
|
|
998
1019
|
]
|
|
999
1020
|
}
|
|
1000
1021
|
},
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* 手动拉取(契约 §3.2):面板「拉取可用模型」按钮 → keystone
|
|
1025
|
+
* POST /cli-bridge/settings/models → 这里。这是本 provider 唯一的面板探测入口。
|
|
1026
|
+
*
|
|
1027
|
+
* 复用 listAccountModels(异步 probeSupportedModels + 配置诊断),不写第二套探测;
|
|
1028
|
+
* 其成功路径已把权威清单写回 modelsCache,随后 meta() 直接命中(无需再探测)。
|
|
1029
|
+
* 铁律:失败 = ok:false + 可操作错误 + 空 ids,绝不返回 {ok:true, ids:[]};
|
|
1030
|
+
* 也绝不把 settings.json 里的别名映射冒充成服务端可用全集。
|
|
1031
|
+
*/
|
|
1032
|
+
async refreshModels(settings, env2) {
|
|
1033
|
+
const e = (env2 && typeof env2 === 'object') ? env2 : env
|
|
1034
|
+
const s = descriptor.settings.normalize(settings)
|
|
1035
|
+
const got = await listAccountModels(e, {
|
|
1036
|
+
binPath: s.binPath,
|
|
1037
|
+
timeoutMs: REFRESH_TIMEOUT_MS,
|
|
1038
|
+
refresh: true
|
|
1039
|
+
})
|
|
1040
|
+
if (!got.ok) {
|
|
1041
|
+
return { ok: false, ids: [], source: 'account-live', error: String(got.error || 'probe failed') + ' ' + REFRESH_HINT }
|
|
1042
|
+
}
|
|
1043
|
+
const ids = (got.models || []).map((m) => m.id).filter(Boolean)
|
|
1044
|
+
if (!ids.length) {
|
|
1045
|
+
// 走到这里说明探测自称成功却没给出任何 id —— 那是 bug,不是「无模型」,
|
|
1046
|
+
// 必须显式失败,绝不能让面板显示一个空清单当成功。
|
|
1047
|
+
return { ok: false, ids: [], source: 'account-live', error: '探测声称成功但未返回任何模型 id(内部不一致)。' + REFRESH_HINT }
|
|
1048
|
+
}
|
|
1049
|
+
// labels 只给「配置里还映射着某个 ANTHROPIC_*_MODEL」的项加注(纯展示,value 仍是 id)。
|
|
1050
|
+
const labels = {}
|
|
1051
|
+
for (const m of got.models) if (m.configuredAs) labels[m.id] = m.id + ' [' + m.configuredAs + ']'
|
|
1052
|
+
return { ok: true, ids, labels, source: 'account-live' }
|
|
1053
|
+
},
|
|
1001
1054
|
diagnose: (settings, env2) => {
|
|
1002
1055
|
const s = settings && typeof settings === 'object' ? settings : {}
|
|
1003
1056
|
const e = env2 && typeof env2 === 'object' ? env2 : {}
|
|
@@ -1200,18 +1253,23 @@ function claudePathHint(env) {
|
|
|
1200
1253
|
// ~/.claude/settings.json 的 env.ANTHROPIC_DEFAULT_*_MODEL 只是「别名映射」,
|
|
1201
1254
|
// 不是服务端可用全集 —— 红线:绝不把它冒充成可用清单,也绝不静默返回空数组。
|
|
1202
1255
|
//
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1256
|
+
// 缓存与「谁探测」(契约 §3.2 手动拉取):
|
|
1257
|
+
// • settings.meta() 是同步回调,且**不再自动探测** —— 它只调 readCachedModels() 读这里的
|
|
1258
|
+
// 缓存(纯读、不 spawn、不联网);没命中就留空,由 note 提示用户点「拉取可用模型」。
|
|
1259
|
+
// • 探测只有一条路:settings.refreshModels() → 异步 probeSupportedModels()(用户显式触发)。
|
|
1260
|
+
// 成功后经 cachedModels() 写回同一个缓存,下次 meta() 直接命中 —— 面板首屏不再被拖慢。
|
|
1261
|
+
// • 5 分钟 TTL 只约束**工具调用**(claude_models 的重复调用);面板展示的是「上次拉取到的
|
|
1262
|
+
// 权威清单」,过期也照旧展示,但 note 会标注拉取时间(宁可显式标旧,也不让下拉莫名清空)。
|
|
1205
1263
|
// import { claudeExeCandidates, claudeProbeCandidates, splitExeSpec, quoteShellArg } from './claude-exe.mjs'
|
|
1206
1264
|
// 故意无效的探测模型名(服务端必然拒绝;保持带前缀,便于日志辨认来源)。
|
|
1207
1265
|
const PROBE_MODEL = '__clb_probe_invalid__'
|
|
1208
1266
|
const MODELS_TTL_MS = 5 * 60 * 1000
|
|
1209
1267
|
|
|
1210
|
-
// 探测结果缓存:key(后端/端点标识) → { at,
|
|
1268
|
+
// 探测结果缓存:key(后端/端点标识) → { at, value }
|
|
1211
1269
|
const modelsCache = new Map()
|
|
1212
1270
|
|
|
1213
1271
|
/** 清空探测缓存(测试与「模型变更后刷新」用)。 */
|
|
1214
|
-
function resetModelsCache() { modelsCache.clear()
|
|
1272
|
+
function resetModelsCache() { modelsCache.clear() }
|
|
1215
1273
|
|
|
1216
1274
|
// 缓存键 = 候选可执行文件集合(同一台机器可能切换 CLAUDE_BIN / 面板路径,键必须跟着变,
|
|
1217
1275
|
// 否则会把「上一个 exe 的探测结果」错配给新 exe)。
|
|
@@ -1220,8 +1278,18 @@ function candidateKey(env, binPath) {
|
|
|
1220
1278
|
return list.length ? list.join('|') : 'claude'
|
|
1221
1279
|
}
|
|
1222
1280
|
|
|
1281
|
+
/** 缓存的键(面板读缓存与工具探测必须用同一把键,否则写进去读不出来)。 */
|
|
1282
|
+
function modelsCacheKey(env, binPath) {
|
|
1283
|
+
return 'claude:' + candidateKey(env, binPath)
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** 缓存里只可能存「成功的权威清单」;失败绝不入缓存(否则会把坏状态固化)。 */
|
|
1287
|
+
function isUsableProbe(v) {
|
|
1288
|
+
return !!(v && v.ok === true && Array.isArray(v.ids) && v.ids.length)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1223
1291
|
/**
|
|
1224
|
-
*
|
|
1292
|
+
* 缓存包装(异步工具路径):TTL 内直接返回上次成功结果;失败不写缓存。
|
|
1225
1293
|
* @param {string} key
|
|
1226
1294
|
* @param {() => Promise<object>|object} load
|
|
1227
1295
|
* @param {number} [nowMs]
|
|
@@ -1231,11 +1299,47 @@ function cachedModels(key, load, nowMs) {
|
|
|
1231
1299
|
const hit = modelsCache.get(key)
|
|
1232
1300
|
if (hit && at - hit.at < MODELS_TTL_MS) return hit.value
|
|
1233
1301
|
return Promise.resolve().then(load).then((v) => {
|
|
1234
|
-
if (
|
|
1302
|
+
if (isUsableProbe(v)) modelsCache.set(key, { at, value: v })
|
|
1235
1303
|
return v
|
|
1236
1304
|
})
|
|
1237
1305
|
}
|
|
1238
1306
|
|
|
1307
|
+
/**
|
|
1308
|
+
* 只读缓存(settings.meta 专用):纯读、不 spawn、不联网。
|
|
1309
|
+
* 与 cachedModels 的区别:这里**不受 TTL 限制** —— 面板展示的是「上次真正拉取到的那份
|
|
1310
|
+
* 服务端清单」,附 ageMs 让 note 如实标注新旧;空/过期与否都不该凭空清空用户看到的下拉。
|
|
1311
|
+
*
|
|
1312
|
+
* 为什么要「精确键未命中 → 取最近一次成功结果」:keystone 渲染面板时只把 env 传给
|
|
1313
|
+
* meta(),**拿不到面板里保存的 binPath**(settingsView 里 createProvider 只带 env),
|
|
1314
|
+
* 而缓存键含候选 exe 集合 → 用户一旦填过 binPath,精确键必然错开,下拉会在拉取成功后
|
|
1315
|
+
* 仍然空着(看起来像按钮没起作用)。这不算降级:清单是 claude 自己 settings.json 里
|
|
1316
|
+
* 端点+凭据裁决的,与走哪个候选 exe 无关(exe 只决定探测能否跑起来),且 note 会带上
|
|
1317
|
+
* 「N 分钟前拉取 / 源 account-live」,用户看到的是真实的探测结果与真实的年龄。
|
|
1318
|
+
* @param {object} env
|
|
1319
|
+
* @param {{binPath?: string, nowMs?: number}} [opts]
|
|
1320
|
+
* @returns {{ids: string[], mode: string, exe: (string|null), at: number, ageMs: number}|null}
|
|
1321
|
+
*/
|
|
1322
|
+
function readCachedModels(env, opts) {
|
|
1323
|
+
const o = opts || {}
|
|
1324
|
+
const at = Number(o.nowMs) || Date.now()
|
|
1325
|
+
const exact = modelsCache.get(modelsCacheKey(env, o.binPath))
|
|
1326
|
+
let hit = isUsableProbe(exact && exact.value) ? exact : null
|
|
1327
|
+
if (!hit) {
|
|
1328
|
+
for (const entry of modelsCache.values()) {
|
|
1329
|
+
if (!isUsableProbe(entry.value)) continue
|
|
1330
|
+
if (!hit || entry.at > hit.at) hit = entry
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (!hit) return null
|
|
1334
|
+
return {
|
|
1335
|
+
ids: hit.value.ids.slice(),
|
|
1336
|
+
mode: hit.value.mode || 'account-live',
|
|
1337
|
+
exe: hit.value.exe || null,
|
|
1338
|
+
at: hit.at,
|
|
1339
|
+
ageMs: Math.max(0, at - hit.at)
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1239
1343
|
/**
|
|
1240
1344
|
* 从 400 报文里解析「服务端支持的模型名」。
|
|
1241
1345
|
* 主格式(实测):`The supported API model names are A, B, but you passed Z.`
|
|
@@ -1356,68 +1460,8 @@ function probeFailureText(reasons, tried) {
|
|
|
1356
1460
|
}
|
|
1357
1461
|
|
|
1358
1462
|
/**
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
* @param {{binPath?: string, timeoutMs?: number}} [opts]
|
|
1362
|
-
* @returns {{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}}
|
|
1363
|
-
*/
|
|
1364
|
-
function probeSupportedModelsSync(env, opts) {
|
|
1365
|
-
const o = opts || {}
|
|
1366
|
-
const child = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:child_process') : null } catch (x) { return null } })()
|
|
1367
|
-
if (!child || typeof child.spawnSync !== 'function') {
|
|
1368
|
-
return { ok: false, ids: [], mode: 'account-live', error: 'no spawnSync capability in this host form' }
|
|
1369
|
-
}
|
|
1370
|
-
const cands = claudeProbeCandidates(env, o.binPath)
|
|
1371
|
-
const probeList = cands.length ? cands : ['claude']
|
|
1372
|
-
const timeout = Math.max(3000, Math.min(Number(o.timeoutMs) || 12000, 30000))
|
|
1373
|
-
const reasons = []
|
|
1374
|
-
const proc = globalThis.process
|
|
1375
|
-
for (const exe of probeList) {
|
|
1376
|
-
const spec = splitExeSpec(exe)
|
|
1377
|
-
const argv = spec.prefixArgs.concat(probeArgv())
|
|
1378
|
-
const cmd = spec.shell ? [spec.cmd].concat(argv).map(quoteShellArg).join(' ') : spec.cmd
|
|
1379
|
-
const r = child.spawnSync(cmd, spec.shell ? [] : argv, {
|
|
1380
|
-
encoding: 'utf8', timeout, windowsHide: true, shell: !!spec.shell,
|
|
1381
|
-
env: Object.assign({}, (proc && proc.env) || {}, {})
|
|
1382
|
-
})
|
|
1383
|
-
if (!r || r.error) { reasons.push(exe + ': ' + String((r && r.error && (r.error.code || r.error.message)) || 'spawn failed')); continue }
|
|
1384
|
-
const got = extractProbeIds(r.stdout, r.stderr)
|
|
1385
|
-
if (got.ids && got.ids.length) return { ok: true, ids: got.ids, mode: 'account-live', exe }
|
|
1386
|
-
const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
1387
|
-
reasons.push(exe + ': exit ' + r.status + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
|
|
1388
|
-
}
|
|
1389
|
-
return { ok: false, ids: [], mode: 'account-live', error: probeFailureText(reasons, probeList.length) }
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// 同步探测缓存(面板每次渲染都会取,避免反复 spawnSync 打网络)。
|
|
1393
|
-
const syncModelsCache = new Map()
|
|
1394
|
-
// 失败结果的反向缓存 TTL:成功缓存 5 分钟;失败只缓存 60 秒。
|
|
1395
|
-
// 为什么必须有反向缓存:面板是同步回调,CLI 缺失/未登录时一次 spawnSync 可能阻塞数秒,
|
|
1396
|
-
// 每次渲染都重探会让设置面板卡顿。60 秒足够让用户装好 CLI/登录后立刻看到新结果,
|
|
1397
|
-
// 又不会把「当时读不到」长期固化成假象(状态照旧 ok:false + 可操作错误,语义不变)。
|
|
1398
|
-
const NEGATIVE_TTL_MS = 60 * 1000
|
|
1399
|
-
|
|
1400
|
-
/**
|
|
1401
|
-
* 带缓存的同步探测(settings.meta 专用;面板是同步回调,只能 spawnSync)。
|
|
1402
|
-
* 成功/失败都进缓存(TTL 不同),失败语义保持不变:ok:false + 可操作错误。
|
|
1403
|
-
* @returns {{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}}
|
|
1404
|
-
*/
|
|
1405
|
-
function probeSupportedModelsSyncCached(env, opts) {
|
|
1406
|
-
const o = opts || {}
|
|
1407
|
-
const key = 'sync:claude:' + candidateKey(env, o.binPath)
|
|
1408
|
-
const at = Date.now()
|
|
1409
|
-
const hit = syncModelsCache.get(key)
|
|
1410
|
-
if (hit) {
|
|
1411
|
-
const ttl = hit.value && hit.value.ok ? MODELS_TTL_MS : NEGATIVE_TTL_MS
|
|
1412
|
-
if (at - hit.at < ttl) return hit.value
|
|
1413
|
-
}
|
|
1414
|
-
const v = probeSupportedModelsSync(env, o)
|
|
1415
|
-
syncModelsCache.set(key, { at, value: v })
|
|
1416
|
-
return v
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
/**
|
|
1420
|
-
* 异步探测(MCP / preset / dynamic 三形态共用):起一次真实 CLI 调用,解析 400 报文。
|
|
1463
|
+
* 异步探测(唯一探测实现:MCP / preset / dynamic 三形态 + 面板「拉取可用模型」共用):
|
|
1464
|
+
* 起一次真实 CLI 调用,解析 400 报文。
|
|
1421
1465
|
* 铁律:只认探测结果;失败 = ok:false + 可操作错误,绝不回落到配置文件/静态表。
|
|
1422
1466
|
* @param {object} env
|
|
1423
1467
|
* @param {{binPath?: string, timeoutMs?: number}} [opts]
|
|
@@ -1454,20 +1498,33 @@ function probeSupportedModels(env, opts) {
|
|
|
1454
1498
|
let out = ''
|
|
1455
1499
|
let err = ''
|
|
1456
1500
|
const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(Number(o.timeoutMs) || 30000, 90000)))
|
|
1501
|
+
// 一个候选只允许结算一次:spawn 失败时 'error' 与 'close' 会先后都触发,
|
|
1502
|
+
// 两个回调各调一次 attempt() 会把后面的候选整个跳过(idx 多走一格)——
|
|
1503
|
+
// 实测后果是「binPath 填错 → 明明 CLAUDE_BIN 里那个能跑,探测却直接失败」。
|
|
1504
|
+
let settled = false
|
|
1505
|
+
const nextCandidate = (reason) => {
|
|
1506
|
+
if (settled) return
|
|
1507
|
+
settled = true
|
|
1508
|
+
clearTimeout(timer)
|
|
1509
|
+
if (reason) reasons.push(reason)
|
|
1510
|
+
attempt()
|
|
1511
|
+
}
|
|
1457
1512
|
c.stdout.on('data', (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) })
|
|
1458
1513
|
c.stderr.on('data', (d) => { err += d; if (err.length > 200000) err = err.slice(-100000) })
|
|
1459
1514
|
c.on('error', (e) => {
|
|
1460
|
-
|
|
1461
|
-
reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
|
|
1462
|
-
attempt()
|
|
1515
|
+
nextCandidate(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
|
|
1463
1516
|
})
|
|
1464
1517
|
c.on('close', (code) => {
|
|
1465
|
-
|
|
1518
|
+
if (settled) return
|
|
1466
1519
|
const got = extractProbeIds(out, err)
|
|
1467
|
-
if (got.ids && got.ids.length) {
|
|
1520
|
+
if (got.ids && got.ids.length) {
|
|
1521
|
+
settled = true
|
|
1522
|
+
clearTimeout(timer)
|
|
1523
|
+
resolve({ ok: true, ids: got.ids, mode: 'account-live', exe })
|
|
1524
|
+
return
|
|
1525
|
+
}
|
|
1468
1526
|
const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
1469
|
-
|
|
1470
|
-
attempt()
|
|
1527
|
+
nextCandidate(exe + ': exit ' + code + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
|
|
1471
1528
|
})
|
|
1472
1529
|
}
|
|
1473
1530
|
attempt()
|
|
@@ -1483,7 +1540,7 @@ function probeSupportedModels(env, opts) {
|
|
|
1483
1540
|
*/
|
|
1484
1541
|
async function listAccountModels(env, opts) {
|
|
1485
1542
|
const o = opts || {}
|
|
1486
|
-
const key =
|
|
1543
|
+
const key = modelsCacheKey(env, o.binPath)
|
|
1487
1544
|
if (o.refresh) modelsCache.delete(key)
|
|
1488
1545
|
const probe = await cachedModels(key, () => probeSupportedModels(env, o))
|
|
1489
1546
|
const cfg = readConfiguredModels(env)
|