@luchenglong/cli-bridge 0.2.9 → 0.2.11

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 CHANGED
@@ -9,6 +9,79 @@ 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.11] - 2026-10-20
13
+
14
+ > 用户反馈:「claude 我是用 cc-switch 改的配置,你现在获取的模型不对」。查证后不是列表写错,
15
+ > 而是**整条链路**有两处判断错误 —— 本版修复。
16
+
17
+ ### Fixed
18
+ - **claude 直连 cc-switch 的 proxy 端点必失败**(这是根本原因)。cc-switch 的 **proxy 模式**
19
+ 把 Claude 指向第三方网关(本机实测 `https://opencode.ai/zen/go`),该网关要求自家 session 头
20
+ (缺则 `400 MissingSessionID`),而 claude CLI 不会发这个头。同一配置实测对照:
21
+ - 直连 → `exit=1, API Error: 400 MissingSessionID`
22
+ - 改指 cc-switch 本地代理 `http://127.0.0.1:15721` → `exit=0, "OK"`
23
+ 现在桥接用 `--settings '{"env":{"ANTHROPIC_BASE_URL":"<proxy>"}}'` **只覆盖 base_url**,
24
+ **绝不改写** `~/.claude/settings.json`(那是 cc-switch 的领地,改它下次切换即被打回)。
25
+ 新增设置项:`endpointMode`(`auto` 探活代理 / `proxy` 强制,探不到**明确报错** / `direct`
26
+ 强制直连)、`proxyUrl`(默认 `127.0.0.1:15721`)。面板会显示当前实际走法。
27
+ - **模型清单漏掉了用户实际在用的模型**。原先只认「无效模型触发的服务端 400 报文」,而实测:
28
+ - 服务端只回**基础名** `deepseek-flash, deepseek-v4-pro`;
29
+ - 用户配置的 `deepseek-v4-pro[1M]` / `deepseek-v4-flash` 逐个实测**都 200 可用**;
30
+ - 且走本地代理时网关对任何模型名都放行(连探测用的假模型名都回显),**永远不产生 400 报文**
31
+ —— 只靠探测时这份清单等于空。
32
+ 现在改为**配置来源 ∪ 服务端报文**,各带 `source` 标注(`configured` 在前、`account-live` 补充),
33
+ 面板标签会写明「来自哪个 ANTHROPIC_*_MODEL」或「[服务端清单]」。
34
+ **红线不变**:来源必须如实标注,配置来源绝不冒充服务端权威。
35
+
36
+ ### Changed
37
+ - `providers/claude-endpoint.mjs`(新片):端点解析与代理探活(`node -e` 做 HTTP GET,
38
+ 结果按 URL 缓存 30s,因为 buildArgv/meta 都是同步回调,Node 无同步 HTTP)。
39
+ - 测试改为**密闭**:claude 的模型测试与 MCP 测试此前会读本机真实 `~/.claude/settings.json`
40
+ (实测期望 2 个却出现 5 个),现统一注入 `CLAUDE_CONFIG_DIR` 指向临时目录;
41
+ buildArgv 断言注入 `probeProxy`,与本机代理是否在跑解耦。
42
+
43
+ ## [0.2.10] - 2026-10-20
44
+
45
+ > ⚠️ **0.2.9 的 dynamic 沙箱形态在 opencode / mimo 两家是坏的**,本版修复。若你在用这两家的
46
+ > dynamic 形态,请升级。
47
+
48
+ ### Fixed
49
+ - **`dynamic/opencode.host.js` 与 `dynamic/mimo.host.js` 运行即 `ReferenceError`**(0.2.9 已发布)。
50
+ 根因:这两家的代码被拆进片文件(`opencode-exe.mjs` / `mimo-pure.mjs`)后,它们的 dynamic
51
+ **模板里漏了片文件占位标记**,build 只在模板声明占位时才内联片文件 —— 于是生成物里
52
+ `candidateExePaths` / `parseMimoJson` 只有引用、没有定义。实测证据:修前 `apply()` 直接抛
53
+ `ReferenceError: candidateExePaths is not defined`;修后两家 `apply()` 正常。
54
+ 已补占位并重建,生成物内符号定义数 0 → 1。
55
+ - **新增永久防线**:`tests/{opencode,mimo}/dynamic-template.test.mjs` 断言生成物里片文件符号
56
+ **"定义数 = 1"**(而不是"出现过")—— 纯求值检查抓不到这类缺陷,因为未定义标识符只在函数
57
+ 被调用时才炸。
58
+ - **EOL 与 `.gitattributes` 不一致**:`providers/codebuddy*.mjs` 等工作区曾被写成 CRLF
59
+ (PowerShell 默认行为),而仓库要求 `eol=lf`,导致生成物按 CRLF 源拼出、别的机器 clone 后
60
+ `build --check` 必然 drift(`dynamic/codebuddy.host.js` 甚至被 git 当二进制)。
61
+ 已规范化全部源码为 LF 并重建;`git ls-files --eol` 复核全部 `w/lf`。
62
+
63
+ ### Added
64
+ - **模型下拉改为「手动拉取」(契约 §3.2)** —— 用户反馈 codebuddy 各产品面都自动拿不到可用
65
+ 清单,希望加刷新按钮、每次手动拉取:
66
+ - `settings.meta()` **不再自动探测**(面板同步回调,探测会拖慢首屏;且探不到时下拉空着,
67
+ 用户分不清"没模型"与"没拉到")。现在只读缓存,字段声明 `refresh: { label }` 即渲染按钮。
68
+ - 新增可选契约 `settings.refreshModels(settings, env) → { ok, ids, labels?, source, error? }`,
69
+ 已由 5 家实现(codebuddy / opencode / mimo / claude / codex);agy 无模型字段故不适用。
70
+ - 新端点 `POST /cli-bridge/settings/models`(body `{provider}`):**业务失败恒返回 HTTP 200**
71
+ + `{ok:false, error}`,不用 4xx/5xx —— 否则前端容易把"没拉到"当网络故障静默吞掉。
72
+ - `client.js` 渲染「拉取可用模型」按钮 + 进行中态 + 结果/错误文本;拿到 options 只更新展示态,
73
+ 用户选中仍走原有 POST 保存。
74
+ - 各家的 `source` 如实标注(`account-live` / `live` / `opencode-models` / `mimo-models`)。
75
+
76
+ ### 说明(各团队实测发现并修掉的真缺陷)
77
+ - claude:异步探测在 spawn 失败时 `error` 与 `close` 各推进一次候选游标,会**跳过后面能跑的
78
+ 候选**(用户填错 binPath 就再也走不到有效 exe);已加 per-attempt 守卫 + 回归钉子。
79
+ - codex:live `GET /models` 无超时,端点卡住会让面板一直转圈;已加 AbortController 上界(20s)。
80
+ - codebuddy/claude:`meta()` 与 `refreshModels` 的"产品面/缓存键"口径必须同源,否则会出现
81
+ 「刷新成功但下拉仍空」。已统一取值来源并各加用例。
82
+ - codebuddy:探测进程的环境曾硬编码 `process.env`,不尊重调用者传入的 env,导致测试无法隔离
83
+ (会回落到本机真实 CLI 并"假装成功")。现传入 env 可覆盖。
84
+
12
85
  ## [0.2.9] - 2026-10-20
13
86
 
14
87
  ### Added
@@ -134,13 +134,46 @@ meta(env) → {
134
134
  allowCustom?: boolean, // select 允许自定义输入(defaultModel 就是它)
135
135
  placeholder?: string,
136
136
  note?: string, // 字段下方说明
137
- secret?: boolean // 凭据字段:必须同时列入 secretKeys
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
@@ -254,12 +287,30 @@ export function check(env) → true | throw // --che
254
287
  - **成功判定陷阱**:`is_error=true` 时退出码可能是 0 也可能是 1,且 `subtype` 仍可能是
255
288
  `"success"`(无效模型实测就是这个形状)。判定必须 `is_error === false && exitCode === 0`。
256
289
  - 续聊:`--resume <session_id>` / `-c|--continue` / `--fork-session`。
257
- - 模型清单:**唯一权威来源 = 无效模型触发的服务端 400 报文**
258
- `"The supported API model names are X, Y, ..."`。`~/.claude/settings.json` 的 env 块里
259
- `ANTHROPIC_DEFAULT_*_MODEL` / `ANTHROPIC_MODEL` 是**用户配置的别名映射,不是服务端可用
260
- 全集**(实测两者不一致),只能作诊断线索。
290
+ - **模型清单 = 配置来源 ∪ 服务端报文**(0.2.11 起,两者各带 `source` 标注)。踩过的两个坑:
291
+ ① **只信服务端 400 报文会给出「比用户配的还少」的清单** —— 实测 DeepSeek 端点只回
292
+ `deepseek-flash, deepseek-v4-pro` 两个基础名,而用户配置的 `deepseek-v4-pro[1M]` /
293
+ `deepseek-v4-flash` 逐个实测都 200(带上下文后缀/命名变体,报文不列)。
294
+ ② **走本地代理时探测会失效**:某些网关对任何模型名都放行(甚至回显探测用的假名),
295
+ 永远不产生 400 报文 —— 此时若只靠探测就等于没有清单。
296
+ 故 `~/.claude/settings.json` 的 `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_*_MODEL` 是**合法
297
+ 清单来源之一**(`source: 'configured'`,排在前),服务端报文作补充(`source: 'account-live'`)。
298
+ **红线不变**:来源必须如实标注,配置来源绝不冒充服务端权威。
299
+ - **端点必须能走 cc-switch 本地代理**(0.2.11 起)。用户用 cc-switch 的 **proxy 模式** 把
300
+ Claude 指向第三方网关(例 `https://opencode.ai/zen/go`),该网关要求自家 session 头
301
+ (缺则 `400 MissingSessionID`),而 **claude CLI 不会发这个头 → 直连必然失败**。
302
+ 实测对照:同一配置直连 → 400;改指 `http://127.0.0.1:15721`(cc-switch 本地代理)→ 200。
303
+ 做法:`--settings '{"env":{"ANTHROPIC_BASE_URL":"<proxy>"}}'` 只覆盖 base_url,
304
+ **绝不改写** `~/.claude/settings.json`(那是 cc-switch 的领地,改它下次切换即被打回)。
305
+ 设置项 `endpointMode`:`auto`(探活代理,活着就用)/ `proxy`(强制,探不到则**明确报错**)/
306
+ `direct`(强制直连)。代理地址 `proxyUrl` 默认 `http://127.0.0.1:15721`。
307
+ 探活靠一次性 `node -e` 子进程做 HTTP GET(Node 无同步 HTTP),结果按 URL 缓存 30s。
261
308
  - 权限:`-p` 无 TTY,任何需要交互批准的动作都会挂到超时,故 auto 模式映射为
262
309
  `--dangerously-skip-permissions`(或 plan 态 `--permission-mode plan`)。
310
+ - **Claude Desktop 不是可执行后端**:它是 GUI、无 `-p` 式非交互入口(本机实测其配置目录为空 =
311
+ 未安装);cc-switch 对它的 `/claude-desktop/v1/messages` 还需一个内部 gateway token
312
+ (db 里 `claude_desktop_gateway_token` 实测校验不过)。故它只能作为**配置来源**被读取,
313
+ 不能作为 provider 的第二个"执行面"。
263
314
 
264
315
  ### codex(OpenAI Codex CLI,实测 0.98.0)
265
316
  - 非交互:`codex exec [opts] <prompt>`;`--json` 出事件流,不加就是纯文本。
package/docs/HANDOVER.md CHANGED
@@ -2,30 +2,32 @@
2
2
 
3
3
  > 面向接手这个仓库/这台机器的下一个人(或下一个 Agent)。
4
4
  > 目标:**不读完所有历史也能安全改动、发布、排障**。历史事故与红线集中在 §5 与 §10。
5
- > 最近一次更新:2026-10-16(0.2.4:**第四个 provider OpenCode** 接入 + 状态灯淡出修复 +
6
- > 版本单一来源;运行时改为 **npm registry 标准安装**,不再依赖本地开发目录)。
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` 把**本机四个编码 CLI**(agy / CodeBuddy / MiMo / **OpenCode**)以同一套
13
- provider 描述符接入 DSH,并有四种落地形态(preset / 动态宿主 / 家级宿主插件+指示灯 / MCP stdio server)。
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)。OpenCode 是统一包首个**新增**
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.4**(三处版本锁:`package.json` / `mcp/cli-bridge-mcp.mjs` 的 `SERVER_VERSION` / `CHANGELOG.md` 顶部;`verify.mjs` 机器比对) |
23
- | npm | `@luchenglong/cli-bridge@0.2.4`(`latest`;shasum `08ca3325376e96abc935435784106fd1ee158ba4`,68 files / 376.0 kB) |
24
- | GitHub | `new-256/cli-bridge`,`main` @ `acf6f09`;tag `v0.2.1` / `v0.2.2` / `v0.2.3` / `v0.2.4` |
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
- | 测试 | **327 pass / 0 fail**(33 个 `*.test.mjs`),四闸门 `npm run check` 全绿 |
28
- | 本机运行时 | DSH Desktop;`dsh-home/profiles/web` 以 **npm registry 标准依赖** `@luchenglong/cli-bridge: "0.2.4"` + pnpm 快照部署(2026-10-15 起不再用 `file:`) |
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` 全绿(327 测试)
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` 返回**四家** + `scheduling`
383
- - [ ] 设置面板有 `总设置 | AGY | CB | MIMO | OPENCODE` 五个标签;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 收敛的坑)