@luchenglong/cli-bridge 0.0.0-stage → 0.2.3

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/MCP-POLICY.md +53 -0
  4. package/PROVIDER-CONTRACT.md +235 -0
  5. package/README.en.md +98 -0
  6. package/README.md +151 -2
  7. package/assets/indicator-states.svg +62 -0
  8. package/bin/agy-quota.mjs +223 -0
  9. package/core/bridge-kit.mjs +549 -0
  10. package/core/scheduling.mjs +149 -0
  11. package/docs/ARCHITECTURE.md +117 -0
  12. package/docs/FALLBACK-AND-INDICATOR.md +93 -0
  13. package/docs/HANDOVER.md +226 -0
  14. package/docs/INSTALL.md +118 -0
  15. package/docs/MCP-POLICY.zh.md +49 -0
  16. package/docs/MIGRATION.md +43 -0
  17. package/docs/RELEASE-SOP.md +80 -0
  18. package/docs/history/agy/CHANGELOG.md +326 -0
  19. package/docs/history/codebuddy/CHANGELOG.md +668 -0
  20. package/docs/history/mimo/CHANGELOG.md +346 -0
  21. package/dynamic/agy.host.js +1237 -0
  22. package/dynamic/agy.host.template.mjs +140 -0
  23. package/dynamic/codebuddy.host.js +1787 -0
  24. package/dynamic/codebuddy.host.template.mjs +162 -0
  25. package/dynamic/mimo.host.js +1732 -0
  26. package/dynamic/mimo.host.template.mjs +169 -0
  27. package/dynamic/opencode.host.js +1443 -0
  28. package/dynamic/opencode.host.template.mjs +125 -0
  29. package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
  30. package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
  31. package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
  32. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
  33. package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
  34. package/mcp/agy-mcp-server.mjs +6 -0
  35. package/mcp/cli-bridge-mcp.mjs +185 -0
  36. package/mcp/codebuddy-mcp-server.mjs +6 -0
  37. package/mcp/mimo-mcp-server.mjs +6 -0
  38. package/mcp/providers/agy.mjs +254 -0
  39. package/mcp/providers/codebuddy.mjs +397 -0
  40. package/mcp/providers/mimo.mjs +290 -0
  41. package/mcp/providers/opencode.mjs +236 -0
  42. package/package.json +89 -4
  43. package/preset/agy-first/agent.cordis.yml +141 -0
  44. package/preset/agy-first/cli-bridge.mjs +169 -0
  45. package/preset/agy-first/plugin-list.mjs +26 -0
  46. package/preset/agy-first/preset.yml +3 -0
  47. package/preset/codebuddy-first/agent.cordis.yml +141 -0
  48. package/preset/codebuddy-first/cli-bridge.mjs +188 -0
  49. package/preset/codebuddy-first/plugin-list.mjs +20 -0
  50. package/preset/codebuddy-first/preset.yml +3 -0
  51. package/preset/mimo-first/agent.cordis.yml +143 -0
  52. package/preset/mimo-first/cli-bridge.mjs +236 -0
  53. package/preset/mimo-first/plugin-list.mjs +39 -0
  54. package/preset/mimo-first/preset.yml +3 -0
  55. package/preset/opencode-first/agent.cordis.yml +141 -0
  56. package/preset/opencode-first/cli-bridge.mjs +147 -0
  57. package/preset/opencode-first/plugin-list.mjs +26 -0
  58. package/preset/opencode-first/preset.yml +3 -0
  59. package/preset/standard-base.mjs +102 -0
  60. package/providers/agy.mjs +398 -0
  61. package/providers/codebuddy.mjs +926 -0
  62. package/providers/mimo.mjs +864 -0
  63. package/providers/opencode.mjs +619 -0
  64. package/providers/registry.mjs +39 -0
  65. package/scripts/build.mjs +235 -0
  66. package/scripts/deploy.mjs +61 -0
  67. package/scripts/verify.mjs +148 -0
@@ -0,0 +1,1787 @@
1
+ // dynamic/codebuddy.host.template.mjs — codebuddy 动态沙箱宿主模板(cordis_define code.host 源)。
2
+ //
3
+ // 构建(node scripts/build.mjs --only=codebuddy)把 core/bridge-kit.mjs 与
4
+ // providers/codebuddy.mjs 分别内联进下方两个占位标记行(各占一行、行内仅此标记),
5
+ // 生成 dynamic/codebuddy.host.js。注意:标记字面量绝不能出现在本注释里 —— 构建做全局
6
+ // 替换,注释中出现同样会被替换成正文代码而炸掉语法。
7
+ // 沙箱纪律(旧仓 dynamic/host.js 头注 verbatim 精神):
8
+ // - 禁 import/require、无 TypeScript;无 AbortController/process 裸引用/Buffer/原生
9
+ // 定时器 —— process 只经 typeof 守卫探测;宿主能力全部来自注入的 harness.*/ctx。
10
+ // - 顶层 return { inject, apply } 为动态宿主约定形态(生成物是 .js/CJS 包装,顶层
11
+ // return 合法;本模板文件是 .mjs,不要直接 node --check 模板本身,只 check 生成物)。
12
+ // - 无 ctx.emit:状态经 ctx.get('cliBridgeCollector').mergeSnapshot('codebuddy', snap)
13
+ // 推给家级灯(旧 codebuddyCollector 服务名在 keystone 里保留别名);动态形态不宣告
14
+ // mode(旧语义:普通模式灯仅在调用 codebuddy 时临时显示)。
15
+ // - exe 解析保持旧动态三面语义(各产品面登录互斥、不跨面回退):workbuddy /
16
+ // codebuddy-en 用各自的 Program Files 候选(WORKBUDDY_BIN / CODEBUDDY_EN_BIN 覆盖);
17
+ // npm 面走 CODEBUDDY_BIN > subprocess.resolveExecutable(PATH) > npm 全局 bin 脚本。
18
+ // - 设置:沙箱经 getBuiltinModule('node:fs') 读一次统一设置文件(apply 时一次,
19
+ // 旧 dynamic 语义);不可得时回退旧扁平文件,再不可得用默认值。EN 面无凭据且
20
+ // 无 fs(descriptor 的 dsh-store 通道缺席)→ preflight 确定性 abort 给提示即可。
21
+ // core/bridge-kit.mjs — CLI Bridge 单一事实来源:通用执行/状态骨架。
22
+ //
23
+ // 深度重构核心(合并 agy-first-bridge / codebuddy-first-bridge / mimo-first-bridge):
24
+ // 三条 bridge 的 host 半此前各自复制了一份几乎相同的「状态引擎 + 行流 + 执行编排
25
+ // + 限流回退弹窗 + MCP 快照上报 + 展示层」。本文件把这些**与 provider 无关**的骨架
26
+ // 收敛成一份参数化实现,每个 provider 只提供一个 descriptor(见 providers/*.mjs 与
27
+ // PROVIDER-CONTRACT.md),由 createRunner/createStatusEngine 回调它完成差异化步骤。
28
+ //
29
+ // 骨架覆盖:
30
+ // - 纯函数:projectName / clampInt / shortLabel / summarizeArgs / endpointHost
31
+ // - 限流/网络判定:LIMIT_RE + isLimited(provider 可收窄)
32
+ // - 半行安全行流:createLineStream
33
+ // - 按项目(cwd)聚合、按 provider 分桶的状态引擎:createStatusEngine
34
+ // - 执行编排(前台/后台/超时守卫/瞬时错误重试/限流三选一弹窗/回退):createRunner
35
+ // - 工具结果与状态的人类可读渲染:renderResult / renderStatus
36
+ // - MCP 子进程快照上报:buildMcpBridgePayload / normalizeMcpBridge(provider 分文件)
37
+ // - 回退弹窗三选一文案(provider 名注入)
38
+ //
39
+ // 约束(沙箱与真实 Node 共用,勿违反):
40
+ // - 本模块**不 import 任何模块**、不用 process/Buffer/原生定时器/AbortController;
41
+ // 宿主能力(subprocess.spawn / ctx.interval / ctx.timeout / jobs / userQuestions)
42
+ // 全部经 createRunner 的 opts 注入,取消用 exec.signal + handle.terminate()。
43
+ // - 不序列化任何 DSH 活对象;跨 RPC 只传无 Host 引用的纯 JSON 叶子字段。
44
+
45
+ // ── 常量 ──────────────────────────────────────────────────────────────────────
46
+
47
+ const MAX_TRAIL = 12
48
+ const MAX_ARG_LEN = 120
49
+ const MAX_PROJECTS = 24
50
+ const MAX_SESSIONS = 256
51
+
52
+ // 限流/网络类错误的宽松正则(英文 + 中文)。provider descriptor 可用自己的收窄版覆盖。
53
+ const LIMIT_RE =
54
+ /rate.?limit|ratelimit|\b429\b|too many|quota|insufficient|credit|balance|exhausted|exceed|\bnetwork\b|offline|ENETUNREACH|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|\btimeouts?\b|timed out|unavailable|\b50[023]\b|\b40[13]\b|unauthorized|invalid api|api key|proxy|socket|tls|ssl|\bdns\b|网络|超时|限流|流量|受限|配额|金额|余额|额度|认证|连接|断开/i
55
+
56
+ // 家级插件与 MCP 子进程之间的快照文件:{dsh-home}/cli-bridge-mcp-<provider>.json
57
+ const MCP_BRIDGE_PREFIX = 'cli-bridge-mcp-'
58
+ const MCP_BRIDGE_STALE_MS = 90000
59
+ function mcpBridgeFileName(providerId) { return MCP_BRIDGE_PREFIX + providerId + '.json' }
60
+
61
+ // ── 纯函数 ────────────────────────────────────────────────────────────────────
62
+
63
+ function projectName(cwd) {
64
+ const s = String(cwd || '')
65
+ const parts = s.split(/[\\/]/).filter(Boolean)
66
+ return parts.length ? parts[parts.length - 1] : s
67
+ }
68
+
69
+ function clampInt(v, def, min, max) {
70
+ const n = Number(v)
71
+ if (!Number.isFinite(n)) return def
72
+ const i = Math.floor(n)
73
+ if (i < min) return min
74
+ if (i > max) return max
75
+ return i
76
+ }
77
+
78
+ function shortLabel(prompt) {
79
+ const s = String(prompt || '').replace(/\s+/g, ' ').trim()
80
+ return s.length > 80 ? s.slice(0, 77) + '...' : s
81
+ }
82
+
83
+ // 工具入参摘要(状态灯/活动面板)。返回可无损 JSON 化的数据;undefined 值跳过
84
+ // (DSH tool 渲染层不接受 undefined)。
85
+ function summarizeArgs(parameters) {
86
+ if (!parameters || typeof parameters !== 'object') return null
87
+ const out = {}
88
+ for (const k of Object.keys(parameters)) {
89
+ let v = parameters[k]
90
+ if (v === undefined) continue
91
+ if (typeof v === 'string' && v.length > MAX_ARG_LEN) v = v.slice(0, MAX_ARG_LEN) + '…'
92
+ out[k] = v
93
+ }
94
+ return out
95
+ }
96
+
97
+ function endpointHost(url) {
98
+ const m = String(url || '').match(/^https?:\/\/([^/]+)/i)
99
+ return m ? m[1] : ''
100
+ }
101
+
102
+ // 限流/网络类失败(要问用户,重试可能烧额度)。
103
+ function isLimited(res) {
104
+ if (!res || res.ok) return false
105
+ // AUTH_REQUIRED 是配置问题(缺凭据),弹回退/重试无意义 —— 直接给可操作指引。
106
+ if (res.status === 'AUTH_REQUIRED') return false
107
+ if (res.status === 'SPAWN_ERROR' || res.status === 'CLI_UNAVAILABLE' || res.status === 'HUNG_TIMEOUT') return true
108
+ const hay = String(res.stderr || '') + ' ' + String(res.status || '')
109
+ // 端点域与登录域不匹配的 401:需要用户换登录域,不是限流。
110
+ if (/401 authentication failed|differs from the current product endpoint/i.test(hay)) return false
111
+ return LIMIT_RE.test(hay)
112
+ }
113
+
114
+ // ── 半行安全的行流(跨 chunk 的 NDJSON 行拼接)───────────────────────────────
115
+ function createLineStream(consumeLine) {
116
+ let pending = ''
117
+ function pushChunk(text) {
118
+ if (typeof text !== 'string' || !text) return
119
+ const lines = (pending + text).split(/\r?\n/)
120
+ pending = lines.pop() || ''
121
+ for (const ln of lines) consumeLine(ln)
122
+ }
123
+ function flush() {
124
+ const rest = pending
125
+ pending = ''
126
+ if (rest) consumeLine(rest)
127
+ }
128
+ return { pushChunk, flush }
129
+ }
130
+
131
+ // ── 状态引擎(按 provider + 项目目录聚合的运行状态表)─────────────────────────
132
+ // opts: { provider: <descriptor>, publish(snapshot) }
133
+ // 快照结构(顶层为全局聚合,向后兼容单 provider 消费者):
134
+ // { provider, state, running, lastStatus, lastAt, lastSessionId, lastBackend,
135
+ // fallbackActive, current, trail, runs, totalTokens, updatedAt,
136
+ // projects:[{ cwd,name,provider,state,running,current,trail,lastStatus,lastAt,
137
+ // lastSessionId,lastBackend,fallbackActive,runs,totalTokens,updatedAt }] }
138
+ function createStatusEngine(opts) {
139
+ const o = opts || {}
140
+ const provider = o.provider
141
+ const providerId = provider && provider.id ? provider.id : 'cli'
142
+ const publish = typeof o.publish === 'function' ? o.publish : null
143
+ const projects = Object.create(null)
144
+ // sessionId -> { cwd, backend, provider }。会话按 provider 归属(登录域互斥),
145
+ // 续接时一次解析出 cwd 与 backend。容量上限 MAX_SESSIONS,FIFO 淘汰。
146
+ const sessions = Object.create(null)
147
+
148
+ function ensureProject(cwd) {
149
+ const key = String(cwd || '')
150
+ let p = projects[key]
151
+ if (!p) {
152
+ if (Object.keys(projects).length >= MAX_PROJECTS) {
153
+ const idle = Object.keys(projects).filter((k) => projects[k].running === 0)
154
+ const pool = idle.length ? idle : Object.keys(projects)
155
+ const victim = pool.sort((a, b) => projects[a].updatedAt - projects[b].updatedAt)[0]
156
+ delete projects[victim]
157
+ }
158
+ p = { cwd: key, name: projectName(key), provider: providerId, state: 'idle', running: 0, lastStatus: null, lastAt: 0, lastSessionId: null, lastBackend: null, lastOk: false, lastFailed: false, fallbackActive: false, current: null, trail: [], runs: 0, totalTokens: 0, updatedAt: 0 }
159
+ projects[key] = p
160
+ }
161
+ return p
162
+ }
163
+
164
+ function globalStatus() {
165
+ const list = Object.keys(projects).map((k) => projects[k])
166
+ let running = 0, lastStatus = null, lastAt = 0, lastSessionId = null, lastBackend = null, fallbackActive = false, current = null, trail = [], updatedAt = 0, runs = 0, totalTokens = 0
167
+ for (const p of list) {
168
+ running += p.running
169
+ if (p.updatedAt > updatedAt) updatedAt = p.updatedAt
170
+ if (p.running > 0 && !current && p.current) current = p.current
171
+ if (p.lastAt > lastAt) { lastAt = p.lastAt; lastStatus = p.lastStatus; lastSessionId = p.lastSessionId; lastBackend = p.lastBackend }
172
+ if (p.fallbackActive) fallbackActive = true
173
+ runs += p.runs || 0
174
+ totalTokens += p.totalTokens || 0
175
+ for (const e of p.trail) trail.push(e)
176
+ }
177
+ trail.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0))
178
+ trail = trail.slice(-MAX_TRAIL)
179
+ const state = running > 0 ? 'running' : (fallbackActive ? 'fallback' : (lastStatus ? (lastStatus === 'SUCCESS' ? 'ok' : 'failed') : 'idle'))
180
+ return { state, running, lastStatus, lastAt, lastSessionId, lastBackend, fallbackActive, current, trail, runs, totalTokens, updatedAt }
181
+ }
182
+
183
+ function statusSnapshot() {
184
+ const g = globalStatus()
185
+ const list = Object.keys(projects).map((k) => projects[k]).sort((a, b) => b.updatedAt - a.updatedAt)
186
+ return Object.assign({ provider: providerId }, g, {
187
+ projects: list.map((p) => ({ cwd: p.cwd, name: p.name, provider: providerId, state: p.state, running: p.running, current: p.current, trail: p.trail.slice(-MAX_TRAIL), lastStatus: p.lastStatus, lastAt: p.lastAt, lastSessionId: p.lastSessionId, lastBackend: p.lastBackend, fallbackActive: p.fallbackActive, runs: p.runs || 0, totalTokens: p.totalTokens || 0, updatedAt: p.updatedAt }))
188
+ })
189
+ }
190
+
191
+ function emit() { if (publish) { try { publish(statusSnapshot()) } catch (e) {} } }
192
+
193
+ function begin(cwd) {
194
+ const p = ensureProject(cwd)
195
+ p.running += 1
196
+ p.state = 'running'
197
+ p.updatedAt = Date.now()
198
+ emit()
199
+ }
200
+
201
+ function end(res, cwd) {
202
+ const p = ensureProject(cwd)
203
+ p.running = Math.max(0, p.running - 1)
204
+ p.lastStatus = res ? res.status : null
205
+ p.lastAt = Date.now()
206
+ if (res && res.sessionId) {
207
+ p.lastSessionId = res.sessionId
208
+ sessions[res.sessionId] = { cwd: p.cwd, backend: res.backend || (provider && provider.defaultBackend), provider: providerId }
209
+ const keys = Object.keys(sessions)
210
+ if (keys.length > MAX_SESSIONS) delete sessions[keys[0]]
211
+ }
212
+ if (res && res.backend) p.lastBackend = res.backend
213
+ p.runs = (p.runs || 0) + 1
214
+ if (res && typeof res.totalTokens === 'number' && res.totalTokens > 0) p.totalTokens = (p.totalTokens || 0) + res.totalTokens
215
+ if (res && res.fallback) { p.fallbackActive = true; p.state = 'fallback' }
216
+ else if (p.running > 0) { p.state = 'running' }
217
+ else { p.state = res && res.ok ? 'ok' : 'failed'; p.lastOk = !!(res && res.ok); p.lastFailed = !(res && res.ok) }
218
+ p.current = null
219
+ p.updatedAt = Date.now()
220
+ emit()
221
+ }
222
+
223
+ // 流事件折叠为实时状态。provider 若有 stream-json,提供 foldStreamEvent(ev, project);
224
+ // 否则回退到 generic 形态(不产生实时步骤)。
225
+ function foldEvent(ev, cwd) {
226
+ const p = ensureProject(cwd)
227
+ p.updatedAt = Date.now()
228
+ if (provider && typeof provider.foldStreamEvent === 'function') {
229
+ try { provider.foldStreamEvent(ev, p, summarizeArgs, MAX_ARG_LEN, MAX_TRAIL) } catch (e) {}
230
+ }
231
+ emit()
232
+ }
233
+
234
+ // 会话路由(cwd + backend 一次解析)。续接(--resume/--continue)时未给 cwd
235
+ // 则回落到该 session 所在项目的 cwd(否则换目录报找不到会话)。
236
+ function resolveTarget(args, fallbackCwd) {
237
+ const a = args || {}
238
+ let backend = null, cwd = null
239
+ if (a.sessionId) { const hit = sessions[a.sessionId]; if (hit && hit.provider === providerId) { backend = hit.backend; cwd = hit.cwd } }
240
+ if (a.cwd) {
241
+ cwd = String(a.cwd)
242
+ } else if (!cwd && (a.sessionId || a.continueLatest)) {
243
+ const list = Object.keys(projects).map((k) => projects[k]).sort((a, b) => b.updatedAt - a.updatedAt)
244
+ if (a.sessionId) {
245
+ const projHit = list.find((p) => p.lastSessionId === a.sessionId)
246
+ if (projHit) { cwd = projHit.cwd; if (!backend) backend = projHit.lastBackend }
247
+ } else if (list.length && list[0].lastSessionId) { cwd = list[0].cwd; if (!backend) backend = list[0].lastBackend }
248
+ }
249
+ return { cwd: cwd || fallbackCwd, backend }
250
+ }
251
+
252
+ function resolveCwd(args, fallbackCwd) { return resolveTarget(args, fallbackCwd).cwd }
253
+
254
+ return { providerId, projects, sessions, ensureProject, globalStatus, statusSnapshot, begin, end, foldEvent, resolveCwd, resolveTarget }
255
+ }
256
+
257
+ // ── 回退弹窗三选一(provider 名注入)──────────────────────────────────────────
258
+ function fallbackLabels(provider) {
259
+ const bk = (provider && provider.id) || 'CLI'
260
+ return {
261
+ fallback: '使用 DSH 本地 API 配置(回退)',
262
+ retry: '重试 ' + bk + ' 一次',
263
+ cancel: '不回退(返回错误)'
264
+ }
265
+ }
266
+
267
+ // 失败结果的可行动指引;provider.failureHint 优先,未给则通用兜底。
268
+ function failureHint(res, provider) {
269
+ if (!res || res.ok) return ''
270
+ if (provider && typeof provider.failureHint === 'function') {
271
+ try { const h = provider.failureHint(res); if (h) return h } catch (e) {}
272
+ }
273
+ if (res.status === 'AUTH_REQUIRED') return '[诊断] ' + String(res.stderr || '')
274
+ if (res.status === 'PARSE_ERROR') return '[诊断] 未能从输出解析出 result 事件(进程可能被中断或输出被截断)。可重试;持续出现请检查 CLI 版本。'
275
+ if (/401 authentication failed|differs from the current product endpoint/i.test(String(res.stderr || ''))) {
276
+ return '[诊断] 认证失败(401):请求端点与当前 CLI 登录域不匹配(国际/国内登录互斥)。请让用户在对应产品域完成 CLI 登录后重试;这不是限流。'
277
+ }
278
+ return ''
279
+ }
280
+
281
+ function fallbackResult(res, mode, provider) {
282
+ const bk = (provider && provider.id) || 'CLI'
283
+ return Object.assign({}, res, { ok: false, fallback: true, status: 'FALLBACK_TO_DSH', response: '', stderr: (res && res.stderr ? res.stderr : '') + ' [user chose DSH fallback for ' + bk + ']' })
284
+ }
285
+
286
+ // ── 展示层 ────────────────────────────────────────────────────────────────────
287
+ function renderResult(value, provider) {
288
+ const v = value || {}
289
+ // backendLabel(codebuddy 旧语义 v1.1.0 verbatim:'workbuddy OK'/'codebuddy-intl OK'):
290
+ // 结果带具体 backend 时用 backend 标头;agy/mimo 旧包标头是硬编码 provider 名,不开此旗。
291
+ const bk = (provider && provider.backendLabel && v.backend) || (provider && provider.id) || v.backend || 'CLI'
292
+ if (v.background) return [{ type: 'text', text: bk + ' dispatched in background (mode=' + v.mode + '). jobId=' + v.jobId + '. Collect with job_output.' }]
293
+ if (v.fallback) return [{ type: 'text', text: bk + ' 回退:用户选择使用 DSH 本地 API 配置(原因 ' + v.reason + ')。请改用原生工具/本地模型完成本任务,不要再调 ' + bk + '。' }]
294
+ const head = bk + ' ' + (v.ok ? 'OK' : 'FAILED') + ' [status=' + v.status + ' mode=' + v.mode + (v.modelUsed ? ' model=' + v.modelUsed : '') + (v.retried ? ' retried=1' : '') + (v.sessionId ? ' session=' + v.sessionId : '') + (v.totalTokens != null ? ' tokens=' + v.totalTokens : '') + (v.durationSeconds != null ? ' ' + v.durationSeconds + 's' : '') + ']'
295
+ const hint = failureHint(v, provider)
296
+ let body = v.response ? v.response : (v.stderr ? '[stderr] ' + v.stderr : (v.rawStdout ? '[raw] ' + v.rawStdout : ''))
297
+ if (hint) body = (body ? body + '\n\n' : '') + hint
298
+ return [{ type: 'text', text: head + (body ? '\n\n' + body : '') }]
299
+ }
300
+
301
+ function renderStatus(value, provider) {
302
+ const v = value || {}
303
+ const bk = (provider && provider.id) || 'CLI'
304
+ const lines = []
305
+ lines.push(bk + ' status: ' + v.state + (v.running > 0 ? ' (' + v.running + ' running)' : '') + (v.projects && v.projects.length > 1 ? ' across ' + v.projects.length + ' projects' : '') + (v.totalTokens ? ' | total ' + v.runs + ' runs, ' + v.totalTokens + ' tokens' : ''))
306
+ const projList = (v.projects && v.projects.length) ? v.projects : null
307
+ if (projList) {
308
+ for (const p of projList) {
309
+ const cur = p.current ? (' step ' + p.current.stepIndex + ' → ' + p.current.tool + (p.current.args ? ' ' + JSON.stringify(p.current.args) : '')) : (p.running > 0 ? ' (starting / thinking)' : '')
310
+ const usage = (p.runs ? ' | total ' + p.runs + ' runs, ' + (p.totalTokens || 0) + ' tokens' : '')
311
+ const bkTag = (p.lastBackend && p.lastBackend !== bk) ? ' [' + p.lastBackend + ']' : ''
312
+ lines.push('· ' + p.name + ' [' + p.state + (p.running > 0 ? ' ×' + p.running : '') + ']' + bkTag + cur + (p.lastStatus ? ' | last=' + p.lastStatus + (p.lastSessionId ? ' ' + p.lastSessionId.slice(0, 8) : '') : '') + usage)
313
+ if (p.trail && p.trail.length) {
314
+ lines.push(' steps:')
315
+ for (const e of p.trail.slice(-3)) { const a = e.args ? ' ' + JSON.stringify(e.args) : ''; lines.push(' [' + e.state + '] step ' + e.stepIndex + ' ' + e.tool + a) }
316
+ }
317
+ }
318
+ } else {
319
+ if (v.current) { const c = v.current; lines.push('current: step ' + c.stepIndex + ' → ' + c.tool + (c.args ? ' ' + JSON.stringify(c.args) : '')) }
320
+ else if (v.state === 'running') lines.push('current: (starting / thinking)')
321
+ if (v.trail && v.trail.length) {
322
+ lines.push('recent steps:')
323
+ for (const e of v.trail.slice(-6)) { const a = e.args ? ' ' + JSON.stringify(e.args) : ''; lines.push(' [' + e.state + '] step ' + e.stepIndex + ' ' + e.tool + a) }
324
+ }
325
+ if (v.lastStatus) lines.push('last: ' + v.lastStatus + (v.lastSessionId ? ' session=' + v.lastSessionId : '') + (v.lastAt ? ' @ ' + new Date(v.lastAt).toISOString() : ''))
326
+ }
327
+ if (v.updatedAt) lines.push('updatedAt: ' + new Date(v.updatedAt).toISOString())
328
+ return [{ type: 'text', text: lines.join('\n') }]
329
+ }
330
+
331
+ // ── MCP 子进程快照上报 ────────────────────────────────────────────────────────
332
+ // MCP server 是独立 stdio 子进程:没有 ctx.emit、拿不到 collector 服务,所以把快照
333
+ // 原子写进 {dsh-home}/cli-bridge-mcp-<provider>.json,家级灯在响应请求时读取合并。
334
+ function buildMcpBridgePayload(snap, pid, nowMs) {
335
+ const s = snap && typeof snap === 'object' ? snap : {}
336
+ const src = Array.isArray(s.projects) ? s.projects : []
337
+ const projects = []
338
+ for (const p of src) {
339
+ if (!p || !p.cwd) continue
340
+ projects.push({
341
+ cwd: String(p.cwd), name: p.name ? String(p.name) : '', provider: p.provider || s.provider || '', state: p.state ? String(p.state) : 'idle',
342
+ running: Number(p.running) || 0,
343
+ current: p.current ? { stepIndex: Number(p.current.stepIndex) || 0, tool: String(p.current.tool || ''), args: p.current.args || null } : null,
344
+ trail: Array.isArray(p.trail) ? p.trail.slice(-MAX_TRAIL).map((e) => ({ stepIndex: Number(e && e.stepIndex) || 0, tool: String((e && e.tool) || ''), state: String((e && e.state) || ''), args: (e && e.args) || null })) : [],
345
+ lastStatus: p.lastStatus ? String(p.lastStatus) : null, lastAt: Number(p.lastAt) || 0, lastSessionId: p.lastSessionId ? String(p.lastSessionId) : null,
346
+ lastBackend: p.lastBackend ? String(p.lastBackend) : null, fallbackActive: !!p.fallbackActive, runs: Number(p.runs) || 0, totalTokens: Number(p.totalTokens) || 0, updatedAt: Number(p.updatedAt) || 0
347
+ })
348
+ }
349
+ return { source: 'mcp', provider: s.provider || '', pid: Number(pid) || 0, updatedAt: Number(nowMs) || 0, projects }
350
+ }
351
+
352
+ function normalizeMcpBridge(payload, nowMs, staleMs) {
353
+ if (!payload || typeof payload !== 'object') return null
354
+ const projects = Array.isArray(payload.projects) ? payload.projects : []
355
+ const updatedAt = Number(payload.updatedAt) || 0
356
+ const stale = (Number(nowMs) || 0) - updatedAt > (Number(staleMs) || MCP_BRIDGE_STALE_MS)
357
+ const out = []
358
+ for (const p of projects) {
359
+ if (!p || !p.cwd) continue
360
+ if (stale && (Number(p.running) || 0) > 0) out.push(Object.assign({}, p, { running: 0, current: null, state: p.state === 'running' ? 'idle' : p.state }))
361
+ else out.push(p)
362
+ }
363
+ return { source: 'mcp', provider: payload.provider || '', pid: Number(payload.pid) || 0, updatedAt, stale, projects: out }
364
+ }
365
+
366
+ // ── 执行编排(三形态共用;provider 差异全走 descriptor)──────────────────────
367
+ // o: {
368
+ // ctx, // Cordis ctx(interval/timeout/get)
369
+ // subprocess, // DSH subprocess 服务
370
+ // provider, // descriptor(见 PROVIDER-CONTRACT.md)
371
+ // engine, // createStatusEngine({provider}) 实例
372
+ // resolveExe, // async (backend, signal) => 'exe' | [nodeExe, binPath] | throw
373
+ // getCwdFallback, // () => 默认 cwd
374
+ // getPreferredBackend, // () => 用户偏好后端(可缺省)
375
+ // getSettings, // () => 当前设置对象(可缺省;provider 用它读 defaultModel 等)
376
+ // planActiveFor, // (exec) => bool(plan 模式判定,可缺省)
377
+ // defaultMode // 'auto'
378
+ // }
379
+ function createRunner(o) {
380
+ const ctx = o.ctx
381
+ const subprocess = o.subprocess
382
+ const provider = o.provider
383
+ const engine = o.engine
384
+ const stdio = { stdin: 'ignore', stdout: { maxBytes: 4000000, spill: { maxBytes: 40000000 } }, stderr: { maxBytes: 1000000, spill: { maxBytes: 8000000 } } }
385
+
386
+ function readStreams(handle) {
387
+ const stdoutText = handle.collected.stdout ? handle.collected.stdout.readFrom(0).text : ''
388
+ const stderrText = handle.collected.stderr ? handle.collected.stderr.readFrom(0).text : ''
389
+ return { stdoutText, stderrText }
390
+ }
391
+
392
+ let built_env = null
393
+ function spawnSpec(argv, cwd, callerSignal) {
394
+ const spec = { argv, cwd, stdio, graceMs: 5000 }
395
+ if (callerSignal) spec.signal = callerSignal
396
+ if (built_env && typeof built_env === 'object') spec.env = built_env
397
+ return spec
398
+ }
399
+
400
+ function startLiveParser(handle, cwd) {
401
+ let cursor = 0
402
+ const stream = createLineStream((ln) => {
403
+ const t = ln.trim()
404
+ if (!t.startsWith('{')) return
405
+ try { engine.foldEvent(JSON.parse(t), cwd) } catch (e) {}
406
+ })
407
+ const tick = function () {
408
+ try {
409
+ const full = handle.collected.stdout ? handle.collected.stdout.readFrom(0).text : ''
410
+ if (full.length <= cursor) return
411
+ const fresh = full.slice(cursor)
412
+ cursor = full.length
413
+ stream.pushChunk(fresh)
414
+ } catch (e) {}
415
+ }
416
+ const disposeInterval = ctx.interval(tick, 250)
417
+ return function disposeLive() { try { stream.flush() } catch (e) {} disposeInterval() }
418
+ }
419
+
420
+ async function runSync(argv, cwd, timeoutSec, callerSignal, extraEnv) {
421
+ const spec = spawnSpec(argv, cwd, callerSignal)
422
+ if (extraEnv && typeof extraEnv === 'object') spec.env = Object.assign({}, spec.env, extraEnv)
423
+ const handle = subprocess.spawn(spec)
424
+ let lastEventSummary = '(no events yet)'
425
+ let timedOut = false
426
+ const disposeTimer = ctx.timeout(function () {
427
+ timedOut = true
428
+ try {
429
+ const p = engine.ensureProject(cwd)
430
+ lastEventSummary = (p.current ? ('last step ' + p.current.stepIndex + ' -> ' + p.current.tool) : '') +
431
+ ' | trail=' + (p.trail.length ? p.trail.slice(-2).map((e) => '[' + e.state + ']' + e.tool).join(',') : 'empty') +
432
+ ' | elapsed=' + Math.round((Date.now() - (p.updatedAt || Date.now())) / 1000) + 's since last activity'
433
+ } catch (e) {}
434
+ try { handle.terminate() } catch (e) {}
435
+ }, (timeoutSec + 60) * 1000)
436
+ const disposeLive = startLiveParser(handle, cwd)
437
+ try {
438
+ const outcome = await handle.done
439
+ const s = readStreams(handle)
440
+ return { outcome, stdoutText: s.stdoutText, stderrText: s.stderrText, timedOut, lastEventSummary }
441
+ } finally { disposeLive(); disposeTimer() }
442
+ }
443
+
444
+ async function askFallback(exec, res, canRetry) {
445
+ const uq = ctx.get('userQuestions')
446
+ if (!uq || !exec || !exec.agent) return 'error'
447
+ const labels = fallbackLabels(provider)
448
+ const bk = provider.id
449
+ const detail = String(res.stderr || res.response || res.status || '').slice(-600)
450
+ const opts = [{ label: labels.fallback, description: '本次改由 DSH 本地模型/原生工具完成,不再走 ' + bk }]
451
+ if (canRetry) opts.push({ label: labels.retry, description: '再调用一次 ' + bk + '(网络抖动时可用)' })
452
+ opts.push({ label: labels.cancel, description: '不回退,直接返回 ' + bk + ' 错误' })
453
+ try {
454
+ const ans = await uq.ask({ agent: exec.agent, signal: exec.signal, questions: [{ id: bk + '-fallback', header: bk + ' 受限', question: bk + ' 调用失败(疑似流量受限/网络不通,状态=' + String(res.status) + ')。是否改用 DSH 本地 API 配置继续?', detail, options: opts }] })
455
+ const sel = (ans && ans.answers && ans.answers[0] && ans.answers[0].selected) || []
456
+ if (sel.indexOf(labels.fallback) >= 0) return 'fallback'
457
+ if (sel.indexOf(labels.retry) >= 0) return 'retry'
458
+ return 'error'
459
+ } catch (e) { return 'error' }
460
+ }
461
+
462
+ async function coreExecute(rawArgs, exec) {
463
+ const args = rawArgs || {}
464
+ built_env = null
465
+ const mk = (over) => Object.assign({ ok: false, status: 'BAD_ARGS', response: '', sessionId: null, durationSeconds: null, numTurns: null, totalTokens: null, exitCode: null, mode: provider.defaultMode || 'auto', backend: null, stderr: '' }, over)
466
+ if (!args.prompt || !String(args.prompt).trim()) return mk({ stderr: 'prompt is required' })
467
+
468
+ const target = engine.resolveTarget(args, o.getCwdFallback())
469
+ const backend = provider.resolveBackend ? provider.resolveBackend(args, target, o.getPreferredBackend ? o.getPreferredBackend() : null) : (o.getPreferredBackend ? o.getPreferredBackend() : provider.defaultBackend)
470
+ const cwd = target.cwd
471
+
472
+ // provider 前置检查(凭据/桌面健康等):返回 { abort: res } 则直接结束。
473
+ if (provider.preflight) {
474
+ const pre = provider.preflight({ args, exec, backend, cwd, engine, ctx, o })
475
+ if (pre && pre.abort) { engine.begin(cwd); engine.end(pre.abort, cwd); return pre.abort }
476
+ }
477
+
478
+ let exePrefix = provider.fallbackExePrefix ? provider.fallbackExePrefix(backend) : [provider.exeName || provider.id]
479
+ let exeOk = true, resolveErr = ''
480
+ try { const resExe = await o.resolveExe(backend, exec ? exec.signal : undefined); exePrefix = Array.isArray(resExe) ? resExe : [resExe] }
481
+ catch (e) { exeOk = false; resolveErr = String(e && e.message || e) }
482
+
483
+ const built = provider.buildArgv(exePrefix, args, {
484
+ planActive: o.planActiveFor ? o.planActiveFor(exec) : false,
485
+ defaultMode: provider.defaultMode || 'auto',
486
+ defaultModel: (!args.model && o.getSettings) ? (o.getSettings() || {}).defaultModel || null : null,
487
+ backend, settings: o.getSettings ? o.getSettings() : null, engine
488
+ })
489
+ built_env = (built.env && typeof built.env === 'object') ? built.env : null
490
+ const unavailableStatus = provider.unavailableStatus || 'CLI_UNAVAILABLE'
491
+
492
+ if (!exeOk) {
493
+ engine.begin(cwd)
494
+ let res = mk({ status: unavailableStatus, mode: built.mode, backend, stderr: (backend || provider.id) + ' executable not found: ' + resolveErr })
495
+ if (await askFallback(exec, res, true) === 'fallback') res = fallbackResult(res, built.mode, provider)
496
+ engine.end(res, cwd); return res
497
+ }
498
+
499
+ // 非 spawn 路径(如 mimo 桌面走本地 HTTP):provider 若给 execute 且(未给 delegates
500
+ // 或 delegates(backend)=true)则完全交给它;否则落回通用 pipeline(jobs/重试/弹窗)。
501
+ if (provider.execute && (!provider.delegates || provider.delegates(backend, args))) {
502
+ engine.begin(cwd)
503
+ try {
504
+ const res = await provider.execute({ args, exec, backend, cwd, built, engine, ctx, o, runSync })
505
+ engine.end(res, cwd); return res
506
+ } catch (e) {
507
+ const res = mk({ status: 'SPAWN_ERROR', mode: built.mode, backend, stderr: String(e && e.message || e) })
508
+ engine.end(res, cwd); return res
509
+ }
510
+ }
511
+
512
+ const jobs = ctx.get('jobs')
513
+ if (args.background && jobs && exec && exec.agent) {
514
+ try {
515
+ engine.begin(cwd)
516
+ const jobId = jobs.start({
517
+ kind: 'bash', label: provider.id + ': ' + shortLabel(args.prompt), owner: exec.agent,
518
+ run() {
519
+ const handle = subprocess.spawn(spawnSpec(built.argv, cwd))
520
+ const disposeLive = startLiveParser(handle, cwd)
521
+ let bgKilled = false
522
+ const disposeGuard = ctx.timeout(function () { bgKilled = true; try { handle.terminate() } catch (e) {} }, (built.timeoutSec + 60) * 1000)
523
+ const done = handle.done.then(function (outcome) {
524
+ disposeLive(); disposeGuard()
525
+ const s = readStreams(handle)
526
+ const res = provider.buildResult(provider.parseJson(s.stdoutText), outcome, built.mode, s.stderrText, s.stdoutText, backend)
527
+ if (bgKilled && !res.ok) { res.status = 'HUNG_TIMEOUT'; res.stderr = (res.stderr ? res.stderr + ' ' : '') + '[killed by timeout guard after ' + built.timeoutSec + 's; raise timeoutSec for long builds]' }
528
+ engine.end(res, cwd)
529
+ return { status: res.ok ? 'completed' : 'failed', detail: provider.id + ' ' + res.status, output: JSON.stringify(res) }
530
+ }).catch(function (err) { disposeLive(); disposeGuard(); engine.end(mk({ status: 'JOB_ERROR', backend }), cwd); return { status: 'failed', detail: String(err && err.message || err) } })
531
+ return { cancel: function () { try { handle.terminate() } catch (e) {} }, done }
532
+ }
533
+ })
534
+ return { ok: true, background: true, jobId: String(jobId), mode: built.mode, backend, note: provider.id + ' running in background; collect with job_output ' + String(jobId) + '. Background failures do NOT open the fallback dialog; on failure re-run in foreground to be prompted.' }
535
+ } catch (e) {
536
+ const res = mk({ status: 'JOB_START_ERROR', mode: built.mode, backend, stderr: 'failed to start background job: ' + String(e && e.message || e) })
537
+ engine.end(res, cwd); return res
538
+ }
539
+ }
540
+
541
+ engine.begin(cwd)
542
+ try {
543
+ let attempt = 0, cliRetried = false, res
544
+ while (true) {
545
+ attempt += 1
546
+ const r = await runSync(built.argv, cwd, built.timeoutSec, exec ? exec.signal : undefined, built.env)
547
+ if (r.timedOut) { res = mk({ status: 'HUNG_TIMEOUT', mode: built.mode, backend, exitCode: r.outcome ? r.outcome.exitCode : null, stderr: provider.id + ' did not finish within ' + built.timeoutSec + 's (DSH hard timeout). Last activity: ' + r.lastEventSummary + '. NOTE: raise timeoutSec for long builds; this was a hang guard.' }); break }
548
+ res = provider.buildResult(provider.parseJson(r.stdoutText), r.outcome, built.mode, r.stderrText, r.stdoutText, backend)
549
+ if (res.ok) break
550
+ if (provider.isTransientCliError && provider.isTransientCliError(res)) {
551
+ if (!cliRetried) { cliRetried = true; attempt -= 1; continue }
552
+ res.retried = true; break
553
+ }
554
+ if (!(provider.isLimited ? provider.isLimited(res) : isLimited(res))) break
555
+ const decision = await askFallback(exec, res, attempt < 2)
556
+ if (decision === 'fallback') { res = fallbackResult(res, built.mode, provider); break }
557
+ if (decision === 'retry' && attempt < 2) continue
558
+ break
559
+ }
560
+ engine.end(res, cwd); return res
561
+ } catch (e) {
562
+ const res = mk({ status: 'SPAWN_ERROR', mode: built.mode, backend, stderr: String(e && e.message || e) })
563
+ const out = (await askFallback(exec, res, true) === 'fallback') ? fallbackResult(res, built.mode, provider) : res
564
+ engine.end(out, cwd); return out
565
+ }
566
+ }
567
+
568
+ return { coreExecute, runSync, askFallback }
569
+ }
570
+
571
+ // providers/codebuddy.mjs — codebuddy provider descriptor(真移植,替换 Lead 冒烟占位)。
572
+ //
573
+ // 移植基线:codebuddy-first-bridge v1.6.x core/codebuddy-core.mjs 的纯逻辑
574
+ // (后端注册表/端点对齐/凭据链/模型目录/buildArgv/parseCodebuddyJson/buildResult/
575
+ // foldEvent/settings 清洗/三工具 schema)与 preset/codebuddy-first 内联语义,
576
+ // 逐字保持(PROVIDER-CONTRACT.md §8 红线:工具入参语义与 schema 不许漂移)。
577
+ // 与 provider 无关的骨架(状态引擎/行流/回退弹窗/展示层)在 core/bridge-kit.mjs。
578
+ //
579
+ // TODO(Lead 定案,0.1.0 不做):旧包 credits/促销面板的 modelPromotions /
580
+ // isPromotionActive / activePromotionFor(约 200 行)不进统一设置面板;本版
581
+ // modelCatalogDetailed 只给「倍率 + 授权」两标(无促销角标),行为差异在设置面板
582
+ // 单列,运行面(buildArgv/CLI 选择)不受影响。
583
+ //
584
+ // 红线(PROVIDER-CONTRACT.md §8):
585
+ // - 本文件会被 build 内联进 dynamic 沙箱(new Function):绝不出现元环境字面量;
586
+ // process/env/fs 只经 deps 注入或 typeof 守卫读取;宿主能力全部走 attach(hostOpts)。
587
+ // - 只允许 import '../core/bridge-kit.mjs' 一行。
588
+ // - isLimited 用本文件自定义的中文限流表(旧 core verbatim),不用 kit 通用表。
589
+
590
+ // ── 提示词策略段(旧 preset POLICY_TEXT verbatim;systemPrompt order 5)────────
591
+ const POLICY_TEXT = [
592
+ 'codebuddy-first execution policy (local codebuddy CLI bridge, with fallback + live indicator + live status tool).',
593
+ '',
594
+ 'A local sub-agent CLI named "codebuddy" (Tencent CodeBuddy Code) is available through the codebuddy_run and codebuddy_continue tools. DSH fully controls codebuddy: every invocation runs non-interactively with permissions auto-approved (--permission-mode bypassPermissions), so codebuddy never prompts and DSH decides all of its work. A header status light shows when codebuddy is working — one light per project (working directory), so parallel runs in different projects are tracked separately; call codebuddy_status any time to see what codebuddy is doing right now (per project: current tool/step, recent trail, and cumulative usage).',
595
+ '',
596
+ 'Priority rule for EVERY mode (normal, plan, accept-edits, subagent/workflow/ralph delegation, and goal rounds):',
597
+ '- For any real work — implementing features, editing/refactoring code, debugging, running builds, or multi-file investigation — prefer dispatching the task to codebuddy_run FIRST with a complete, self-contained prompt.',
598
+ '- Use native read/write/shell tools mainly for quick read-only lookups and for the final build/test verification of what codebuddy produced.',
599
+ '- In plan mode, call codebuddy_run with mode=auto (or mode=plan); in accept-edits/normal mode codebuddy_run applies edits directly.',
600
+ '- When you delegate to a DSH subagent or workflow, instruct that delegate to also prefer codebuddy_run.',
601
+ '- For long-running tasks, call codebuddy_run with background=true and collect the result with job_output; use codebuddy_status to watch progress.',
602
+ '',
603
+ 'Fallback protocol: when codebuddy is rate-limited or the network is down, codebuddy_run/codebuddy_continue automatically pop a confirmation dialog asking the user whether to use the DSH local API config. If the returned result has fallback=true (status FALLBACK_TO_DSH), the user chose to fall back: complete the task with native DSH tools / the local model and DO NOT call codebuddy again for this task. If ok=false without fallback, report the codebuddy error. Never loop codebuddy calls; never ask codebuddy to call back into DSH.',
604
+ '',
605
+ 'Model selection: codebuddy_run takes an optional model. When unspecified, the CLI default applies unless the user set a preferred default model in the plugin settings (then that is injected automatically per call). Which models ACTUALLY WORK depends on the ACCOUNT, and the authoritative live list comes from the server: running the CLI with a nonexistent model returns 400 followed by "Currently supported models for your account:" and one "- <id>" per line (core exposes parseAccountModels for this; the plugin caches the result automatically and the settings panel shows exactly those ids, with the credit multiplier of each). Do NOT trust the bundled product descriptor as a menu of usable models: it lists ids with an "-ioa" suffix (glm-5.3-ioa) and expensive flagships (claude-opus-4.8, gpt-5.4, gpt-6-astra) that are present in the catalogue but NOT licensed for this account, and picking one fails with 400 "only available for authorized users". The four backends are also not one pool: "codebuddy" and "codebuddy-intl" are the npm CLI and offer its catalogue - including the free tier (hy3 and deepseek-v4.1-flash cost x0.00 credits; hy4-preview costs x0.29 but is frequently free on promotional accounts); "codebuddy-en" (WorkBuddyAI) has its OWN catalogue which does NOT contain hy4-preview or deepseek-v4.1-flash - it offers hy3 (free), gpt-5.1-codex-mini, gemini-3.1-flash-lite, gemini-2.5-flash, minimax-m3, gemini-3.0-flash, deepseek-v3-2-volc, kimi-k2.5/2.6, glm-5.0/5.2/5.3, gpt-5.3-codex, gpt-5.4/5.5, gpt-5.6-sol/terra/luna and the role aliases (fast/balanced/primary/deep-model); "workbuddy" (domestic desktop) has a further catalogue (auto, hy4-preview, hy3, deepseek-v4.1-flash, glm-5.x, kimi-k2.6/2.7/2.8-preview/k3-1, minimax-m3/m2.7, deepseek-v4-pro). Within a face availability is still gated per ACCOUNT: an id outside the account catalogue fails with "400 model [...] service info not found", and one inside it but unlicensed fails with "400 model [...] is only available for authorized users". International ids (claude-sonnet-5, claude-opus-5, gemini-3.1-pro, ...) are per-account gated and return the latter for a domestic account. COST: every id carries a credit multiplier in the credits field of its descriptor (x0.00 means free, x3.31 means 3.31x the base rate); the settings panel shows each candidate with its multiplier and reads it live from the CLI descriptor, so it stays in sync when the CLI is upgraded. Measured free (x0.00) models for this account: hy3 and deepseek-v4.1-flash (both npm faces) and hy3 for WorkBuddyAI; hy4-preview is x0.29 on npm faces. Prefer the cheapest model that can do the job and use a free one for routine work; do not silently upgrade to an expensive id when the task is simple. To get the exact live list for a CLI, run it with a deliberately invalid model id - the reply prints "Currently supported models for your account:" followed by one "- <id>" per line (core exposes parseAccountModels for this). Pass a model only when the task clearly benefits from a specific one; the default is usually right. Optional effort: minimal/low/medium/high/xhigh/max. Optional maxTurns caps agentic turns (default unlimited).',
606
+ '',
607
+ 'Backends: codebuddy_run/codebuddy_continue take an optional backend parameter choosing which CLI face of the same engine (Tencent CodeBuddy Code) runs the task. "codebuddy" is the domestic CodeBuddy (npm CLI @tencent-ai/codebuddy-code, product endpoint www.codebuddy.ai) — default for coding work. "codebuddy-intl" is the INTERNATIONAL face of that SAME npm CLI (product.ioa.json catalogue: claude-sonnet-5, claude-opus-5, gemini-3.1-pro, gpt-6-astra, hy3-ioa …); use it when the account is an international CodeBuddy account. Its aliases "codebuddy-ioa" and "codebuddy-international" are accepted and normalized to codebuddy-intl. "codebuddy-en" is the WorkBuddy INTERNATIONAL edition — the CLI bundled with the WorkBuddyAI desktop app (C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai); the aliases "workbuddy-en" and "workbuddy-ai" are also accepted and normalized to codebuddy-en. "workbuddy" is the CLI bundled with the domestic WorkBuddy desktop app (product endpoint copilot.tencent.com, zero config) — the office-scenario face: documents, slides, spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. When the user asks for office/document/IM work, dispatch with backend="workbuddy"; for international accounts use "codebuddy-intl" (CodeBuddy 国际版) or "codebuddy-en" (WorkBuddy 国际版 / WorkBuddyAI). Sessions are kept per backend (login domains are exclusive), and continuing a session automatically routes back to the backend that owns it (explicit backend wins). A user-preferred default backend (plugin settings) applies when a call is new (no session) and no explicit backend is given.'
608
+ ].join('\n')
609
+
610
+ // ── 限流/网络/认证失败判定(旧 core verbatim)。仅匹配 stderr + status;数字码
611
+ // 加词边界(避免 "1500" 命中 500、"4013" 命中 401)。中文码(流量/受限/金额/
612
+ // 余额/额度/认证/连接/断开 等)为 codebuddy 面专属,必须逐字保留。
613
+ // 命名纪律(mimo 同款):本文件顶层名绝不与 core/bridge-kit.mjs 顶层导出同名 ——
614
+ // dynamic 形态把 kit 与 provider 内联进同一脚本作用域,同名 const 直接炸语法
615
+ // (LIMIT_RE 在 kit L33 已有,故此处带 CODEBUDDY_ 前缀)。
616
+ const CODEBUDDY_LIMIT_RE = /rate.?limit|ratelimit|\b429\b|too many|quota|insufficient|credit|balance|exhausted|exceed|\bnetwork\b|offline|ENETUNREACH|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|\btimeouts?\b|timed out|unavailable|\b50[023]\b|\b40[13]\b|unauthorized|invalid api|api key|proxy|socket|tls|ssl|\bdns\b|网络|超时|限流|流量|受限|配额|金额|余额|额度|认证|连接|断开/i
617
+
618
+ // ── 后端注册表(旧 core verbatim:端点按登录域自动对齐)───────────────────────
619
+ const BACKENDS = ['codebuddy', 'codebuddy-intl', 'codebuddy-en', 'workbuddy']
620
+
621
+ const BACKEND_ENDPOINTS = {
622
+ 'codebuddy': 'https://www.codebuddy.ai/v2',
623
+ 'codebuddy-intl': 'https://www.codebuddy.ai/v2',
624
+ 'codebuddy-en': 'https://www.workbuddy.ai/v2',
625
+ 'workbuddy': 'https://copilot.tencent.com/v2'
626
+ }
627
+
628
+ const AUTH_DOMAIN_ENDPOINTS = {
629
+ 'www.codebuddy.cn': 'https://www.codebuddy.cn/v2',
630
+ 'www.codebuddy.ai': 'https://www.codebuddy.ai/v2',
631
+ 'www.workbuddy.ai': 'https://www.workbuddy.ai/v2',
632
+ 'copilot.tencent.com': 'https://copilot.tencent.com/v2'
633
+ }
634
+
635
+ const BACKEND_AUTH_IDS = {
636
+ 'codebuddy': 'Tencent-Cloud.coding-copilot',
637
+ 'codebuddy-intl': 'Tencent-Cloud.coding-copilot',
638
+ 'codebuddy-en': 'workbuddy-desktop-ai',
639
+ 'workbuddy': 'workbuddy-desktop'
640
+ }
641
+
642
+ const BACKEND_LABELS = {
643
+ 'codebuddy': 'CodeBuddy 国内版(npm CLI)',
644
+ 'codebuddy-intl': 'CodeBuddy 国际版(npm CLI · 国际面)',
645
+ 'codebuddy-en': 'WorkBuddy 国际版(WorkBuddyAI 桌面 CLI)',
646
+ 'workbuddy': 'WorkBuddy 国内版(桌面 CLI)'
647
+ }
648
+
649
+ const BACKEND_ALIASES = {
650
+ 'workbuddy-en': 'codebuddy-en',
651
+ 'workbuddy-ai': 'codebuddy-en',
652
+ 'codebuddy-ioa': 'codebuddy-intl',
653
+ 'codebuddy-international': 'codebuddy-intl',
654
+ 'codebuddy-oversea': 'codebuddy-intl'
655
+ }
656
+
657
+ const DEFAULT_BACKEND = 'codebuddy'
658
+
659
+ const SETTINGS_KEYS = ['preferredBackend', 'defaultModel', 'codebuddyEnToken', 'endpointOverride']
660
+
661
+ // codebuddy-en 凭据的第三条通道:DSH 凭据库里可能已有的 workbuddy key(旧 core verbatim)。
662
+ const CREDENTIAL_KEYS = ['WORKBUDDY_TOKEN', 'WORKBUDDY_AI_TOKEN', 'CODEBUDDY_EN_TOKEN']
663
+
664
+ // 离线/读不到描述文件时的回退静态表(真机按账号实测快照,2026-09)。
665
+ const BACKEND_MODEL_IDS = {
666
+ 'codebuddy': ['hy4-preview', 'hy3', 'hy3-x', 'deepseek-v4.1-flash', 'glm-5.3', 'glm-5.3-flash', 'glm-5.2', 'glm-5.1', 'glm-5v-turbo', 'minimax-m3', 'minimax-m2.7', 'kimi-k3-1', 'kimi-k2.8-preview', 'kimi-k2.7', 'kimi-k2.6', 'deepseek-v4-pro'],
667
+ 'codebuddy-intl': ['claude-sonnet-5', 'claude-sonnet-5-1m', 'claude-opus-5', 'claude-opus-4.8', 'claude-opus-4.8-1m', 'gemini-3.1-pro', 'gemini-3.5-flash', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.5', 'gpt-5.4', 'glm-5.3-ioa', 'glm-5.3-flash-ioa', 'glm-5.2-ioa', 'kimi-k3-ioa', 'kimi-k2.8-preview', 'kimi-k2.6-ioa', 'minimax-m3-ioa', 'minimax-m2.7-ioa', 'deepseek-v4.1-flash', 'deepseek-v4-pro-ioa'],
668
+ // WorkBuddyAI 自制目录(product.json 顶层 37 项,按其 credits 排序:免费的在前)
669
+ 'codebuddy-en': ['hy3', 'gpt-5.1-codex-mini', 'gemini-3.1-flash-lite', 'gemini-2.5-flash', 'minimax-m3', 'gemini-3.0-flash', 'fast-model', 'deepseek-v3-2-volc', 'balanced-model', 'kimi-k2.5', 'kimi-k2.6', 'default-model-lite', 'glm-5.3', 'glm-5.2', 'glm-5.0', 'gpt-5.1-codex', 'gemini-2.5-pro', 'gemini-3.5-flash', 'gpt-5.3-codex', 'gemini-3.1-pro', 'gpt-5.6-terra', 'kimi-k3', 'gpt-5.4', 'primary-model', 'default-model', 'deep-model', 'gpt-5.5', 'gpt-5.6-sol', 'gpt-5.6-luna'],
670
+ // WorkBuddy 国内桌面自制目录(product.json:applicationName=WorkBuddy)
671
+ 'workbuddy': ['auto', 'hy4-preview', 'hy3', 'hy3-x', 'deepseek-v4.1-flash', 'glm-5.3', 'glm-5.3-flash', 'glm-5.2', 'glm-5.1', 'glm-5v-turbo', 'minimax-m3', 'minimax-m2.7', 'kimi-k3-1', 'kimi-k2.8-preview', 'kimi-k2.7', 'kimi-k2.6', 'deepseek-v4-pro']
672
+ }
673
+
674
+ // 各后端真正生效的 product 描述文件位置(旧 core v1.6.1 verbatim)。
675
+ const PRODUCT_DESCRIPTOR_ENV = {
676
+ 'codebuddy': { env: 'CODEBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
677
+ 'codebuddy-intl': { env: 'CODEBUDDY_INTL_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
678
+ 'codebuddy-en': { env: 'CODEBUDDY_EN_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy-ai' },
679
+ 'workbuddy': { env: 'WORKBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy' }
680
+ }
681
+
682
+ // 倍率跨命名匹配(账号清单 id → 描述文件 id,旧 core verbatim)。
683
+ const CREDIT_ID_ALIASES = {
684
+ 'kimi-k3-1': ['kimi-k3', 'kimi-k3-ioa'],
685
+ 'hy3-x': ['hy3', 'hy3-ioa'],
686
+ 'default': ['codewise-default-model-v2'],
687
+ 'auto': ['hy3', 'hy3-ioa']
688
+ }
689
+
690
+ const MODEL_DESCRIPTION = 'Optional model id. When unspecified, the user-preferred default model (plugin settings) is used if set, else the CLI default. Two layers decide: the PRODUCT FACE decides whether the id exists in its catalogue, then the ACCOUNT decides whether it is licensed. The free tier (hy3, deepseek-v4.1-flash at x0.00 credits; hy4-preview at x0.29) belongs to the npm CLI faces codebuddy/codebuddy-intl: hy4-preview, hy3, hy3-x, deepseek-v4.1-flash, glm-5.3, glm-5.3-flash, glm-5.2, glm-5.1, glm-5v-turbo, minimax-m3, minimax-m2.7, kimi-k3-1, kimi-k2.8-preview, kimi-k2.7, kimi-k2.6, deepseek-v4-pro. codebuddy-en (WorkBuddyAI) carries NO hy4-preview and NO deepseek-v4.1-flash; its only free model is hy3. International ids (claude-sonnet-5, gemini-3.1-pro, ...) are account-gated. Availability is per ACCOUNT: the CLI rejects an id outside the account catalogue with "400 service info not found" and one inside it but unlicensed with "400 only available for authorized users" (many catalogue flagships such as claude-opus-4.8, gpt-5.4 and gpt-6-astra are in this second group for a domestic account). Run the CLI with a deliberately invalid model id to print "Currently supported models for your account:" and get the authoritative list; the plugin caches that result and the settings panel lists exactly those ids with their credit multiplier (x0.00 = free), read live from the CLI descriptor so it stays in sync with CLI upgrades. Prefer the cheapest model that can do the job, and prefer a free (x0.00) one for routine work. Pass a model only when the task clearly benefits from a specific one.'
691
+
692
+ const BACKEND_ENUM = ['codebuddy', 'codebuddy-intl', 'codebuddy-ioa', 'codebuddy-international', 'codebuddy-en', 'workbuddy-en', 'workbuddy-ai', 'workbuddy']
693
+
694
+ // ── 三工具 schema(旧 preset/codebuddy-first verbatim;契约 §8)────────────────
695
+ const RUN_PARAMS = {
696
+ type: 'object',
697
+ additionalProperties: false,
698
+ required: ['prompt'],
699
+ properties: {
700
+ prompt: { type: 'string', description: 'The full task/instruction for codebuddy. Be complete and self-contained.' },
701
+ backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to dispatch to. codebuddy (default; domestic CodeBuddy npm CLI, product endpoint www.codebuddy.ai) for coding work; codebuddy-intl = CodeBuddy INTERNATIONAL face of the same npm CLI (product.ioa.json catalogue: claude-sonnet-5 / claude-opus-5 / gemini-3.1-pro / gpt-6-astra / hy3-ioa ...; aliases codebuddy-ioa / codebuddy-international) when the account is an international CodeBuddy account; codebuddy-en = WorkBuddy international edition (the WorkBuddyAI desktop bundled CLI at C:\\Program Files\\WorkBuddyAI, product endpoint www.workbuddy.ai; needs codebuddyEnToken unless DSH already holds a workbuddy key) — aliases workbuddy-en / workbuddy-ai also accepted; workbuddy = WorkBuddy domestic desktop CLI (product endpoint copilot.tencent.com, zero config), the office-scenario face: documents/slides/spreadsheets, knowledge-base lookups, image/video generation, WeChat/WeCom replies. New calls without a backend follow the user-preferred default backend (plugin settings). Continuing a session routes back to its owning backend automatically.' },
702
+ mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'auto follows DSH plan state; plan = no writes; accept-edits = allow edits. Default auto.' },
703
+ model: { type: 'string', description: MODEL_DESCRIPTION },
704
+ effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
705
+ maxTurns: { type: 'integer', description: 'Optional max agentic turns (1-500, default unlimited).' },
706
+ cwd: { type: 'string', description: 'Working directory for codebuddy. Defaults to the DSH workspace root.' },
707
+ addDirs: { type: 'array', items: { type: 'string' }, description: 'Extra directories to add to codebuddy workspace.' },
708
+ timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
709
+ background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
710
+ }
711
+ }
712
+ const CONTINUE_PARAMS = {
713
+ type: 'object',
714
+ additionalProperties: false,
715
+ required: ['prompt'],
716
+ properties: {
717
+ prompt: { type: 'string', description: 'Follow-up instruction for the ongoing codebuddy conversation.' },
718
+ sessionId: { type: 'string', description: 'codebuddy session id to resume (from a prior codebuddy_run result).' },
719
+ latest: { type: 'boolean', description: 'Continue the most recent codebuddy conversation instead of a specific id.' },
720
+ backend: { type: 'string', enum: BACKEND_ENUM, description: 'Which CLI face to resume on. codebuddy-en = WorkBuddy international (WorkBuddyAI desktop CLI; aliases workbuddy-en / workbuddy-ai accepted), workbuddy = WorkBuddy domestic desktop CLI. When omitted, the backend that owns the sessionId is used automatically; brand-new conversations follow the user-preferred default backend (plugin settings).' },
721
+ mode: { type: 'string', enum: ['auto', 'plan', 'accept-edits'], description: 'Execution mode; default auto.' },
722
+ model: { type: 'string', description: 'Optional model id (two layers: product face carries it, account licenses it; the credit multiplier of each id comes from the CLI descriptor — see codebuddy_run).' },
723
+ effort: { type: 'string', enum: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'], description: 'Optional reasoning effort.' },
724
+ maxTurns: { type: 'integer', description: 'Optional max agentic turns.' },
725
+ cwd: { type: 'string', description: "Working directory for codebuddy; when resuming, defaults to the resumed session's project directory." },
726
+ timeoutSec: { type: 'integer', description: 'Run timeout seconds (10-3600, default 300); a DSH-side hang guard force-terminates at timeout+60s.' },
727
+ background: { type: 'boolean', description: 'Run as a background job and return a jobId immediately.' }
728
+ }
729
+ }
730
+ const STATUS_PARAMS = { type: 'object', additionalProperties: false, required: [], properties: { cwd: { type: 'string', description: 'Optional: filter the snapshot to a single project (working directory).' } } }
731
+
732
+ function defineProvider (deps = {}) {
733
+ const env = deps.env || {}
734
+ let host = null // attach 后保存 hostOpts(preset/dynamic/MCP 形态宿主能力)
735
+
736
+ // 宿主 fs/path 访问的统一入口:deps 注入优先;dynamic 内联体(无 import,运行在
737
+ // 后端 Node 上下文)回退到 getBuiltinModule —— 与旧 core builtinModule() 的
738
+ // typeof 守卫模式一致(这是旧 dynamic 端点修复生效的同一条通道,缺了它默认
739
+ // 后端会回到 401 老病)。两者皆不可用时自然降级:读不到文件 = 静态表兜底。
740
+ function builtinModule(name) {
741
+ try {
742
+ const proc = globalThis.process
743
+ return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
744
+ } catch (e) { return null }
745
+ }
746
+ function fsRead(p) {
747
+ const nf = deps.nodeFs || builtinModule('node:fs')
748
+ if (!nf || typeof nf.readFileSync !== 'function') return null
749
+ try { return nf.readFileSync(p, 'utf8') } catch (e) { return null }
750
+ }
751
+ function pathJoin(...parts) {
752
+ const np = deps.nodePath || builtinModule('node:path')
753
+ if (np && typeof np.join === 'function') return np.join(...parts)
754
+ return parts.join('/')
755
+ }
756
+ const pathSep = (deps.nodePath && deps.nodePath.sep) || (/[A-Za-z]:[\\/]/.test(env.USERPROFILE || env.APPDATA || '') ? '\\' : '/')
757
+
758
+ // ── 端点/登录域(旧 core endpointHost verbatim:非法 URL 容错)───────────────
759
+ function endpointHost(url) {
760
+ const s = String(url || '').trim()
761
+ if (!s) return ''
762
+ try { return new URL(s).hostname.toLowerCase() } catch (e) { }
763
+ const m = /^[a-z]+:\/\/([^/?#]+)/i.exec(s)
764
+ return m ? m[1].toLowerCase() : ''
765
+ }
766
+
767
+ function resolveEndpoint(explicitBaseUrl, authDomain) {
768
+ const explicit = String(explicitBaseUrl || '').trim()
769
+ if (explicit) return explicit
770
+ const d = String(authDomain || '').trim().toLowerCase()
771
+ if (d && AUTH_DOMAIN_ENDPOINTS[d]) return AUTH_DOMAIN_ENDPOINTS[d]
772
+ return null
773
+ }
774
+
775
+ function isBackend(v) { return BACKENDS.indexOf(v) >= 0 }
776
+
777
+ // 后端名规范化(v1.3.3 verbatim):别名 + 大小写不敏感;非后端名原样返回。
778
+ function normalizeBackend(v) {
779
+ if (typeof v !== 'string') return v
780
+ const lower = v.trim().toLowerCase()
781
+ const aliased = BACKEND_ALIASES[lower] || lower
782
+ return BACKENDS.indexOf(aliased) >= 0 ? aliased : v
783
+ }
784
+
785
+ // 后端解析优先级(旧 core verbatim):显式 > 会话归属 > 用户偏好 > 默认。
786
+ function resolveBackend(args, target, preferredBackend) {
787
+ if (args && isBackend(normalizeBackend(args.backend))) return normalizeBackend(args.backend)
788
+ if (target && target.backend && isBackend(normalizeBackend(target.backend))) return normalizeBackend(target.backend)
789
+ if (preferredBackend && isBackend(normalizeBackend(preferredBackend))) return normalizeBackend(preferredBackend)
790
+ return DEFAULT_BACKEND
791
+ }
792
+
793
+ // 本次调用的端点/凭据 env 注入(旧 core endpointEnv verbatim 三规则)。
794
+ function endpointEnv(backend, explicitBaseUrl, authDomain, authToken) {
795
+ const out = {}
796
+ const endpoint = resolveEndpoint(explicitBaseUrl, authDomain)
797
+ const product = BACKEND_ENDPOINTS[backend]
798
+ if (endpoint && endpointHost(endpoint) !== endpointHost(product)) {
799
+ out.CODEBUDDY_BASE_URL = endpoint
800
+ }
801
+ if (backend === 'codebuddy-en') {
802
+ const tok = String(authToken || '').trim()
803
+ if (tok) out.CODEBUDDY_AUTH_TOKEN = tok
804
+ }
805
+ return Object.keys(out).length ? out : null
806
+ }
807
+
808
+ // 可操作提示(旧 core endpointMismatchHint 三分支 verbatim)。
809
+ function endpointMismatchHint(backend, authDomain, hasToken) {
810
+ if (backend === 'codebuddy-en' && !hasToken) {
811
+ return 'codebuddy-en(WorkBuddy 国际版)没有可用凭据。国际版的登录 token 被桌面 App 的 protector key 封装,该密钥不落盘、headless CLI 无法自行读取(CLI 自身报 category:"missing-key")。桥接会依次尝试:插件设置 codebuddyEnToken → 环境变量 CODEBUDDY_AUTH_TOKEN → DSH 凭据库(.credentials.yaml / .env 里的 WORKBUDDY_TOKEN)——三者皆空。请在 DSH 里配好 workbuddy provider 的 key,或把国际版 token 填入插件设置;也可直接用 backend="workbuddy"(国内版桌面 CLI,免配置)。'
812
+ }
813
+ if (backend === 'codebuddy-intl') {
814
+ return 'codebuddy-intl(CodeBuddy 国际版)使用同一个 npm CLI,但国际面模型由服务端按账号授权放行:未授权时 CLI 返回 400「model [...] is only available for authorized users」。若你用的是国内账号,请改用 backend="codebuddy"(国内面模型清单)或 backend="codebuddy-en"(WorkBuddy 国际版)。'
815
+ }
816
+ const d = String(authDomain || '').trim().toLowerCase()
817
+ if (d && endpointHost(resolveEndpoint(null, d)) !== endpointHost(BACKEND_ENDPOINTS[backend])) {
818
+ return backend + ' 的登录域是 ' + d + ',与产品端点 ' + BACKEND_ENDPOINTS[backend] + ' 不一致;桥接层已自动注入 CODEBUDDY_BASE_URL=' + AUTH_DOMAIN_ENDPOINTS[d] + '。若仍 401,请重新登录对应客户端。'
819
+ }
820
+ return null
821
+ }
822
+
823
+ // 从 auth 库读某后端登录域(旧 core readBackendAuthDomain verbatim 语义;fs 经
824
+ // deps 注入。io 可覆盖 readFileSync/dir/env,供测试夹具与 MCP 面注入真实 fs)。
825
+ function readBackendAuthDomain(backend, io) {
826
+ const o = io || {}
827
+ const id = BACKEND_AUTH_IDS[backend]
828
+ if (!id) return null
829
+ try {
830
+ const read = o.readFileSync || fsRead
831
+ if (typeof read !== 'function') return null
832
+ const e = o.env || env
833
+ let dir = o.dir
834
+ if (!dir) {
835
+ dir = e.CODEBUDDY_AUTH_DIR ||
836
+ pathJoin(e.LOCALAPPDATA || 'C:\\Users\\lcl\\AppData\\Local', 'CodeBuddyExtension', 'Data', 'Public', 'auth')
837
+ }
838
+ const sep = String(dir).indexOf('\\') >= 0 ? '\\' : '/'
839
+ const txt = read(dir + sep + id + '.info', 'utf8')
840
+ if (txt == null) return null
841
+ const raw = JSON.parse(txt)
842
+ const d = raw && raw.auth && raw.auth.domain
843
+ return typeof d === 'string' && d.trim() ? d.trim() : null
844
+ } catch (e) { return null }
845
+ }
846
+
847
+ // DSH 凭据库第三通道(旧 core readDshWorkbuddyToken verbatim 语义;CODEBUDDY_
848
+ // CREDENTIALS_DIR 覆盖测试夹具;fs 经 deps 注入)。
849
+ function readDshWorkbuddyToken() {
850
+ const overrideDir = env.CODEBUDDY_CREDENTIALS_DIR || null
851
+ const home = overrideDir || env.DSH_HOME || null
852
+ if (!home) return null
853
+ const files = [home + '\\.credentials.yaml', home + '\\.env', home + '/.credentials.yaml', home + '/.env']
854
+ for (const f of files) {
855
+ const text = fsRead(f)
856
+ if (!text) continue
857
+ for (const key of CREDENTIAL_KEYS) {
858
+ const m = text.match(new RegExp('^[ \\t]*' + key + '[ \\t]*[:=][ \\t]*(.+)$', 'm'))
859
+ if (!m) continue
860
+ const v = m[1].trim().replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '').trim()
861
+ // 只接受看起来是真实 token 的值(避免把 $VAR 引用或占位符当凭据)。
862
+ if (v.length >= 40 && !v.startsWith('$') && !/^<.*>$/.test(v)) return v
863
+ }
864
+ }
865
+ return null
866
+ }
867
+
868
+ // codebuddy-en 凭据链(旧 core verbatim):设置面板 > CODEBUDDY_AUTH_TOKEN >
869
+ // DSH 凭据库。dynamic 沙箱无 fs → 第三通道自然缺席(旧 dynamic 本也有限)。
870
+ function resolveEnToken(settingToken) {
871
+ const s = String(settingToken || '').trim()
872
+ if (s) return { token: s, source: 'setting' }
873
+ const envTok = env.CODEBUDDY_AUTH_TOKEN || ''
874
+ if (String(envTok).trim()) return { token: String(envTok).trim(), source: 'env' }
875
+ const fromStore = readDshWorkbuddyToken()
876
+ if (fromStore) return { token: fromStore, source: 'dsh-store' }
877
+ return { token: null, source: null }
878
+ }
879
+
880
+ // ── 模型目录(面板/诊断用;无促销角标版,见头注 TODO)───────────────────────
881
+ function accountConfigCandidates(backend) {
882
+ const spec = PRODUCT_DESCRIPTOR_ENV[backend]
883
+ if (!spec || !spec.accountDir) return []
884
+ const home = env.USERPROFILE || env.HOME
885
+ if (!home) return []
886
+ const base = pathJoin(home, spec.accountDir.split('/').join(pathSep))
887
+ return [
888
+ pathJoin(base, 'cache', 'acc-product-config-v3.json'),
889
+ pathJoin(base, 'local_storage', 'entry_d43e96994f944cfb77961c2ea7d04605.info'),
890
+ pathJoin(base, 'local_storage', 'wb_entry_d43e96994f944cfb77961c2ea7d04605.info')
891
+ ]
892
+ }
893
+
894
+ function productDescriptorCandidates(backend) {
895
+ const spec = PRODUCT_DESCRIPTOR_ENV[backend]
896
+ if (!spec) return []
897
+ const out = []
898
+ for (const p of accountConfigCandidates(backend)) out.push(p)
899
+ if (env[spec.env]) out.push(env[spec.env])
900
+ const rels = spec.fallbackRel ? [spec.rel, spec.fallbackRel] : [spec.rel]
901
+ const push = (root) => { if (root) for (const r of rels) out.push(pathJoin(root, ...r.split('/'))) }
902
+ if (backend === 'codebuddy' || backend === 'codebuddy-intl') {
903
+ push(env.APPDATA ? pathJoin(env.APPDATA, 'npm', 'node_modules', '@tencent-ai', 'codebuddy-code') : null)
904
+ } else if (backend === 'codebuddy-en') {
905
+ push('C:\\Program Files\\WorkBuddyAI\\resources\\app.asar.unpacked\\cli')
906
+ } else if (backend === 'workbuddy') {
907
+ push('C:\\Program Files\\WorkBuddy\\resources\\app.asar.unpacked\\cli')
908
+ }
909
+ return out
910
+ }
911
+
912
+ function normalizeProductConfig(raw) {
913
+ try {
914
+ const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
915
+ if (!obj) return null
916
+ if (Array.isArray(obj)) {
917
+ // 数组形态:从后往前找第一个带 data 的条目(末尾最新)。
918
+ for (let i = obj.length - 1; i >= 0; i--) {
919
+ const e = obj[i]
920
+ if (e && typeof e === 'object' && e.data && typeof e.data === 'object') return e.data
921
+ }
922
+ return null
923
+ }
924
+ if (typeof obj !== 'object') return null
925
+ if (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.models) && !Array.isArray(obj.agents)) return obj.data
926
+ return obj
927
+ } catch (e) { return null }
928
+ }
929
+
930
+ function extractModelIds(productJson) {
931
+ try {
932
+ const obj = normalizeProductConfig(productJson)
933
+ if (!obj || typeof obj !== 'object') return null
934
+ const pick = (arr) => {
935
+ if (!Array.isArray(arr) || !arr.length) return null
936
+ const ids = arr.map((m) => (typeof m === 'string' ? m : m && m.id)).filter((s) => typeof s === 'string' && s.trim())
937
+ return ids.length ? [...new Set(ids)] : null
938
+ }
939
+ const agents = Array.isArray(obj.agents) ? obj.agents : []
940
+ const cli = agents.find((a) => a && a.name === 'cli')
941
+ const fromCli = cli ? pick(cli.models) : null
942
+ const fromTop = pick(obj.models)
943
+ if (fromCli && fromTop) return fromTop.length > fromCli.length ? fromTop : fromCli
944
+ return fromCli || fromTop || null
945
+ } catch (e) { return null }
946
+ }
947
+
948
+ function parseCreditValue(raw) {
949
+ if (typeof raw !== 'string') return null
950
+ const s = raw.trim()
951
+ let m = s.match(/^[xX×]\s*([0-9]+(?:\.[0-9]+)?)/)
952
+ if (!m) m = s.match(/^([0-9]+(?:\.[0-9]+)?)\s*[xX×]/)
953
+ if (!m) return null
954
+ const n = Number(m[1])
955
+ return Number.isFinite(n) ? n : null
956
+ }
957
+
958
+ function formatCreditLabel(credits) {
959
+ if (typeof credits !== 'number' || !Number.isFinite(credits)) return ''
960
+ if (credits === 0) return '免费'
961
+ return `x${credits.toFixed(2)}`
962
+ }
963
+
964
+ function extractModelCredits(productJson) {
965
+ try {
966
+ const obj = normalizeProductConfig(productJson)
967
+ if (!obj || typeof obj !== 'object' || !Array.isArray(obj.models)) return null
968
+ const map = new Map()
969
+ for (const m of obj.models) {
970
+ if (!m || typeof m !== 'object') continue
971
+ const id = typeof m.id === 'string' ? m.id.trim() : ''
972
+ if (!id) continue
973
+ const n = parseCreditValue(m.credits)
974
+ if (n !== null) map.set(id, n)
975
+ }
976
+ return map.size ? map : null
977
+ } catch (e) { return null }
978
+ }
979
+
980
+ function lookupModelCredits(creditsMap, id) {
981
+ if (!creditsMap || typeof id !== 'string') return null
982
+ const norm = id.trim()
983
+ const cands = [norm]
984
+ if (norm.endsWith('-ioa')) cands.push(norm.slice(0, -4))
985
+ else cands.push(norm + '-ioa')
986
+ if (CREDIT_ID_ALIASES[norm]) cands.push(...CREDIT_ID_ALIASES[norm])
987
+ for (const c of cands) {
988
+ if (creditsMap.has(c)) {
989
+ const v = creditsMap.get(c)
990
+ if (typeof v === 'number') return v
991
+ }
992
+ }
993
+ return null
994
+ }
995
+
996
+ function backendModelCatalog(backend, io) {
997
+ const known = BACKEND_MODEL_IDS[backend] || []
998
+ const read = (io && io.readFileSync) || fsRead
999
+ let fromFile = null
1000
+ let fromFileRich = null
1001
+ // 旧 v1.6.1 语义:第一份有内容的 + 第一份带促销的,优先后者。
1002
+ for (const p of productDescriptorCandidates(backend)) {
1003
+ let txt = null
1004
+ try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
1005
+ if (txt == null) continue
1006
+ const ids = extractModelIds(txt)
1007
+ if (ids && ids.length) {
1008
+ if (!fromFile) fromFile = ids
1009
+ const obj = normalizeProductConfig(txt)
1010
+ if (!fromFileRich && obj && Array.isArray(obj.modelPromotions)) fromFileRich = ids
1011
+ }
1012
+ if (fromFileRich) break
1013
+ }
1014
+ if (!fromFile) return known
1015
+ return fromFileRich || fromFile
1016
+ }
1017
+
1018
+ // 服务端回吐的账号清单缓存(进程内,旧 core verbatim)。
1019
+ const ACCOUNT_CATALOG_CACHE = new Map()
1020
+ function rememberAccountCatalog(backend, ids) {
1021
+ if (Array.isArray(ids) && ids.length) ACCOUNT_CATALOG_CACHE.set(backend, ids.slice())
1022
+ }
1023
+ function getAccountCatalog(backend) {
1024
+ const v = ACCOUNT_CATALOG_CACHE.get(backend)
1025
+ return v && v.length ? v.slice() : null
1026
+ }
1027
+
1028
+ // CLI 报错里解析「该账号当前支持的模型」(旧 core parseAccountModels verbatim)。
1029
+ function parseAccountModels(text) {
1030
+ const s = String(text || '')
1031
+ const anchor = s.indexOf('Currently supported models for your account')
1032
+ if (anchor < 0) return null
1033
+ const tail = s.slice(anchor)
1034
+ const ids = []
1035
+ for (const line of tail.split(/\r?\n/)) {
1036
+ const m = /^\s*[-*]\s+(\S+)\s*$/.exec(line)
1037
+ if (!m) {
1038
+ if (ids.length) break
1039
+ continue
1040
+ }
1041
+ if (!ids.includes(m[1])) ids.push(m[1])
1042
+ }
1043
+ return ids.length ? ids : null
1044
+ }
1045
+
1046
+ // 带倍率/授权的详细清单(无促销角标,旧 v1.6.1 的 0.1.0 子集;排序规则 verbatim:
1047
+ // 已授权在前 → 有倍率升序 → 无倍率殿后)。
1048
+ function modelCatalogDetailed(backend, io) {
1049
+ const o = io || {}
1050
+ const fromFile = backendModelCatalog(backend, o)
1051
+ const acct = (Array.isArray(o.accountModels) && o.accountModels.length) ? o.accountModels : getAccountCatalog(backend)
1052
+ const ids = []
1053
+ const authorized = new Set()
1054
+ if (acct) for (const id of acct) { if (!ids.includes(id)) ids.push(id); authorized.add(id) }
1055
+ for (const id of fromFile) if (!ids.includes(id)) ids.push(id)
1056
+ const read = o.readFileSync || fsRead
1057
+ let creditsMap = null
1058
+ for (const p of productDescriptorCandidates(backend)) {
1059
+ let txt = null
1060
+ try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
1061
+ if (txt == null) continue
1062
+ const map = extractModelCredits(txt)
1063
+ if (map) { creditsMap = map; break }
1064
+ }
1065
+ const items = ids.map((id) => {
1066
+ const base = lookupModelCredits(creditsMap, id)
1067
+ const credits = base
1068
+ const hasCredits = typeof credits === 'number'
1069
+ const suffix = formatCreditLabel(credits)
1070
+ const marks = []
1071
+ if (suffix) marks.push(suffix)
1072
+ if (acct && !authorized.has(id)) marks.push('未授权')
1073
+ return {
1074
+ id,
1075
+ credits,
1076
+ baseCredits: base,
1077
+ free: hasCredits && credits === 0,
1078
+ hasCredits,
1079
+ authorized: acct ? authorized.has(id) : null,
1080
+ promotion: null,
1081
+ label: marks.length ? `${id} · ${marks.join(' · ')}` : id
1082
+ }
1083
+ })
1084
+ const rank = (x) => (x.authorized === false ? 1 : 0)
1085
+ const withC = items.filter((x) => x.hasCredits).sort((a, b) => rank(a) - rank(b) || a.credits - b.credits)
1086
+ const withoutC = items.filter((x) => !x.hasCredits).sort((a, b) => rank(a) - rank(b))
1087
+ return [...withC, ...withoutC]
1088
+ }
1089
+
1090
+ function formatModelCatalogText(backend, io) {
1091
+ return modelCatalogDetailed(backend, io).map((x) => x.label).join(', ')
1092
+ }
1093
+
1094
+ // ── 设置清洗(旧 normalizeBridgeSettings verbatim;updatedAt 透传)───────────
1095
+ function normalizeSettings(raw) {
1096
+ const snap = { preferredBackend: DEFAULT_BACKEND, defaultModel: '', codebuddyEnToken: '', endpointOverride: '' }
1097
+ if (!raw || typeof raw !== 'object') return snap
1098
+ if (typeof raw.preferredBackend === 'string') {
1099
+ const nb = normalizeBackend(raw.preferredBackend)
1100
+ if (typeof nb === 'string' && BACKENDS.indexOf(nb) >= 0) snap.preferredBackend = nb
1101
+ }
1102
+ if (typeof raw.defaultModel === 'string') snap.defaultModel = raw.defaultModel.trim()
1103
+ if (typeof raw.codebuddyEnToken === 'string') snap.codebuddyEnToken = raw.codebuddyEnToken.trim()
1104
+ if (typeof raw.endpointOverride === 'string') snap.endpointOverride = raw.endpointOverride.trim()
1105
+ if (typeof raw.updatedAt === 'number' && raw.updatedAt > 0) snap.updatedAt = raw.updatedAt
1106
+ return snap
1107
+ }
1108
+
1109
+ // 诊断(旧 diagnoseBackend verbatim;fs/env 走 deps)。
1110
+ function diagnoseBackend(backend, settings, io) {
1111
+ const o = io || {}
1112
+ const e = o.env || env
1113
+ const s = settings || normalizeSettings(null)
1114
+ const authDomain = readBackendAuthDomain(backend, Object.assign({ env: e }, o))
1115
+ const override = String(s.endpointOverride || '').trim()
1116
+ const byDomain = resolveEndpoint(null, authDomain)
1117
+ const endpoint = override || byDomain || BACKEND_ENDPOINTS[backend] || null
1118
+ const endpointSource = override ? 'override' : (byDomain ? 'auth-domain' : 'product')
1119
+ const tok = resolveEnToken(backend === 'codebuddy-en' ? s.codebuddyEnToken : null)
1120
+ return {
1121
+ backend: backend,
1122
+ authDomain: authDomain,
1123
+ endpoint: endpoint,
1124
+ endpointSource: endpointSource,
1125
+ tokenSource: backend === 'codebuddy-en' ? tok.source : 'not-needed',
1126
+ tokenHint: tok.source === 'setting' ? '来自设置面板'
1127
+ : tok.source === 'env' ? '来自环境变量 CODEBUDDY_AUTH_TOKEN'
1128
+ : tok.source === 'dsh-store' ? '来自 DSH 凭据库(自动复用 WORKBUDDY_TOKEN)'
1129
+ : backend === 'codebuddy-en' ? '缺失:国际版 token 不落盘,需在设置面板填写或先在 DSH 配 workbuddy provider'
1130
+ : '无需填写(国内版由 CLI 自行登录)',
1131
+ mismatch: endpointMismatchHint(backend, authDomain, !!tok.token) !== null,
1132
+ hint: endpointMismatchHint(backend, authDomain, !!tok.token) || null,
1133
+ defaultModel: String(s.defaultModel || '').trim() || '(CLI 默认)'
1134
+ }
1135
+ }
1136
+
1137
+ // ── 结果判定(旧 isLimited/isTransientCliError/failureHint verbatim)─────────
1138
+ function isLimitedCodebuddy(res) {
1139
+ if (!res || res.ok) return false
1140
+ // 前置凭据缺失:配置问题,不是限流(弹窗重试不会让 token 出现)。
1141
+ if (res.status === 'AUTH_REQUIRED') return false
1142
+ if (res.status === 'SPAWN_ERROR' || res.status === 'CODEBUDDY_UNAVAILABLE' || res.status === 'HUNG_TIMEOUT') return true
1143
+ const hay = String(res.stderr || '') + ' ' + String(res.status || '')
1144
+ // 401/端点域不匹配:需要用户换登录域,不归限流类(旧 v1.3.0 verbatim)。
1145
+ if (/401 authentication failed|differs from the current product endpoint/i.test(hay)) return false
1146
+ return CODEBUDDY_LIMIT_RE.test(hay)
1147
+ }
1148
+
1149
+ function isTransientCliError(res) {
1150
+ if (!res || res.ok) return false
1151
+ if (isLimitedCodebuddy(res)) return false
1152
+ return res.status === 'ERROR_DURING_EXECUTION'
1153
+ }
1154
+
1155
+ function failureHint(res) {
1156
+ if (!res || res.ok) return ''
1157
+ // AUTH_REQUIRED:stderr 已带完整指引,透传(旧 v1.3.1 verbatim)。
1158
+ if (res.status === 'AUTH_REQUIRED') return '[诊断] ' + String(res.stderr || '')
1159
+ if (isTransientCliError(res)) {
1160
+ return res.retried
1161
+ ? '[诊断] codebuddy CLI 侧瞬时错误(error_during_execution,CLI 未给出原因),已自动重试 1 次仍失败。可换 model 再试一次;若仍失败请改用原生工具完成,或告知用户。'
1162
+ : '[诊断] codebuddy CLI 侧瞬时错误(error_during_execution,CLI 未给出原因)。直接重试同一请求通常即可通过;也可显式指定 model。不要据此认为任务本身有问题。'
1163
+ }
1164
+ if (res.status === 'PARSE_ERROR') {
1165
+ return '[诊断] 未能从 codebuddy 输出解析出 result 事件(进程可能被中断或输出被截断)。可重试;持续出现请检查 codebuddy CLI 版本。'
1166
+ }
1167
+ if (/401 authentication failed|differs from the current product endpoint/i.test(String(res.stderr || ''))) {
1168
+ return '[诊断] 认证失败(401):请求被送到的端点与当前 CLI 登录域不匹配(国际/国内登录互斥)。请让用户在对应产品域完成 `codebuddy` CLI 登录后重试;这不是限流,重试与回退都无效。'
1169
+ }
1170
+ return ''
1171
+ }
1172
+
1173
+ // stream-json 解析(旧 parseCodebuddyJson verbatim)。
1174
+ function parseCodebuddyJson(stdoutText) {
1175
+ const trimmed = String(stdoutText || '').trim()
1176
+ if (!trimmed) return null
1177
+ const lines = trimmed.split(/\r?\n/).filter(Boolean)
1178
+ for (let i = lines.length - 1; i >= 0; i--) {
1179
+ const ln = lines[i].trim()
1180
+ if (!ln.startsWith('{')) continue
1181
+ try {
1182
+ const obj = JSON.parse(ln)
1183
+ if (obj && obj.type === 'result') return obj
1184
+ } catch (e) {}
1185
+ }
1186
+ try { return JSON.parse(trimmed) } catch (e) {}
1187
+ return null
1188
+ }
1189
+
1190
+ // result 事件 → 统一结果对象(旧 buildResult verbatim,含账号清单收割)。
1191
+ function buildResult(parsed, outcome, mode, stderrText, stdoutText, backend) {
1192
+ const bk = backend || 'codebuddy'
1193
+ const exitCode = outcome ? outcome.exitCode : null
1194
+ const errText = parsed && typeof parsed.error === 'string' && parsed.error ? parsed.error : ''
1195
+ const stderr = (stderrText ? String(stderrText).slice(-2000) : '') + (errText ? (stderrText ? ' ' : '') + errText : '')
1196
+ // v1.6.0:顺手收割服务端回吐的「本账号可用模型」清单(失败静默)。
1197
+ try {
1198
+ const harvest = parseAccountModels(stderr) || parseAccountModels(String(stdoutText || ''))
1199
+ if (harvest && harvest.length) rememberAccountCatalog(bk, harvest)
1200
+ } catch (e) { /* 收割失败不影响主流程 */ }
1201
+ if (parsed && parsed.type === 'result') {
1202
+ const isOk = exitCode === 0 && parsed.is_error === false && parsed.subtype === 'success'
1203
+ let modelUsed = null
1204
+ try {
1205
+ if (parsed.modelUsage && typeof parsed.modelUsage === 'object') {
1206
+ const names = Object.keys(parsed.modelUsage)
1207
+ if (names.length) modelUsed = names.join('+')
1208
+ }
1209
+ } catch (e) { }
1210
+ return {
1211
+ ok: isOk,
1212
+ status: String(parsed.subtype || (isOk ? 'SUCCESS' : 'ERROR')).toUpperCase(),
1213
+ response: typeof parsed.result === 'string' ? parsed.result : '',
1214
+ sessionId: typeof parsed.session_id === 'string' ? parsed.session_id : null,
1215
+ durationSeconds: typeof parsed.duration_ms === 'number' ? parsed.duration_ms / 1000 : null,
1216
+ numTurns: typeof parsed.num_turns === 'number' ? parsed.num_turns : null,
1217
+ totalTokens: (parsed.usage && typeof parsed.usage.input_tokens === 'number' && typeof parsed.usage.output_tokens === 'number')
1218
+ ? (parsed.usage.input_tokens + parsed.usage.output_tokens) : null,
1219
+ exitCode: exitCode,
1220
+ mode: mode,
1221
+ backend: bk,
1222
+ modelUsed: modelUsed,
1223
+ stderr: stderr
1224
+ }
1225
+ }
1226
+ return {
1227
+ ok: false,
1228
+ status: 'PARSE_ERROR',
1229
+ response: '',
1230
+ sessionId: null,
1231
+ durationSeconds: null,
1232
+ numTurns: null,
1233
+ totalTokens: null,
1234
+ exitCode: exitCode,
1235
+ mode: mode,
1236
+ backend: bk,
1237
+ stderr: stderr,
1238
+ rawStdout: String(stdoutText || '').slice(-2000)
1239
+ }
1240
+ }
1241
+
1242
+ // 统一 argv 构造(旧 buildArgv verbatim;env 端点/凭据注入在本 provider 内完成,
1243
+ // 因为 kit 只透传 opts,宿主三形态都从本函数的返回值取 built.env 下发 spawn)。
1244
+ function buildArgvCodebuddy(prefix, args, opts) {
1245
+ const o = opts || {}
1246
+ const argv = [prefix[0], ...prefix.slice(1), '-p', String(args.prompt), '--output-format', 'stream-json']
1247
+ let mode = args.mode || o.defaultMode || 'auto'
1248
+ if (mode === 'auto') mode = o.planActive ? 'plan' : 'bypassPermissions'
1249
+ if (mode === 'plan') {
1250
+ argv.push('--permission-mode', 'plan')
1251
+ argv.push('--allowedTools', 'Bash')
1252
+ } else {
1253
+ mode = 'bypassPermissions'
1254
+ argv.push('--permission-mode', 'bypassPermissions')
1255
+ }
1256
+ // 模型优先级:显式 args.model > 用户偏好 defaultModel > CLI 默认。
1257
+ const model = args.model || o.defaultModel
1258
+ if (model) argv.push('--model', String(model))
1259
+ if (args.effort) argv.push('--effort', String(args.effort))
1260
+ if (args.maxTurns !== undefined && args.maxTurns !== null) argv.push('--max-turns', String(clampInt(args.maxTurns, 50, 1, 500)))
1261
+ if (Array.isArray(args.addDirs)) for (const d of args.addDirs) { if (d) argv.push('--add-dir', String(d)) }
1262
+ if (args.sessionId) argv.push('--resume', String(args.sessionId))
1263
+ else if (args.continueLatest) argv.push('--continue')
1264
+ const timeoutSec = clampInt(args.timeoutSec, 300, 10, 3600)
1265
+ // 端点/凭据注入(旧 preset/dynamic/MCP 三形态一致语义,宿主不必自算):
1266
+ // authDomain 只在有 fs 能力的宿主读得到;dynamic 沙箱 → 沿用 CLI 自身端点。
1267
+ const bk = o.backend || DEFAULT_BACKEND
1268
+ const settings = o.settings || null
1269
+ const authDomain = readBackendAuthDomain(bk, {})
1270
+ const tok = resolveEnToken(bk === 'codebuddy-en' && settings ? settings.codebuddyEnToken : null)
1271
+ const inject = endpointEnv(bk, settings ? settings.endpointOverride : null, authDomain, tok.token)
1272
+ return { argv: argv, timeoutSec: timeoutSec, mode: mode, env: o.env || inject }
1273
+ }
1274
+
1275
+ // 流事件折叠(旧 foldEvent 主体 verbatim;project 状态由 kit engine 持有)。
1276
+ function foldStreamEvent(ev, p, summarizeArgs, MAX_ARG_LEN, MAX_TRAIL) {
1277
+ if (!ev || !ev.type) return
1278
+ if (ev.type === 'assistant' && ev.message && Array.isArray(ev.message.content)) {
1279
+ for (const block of ev.message.content) {
1280
+ if (block.type === 'tool_use') {
1281
+ p._stepCounter = (p._stepCounter || 0) + 1
1282
+ const stepIndex = p._stepCounter
1283
+ if (!p._toolMap) p._toolMap = {}
1284
+ p._toolMap[block.id] = { stepIndex: stepIndex, name: block.name }
1285
+ const args = summarizeArgs(block.input)
1286
+ p.trail.push({ stepIndex: stepIndex, state: 'ACTIVE', tool: block.name, args: args, at: new Date().toISOString() })
1287
+ if (p.trail.length > MAX_TRAIL) p.trail.shift()
1288
+ p.current = { tool: block.name, args: args, stepIndex: stepIndex, since: new Date().toISOString() }
1289
+ } else if (block.type === 'thinking') {
1290
+ p.current = { tool: 'thinking', args: { text: String(block.thinking || '').slice(0, MAX_ARG_LEN) }, stepIndex: 0, since: new Date().toISOString() }
1291
+ } else if (block.type === 'text') {
1292
+ p.current = { tool: 'typing', args: { text: String(block.text || '').slice(0, MAX_ARG_LEN) }, stepIndex: 0, since: new Date().toISOString() }
1293
+ }
1294
+ }
1295
+ } else if (ev.type === 'user' && ev.message && Array.isArray(ev.message.content)) {
1296
+ for (const block of ev.message.content) {
1297
+ if (block.type === 'tool_result' && p._toolMap && p._toolMap[block.tool_use_id]) {
1298
+ const rec = p._toolMap[block.tool_use_id]
1299
+ p.trail.push({ stepIndex: rec.stepIndex, state: block.is_error ? 'ERROR' : 'DONE', tool: rec.name, args: null, at: new Date().toISOString() })
1300
+ if (p.trail.length > MAX_TRAIL) p.trail.shift()
1301
+ if (p.current && p.current.stepIndex === rec.stepIndex) p.current = null
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1306
+
1307
+ const descriptor = {
1308
+ id: 'codebuddy',
1309
+ label: 'CB',
1310
+ presetId: 'codebuddy-first',
1311
+ defaultBackend: DEFAULT_BACKEND,
1312
+ defaultMode: 'auto',
1313
+ exeName: 'codebuddy',
1314
+ binEnvVars: ['CODEBUDDY_BIN', 'CODEBUDDY_EN_BIN', 'WORKBUDDY_BIN'],
1315
+ unavailableStatus: 'CODEBUDDY_UNAVAILABLE',
1316
+ // 旧 renderResult verbatim 语义:标头用具体 backend('workbuddy OK'),仅当结果
1317
+ // 无 backend 字段时回落 'codebuddy'(= provider.id)。agy/mimo 旧包标头硬编码
1318
+ // provider 名,不设此旗。
1319
+ backendLabel: true,
1320
+
1321
+ backends: () => BACKENDS.slice(),
1322
+ normalizeBackend,
1323
+ resolveBackend,
1324
+
1325
+ buildArgv: buildArgvCodebuddy,
1326
+ parseJson: parseCodebuddyJson,
1327
+ buildResult,
1328
+ foldStreamEvent,
1329
+ isLimited: isLimitedCodebuddy,
1330
+ isTransientCliError,
1331
+ failureHint,
1332
+
1333
+ // codebuddy-en 无凭据的确定性前置(v1.3.1 语义,经 kit preflight 钩子实现):
1334
+ // 三通道皆空 → 直接 AUTH_REQUIRED(可操作指引进 stderr),不白跑一次 spawn。
1335
+ // 其它后端一律放行。dynamic 沙箱无 fs → dsh-store 通道缺席,abort 提示即可。
1336
+ preflight({ args, backend, engine, o }) {
1337
+ if (backend !== 'codebuddy-en') return null
1338
+ const settings = (o && o.getSettings && o.getSettings()) || null
1339
+ const tok = resolveEnToken(settings ? settings.codebuddyEnToken : null)
1340
+ if (tok.token) return null
1341
+ const authDomain = readBackendAuthDomain('codebuddy-en', {})
1342
+ const hint = endpointMismatchHint('codebuddy-en', authDomain, false)
1343
+ return {
1344
+ abort: {
1345
+ ok: false,
1346
+ status: 'AUTH_REQUIRED',
1347
+ response: '',
1348
+ sessionId: null,
1349
+ durationSeconds: null,
1350
+ numTurns: null,
1351
+ totalTokens: null,
1352
+ exitCode: null,
1353
+ mode: (args && args.mode) || descriptor.defaultMode || 'auto',
1354
+ backend: 'codebuddy-en',
1355
+ stderr: hint
1356
+ }
1357
+ }
1358
+ },
1359
+
1360
+ tools: {
1361
+ run: {
1362
+ name: 'codebuddy_run',
1363
+ description: 'Dispatch a coding/build/debug/investigation task to the local codebuddy agent CLI and return its final answer. Prefer this for implementation, edits, refactors, multi-file investigation and debugging in every mode. DSH fully controls codebuddy: it always runs non-interactively with permissions auto-approved (codebuddy never prompts). Use read-only native tools only for quick lookups and for final build/test verification. In mode=auto the DSH plan state decides plan vs bypassPermissions. Set background=true for long tasks and collect the result via job_output.',
1364
+ inputSchema: RUN_PARAMS
1365
+ },
1366
+ continue: {
1367
+ name: 'codebuddy_continue',
1368
+ description: 'Continue an existing codebuddy conversation with a follow-up prompt, reusing codebuddy context. Pass sessionId from a prior codebuddy_run result, or set latest=true to continue the most recent codebuddy conversation. Same DSH-controlled, no-prompt execution as codebuddy_run.',
1369
+ inputSchema: CONTINUE_PARAMS
1370
+ },
1371
+ status: {
1372
+ name: 'codebuddy_status',
1373
+ description: 'Read a live snapshot of what the local codebuddy agent is currently doing. Returns one section per project (working directory): running count, the current step (tool name + arguments being executed, or agent_response thinking/typing), the recent step trail (tools executed, done/error), the last completed run status + session id, and per-project cumulative usage (runs + total tokens, since codebuddy exposes no quota API). Optional cwd filters to a single project. Call this to check on an in-flight codebuddy_run/codebuddy_continue without waiting for it to finish.',
1374
+ inputSchema: STATUS_PARAMS
1375
+ }
1376
+ },
1377
+
1378
+ extraTools: [],
1379
+
1380
+ // 设置分区(契约 §3):keystone 统一文件 {codebuddy:{...}};旧扁平文件的字段
1381
+ // 级迁移由 keystone LEGACY_SOURCES 承担(字段名逐字相同)。
1382
+ settings: {
1383
+ keys: SETTINGS_KEYS,
1384
+ secretKeys: ['codebuddyEnToken'],
1385
+ defaults: () => normalizeSettings(null),
1386
+ normalize: (raw) => normalizeSettings(raw),
1387
+ meta() {
1388
+ // defaultModel 下拉 = 默认后端(codebuddy 面)的带倍率目录(升级 CLI 后
1389
+ // 自动同步);其余三面目录不同,字段允许自定义输入即可覆盖。促销角标为
1390
+ // 0.1.0 有意省略(见头注 TODO)。
1391
+ const options = modelCatalogDetailed(DEFAULT_BACKEND, {}).map((x) => ({ value: x.id, label: x.label }))
1392
+ return {
1393
+ titleSuffix: 'CodeBuddy',
1394
+ fields: [
1395
+ {
1396
+ key: 'preferredBackend', label: '默认后端', type: 'select',
1397
+ options: BACKENDS.map((b) => ({ value: b, label: BACKEND_LABELS[b] })),
1398
+ note: '新会话未显式指定 backend 时用这里选的后端;单次调用可覆盖。codebuddy-en 需要 codebuddyEnToken 或 DSH 已配 workbuddy key。'
1399
+ },
1400
+ {
1401
+ key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true, placeholder: '(CLI 默认)',
1402
+ options,
1403
+ note: '账号实际可用性以服务端实时裁决(400 报文回吐权威清单,桥接自动缓存)。优先选能完成任务里最便宜的,日常任务优先免费(x0.00)。'
1404
+ },
1405
+ { key: 'codebuddyEnToken', label: 'WorkBuddy 国际版 Token', type: 'secret', note: '仅 backend="codebuddy-en" 需要。国际版登录 token 被 protector key 封装不落盘,只能手工提供;留空则自动尝试 DSH 凭据库的 WORKBUDDY_TOKEN。' },
1406
+ { key: 'endpointOverride', label: '端点覆盖', type: 'text', placeholder: 'https://…/v2', note: '一般留空:端点按登录域自动对齐(www.codebuddy.cn 等)。仅在需要强制指定 API 端点时填写,且必须自带 /v2。' }
1407
+ ]
1408
+ }
1409
+ },
1410
+ diagnose(snap) {
1411
+ const s = normalizeSettings(snap)
1412
+ const lines = []
1413
+ const seen = new Set()
1414
+ // 默认后端一行;codebuddy-en 是 4 面里唯一需要凭据的,恒附一行状态。
1415
+ for (const b of [s.preferredBackend, 'codebuddy-en']) {
1416
+ if (seen.has(b)) continue
1417
+ seen.add(b)
1418
+ const d = diagnoseBackend(b, s, {})
1419
+ lines.push(b + ': ' + (d.endpoint || '?') + '(登录域 ' + (d.authDomain || '未读到') + ',' + d.tokenHint + ')')
1420
+ if (d.hint) lines.push(' ⚠ ' + d.hint)
1421
+ }
1422
+ return lines.join('\n')
1423
+ }
1424
+ },
1425
+
1426
+ mcp: { version: '0.2.2' }
1427
+ }
1428
+
1429
+ // 三形态共用的工具逻辑(签名保持 keystone/队友接线契约不变)。
1430
+ descriptor.runToolLogic = async function (args, exec) {
1431
+ return this.runner.coreExecute(args, exec)
1432
+ }
1433
+ // 旧 preset continueTool.execute verbatim 映射(sessionId 优先,否则 latest)。
1434
+ descriptor.continueToolLogic = async function (args, exec) {
1435
+ const a = args || {}
1436
+ const mapped = { prompt: a.prompt, backend: a.backend, mode: a.mode, model: a.model, effort: a.effort, maxTurns: a.maxTurns, cwd: a.cwd, timeoutSec: a.timeoutSec, background: a.background }
1437
+ if (a.sessionId) mapped.sessionId = a.sessionId
1438
+ else if (a.latest) mapped.continueLatest = true
1439
+ return this.runner.coreExecute(mapped, exec)
1440
+ }
1441
+ // 旧 statusTool.execute verbatim:cwd 过滤 + 单项目聚合覆写(含 runs/totalTokens)。
1442
+ descriptor.statusToolLogic = function (args) {
1443
+ const a = args || {}
1444
+ const snap = this.engine.statusSnapshot()
1445
+ if (a.cwd) {
1446
+ const key = String(a.cwd)
1447
+ snap.projects = snap.projects.filter((p) => p.cwd === key)
1448
+ const g = snap.projects[0]
1449
+ if (g) { snap.state = g.state; snap.running = g.running; snap.current = g.current; snap.trail = g.trail; snap.lastStatus = g.lastStatus; snap.lastAt = g.lastAt; snap.lastSessionId = g.lastSessionId; snap.lastBackend = g.lastBackend; snap.fallbackActive = g.fallbackActive; snap.runs = g.runs; snap.totalTokens = g.totalTokens; snap.updatedAt = g.updatedAt }
1450
+ }
1451
+ return snap
1452
+ }
1453
+
1454
+ // 供 preset/dynamic/MCP/测试形态复用的领域函数(不 import home-plugin,全部经
1455
+ // 描述符暴露;MCP 面据 buildArgv/parseJson/buildResult/isLimited/failureHint 组
1456
+ // 装真实 spawn 执行,语义与旧 mcp/codebuddy-mcp-server.mjs 一致)。
1457
+ descriptor.policyText = POLICY_TEXT
1458
+ descriptor.limits = { LIMIT_RE: CODEBUDDY_LIMIT_RE }
1459
+ descriptor.backendLabels = BACKEND_LABELS
1460
+ descriptor.backendEndpoints = BACKEND_ENDPOINTS
1461
+ descriptor.authDomainEndpoints = AUTH_DOMAIN_ENDPOINTS
1462
+ descriptor.backendAuthIds = BACKEND_AUTH_IDS
1463
+ descriptor.staticModelIds = BACKEND_MODEL_IDS
1464
+ descriptor.endpointHost = endpointHost
1465
+ descriptor.resolveEndpoint = resolveEndpoint
1466
+ descriptor.endpointEnv = endpointEnv
1467
+ descriptor.endpointMismatchHint = endpointMismatchHint
1468
+ descriptor.resolveEnToken = resolveEnToken
1469
+ descriptor.readDshWorkbuddyToken = readDshWorkbuddyToken
1470
+ descriptor.readBackendAuthDomain = readBackendAuthDomain
1471
+ descriptor.diagnoseBackend = diagnoseBackend
1472
+ descriptor.backendModelCatalog = backendModelCatalog
1473
+ descriptor.modelCatalogDetailed = modelCatalogDetailed
1474
+ descriptor.formatModelCatalogText = formatModelCatalogText
1475
+ descriptor.parseAccountModels = parseAccountModels
1476
+ descriptor.rememberAccountCatalog = rememberAccountCatalog
1477
+ descriptor.getAccountCatalog = getAccountCatalog
1478
+ descriptor.normalizeProductConfig = normalizeProductConfig
1479
+ descriptor.extractModelIds = extractModelIds
1480
+ descriptor.extractModelCredits = extractModelCredits
1481
+ descriptor.parseCreditValue = parseCreditValue
1482
+ descriptor.formatCreditLabel = formatCreditLabel
1483
+ descriptor.lookupModelCredits = lookupModelCredits
1484
+ descriptor.normalizeSettings = normalizeSettings
1485
+
1486
+ descriptor.attach = function (hostOpts) {
1487
+ host = hostOpts || {}
1488
+ this.engine = createStatusEngine({ provider: this, publish: host.publish })
1489
+ const runner = createRunner(Object.assign({}, host, { provider: this, engine: this.engine }))
1490
+ this.runner = runner
1491
+ return runner
1492
+ }
1493
+ descriptor.hostOptions = () => host
1494
+ return descriptor
1495
+ }
1496
+
1497
+ // core/scheduling.mjs — CLI 调度(启用集合 + 调用优先级)单源纯逻辑。
1498
+ //
1499
+ // 语义(用户可见的两件事):
1500
+ // • enabled = 勾选启用哪些 CLI(数量随之而定);未启用的一面不注册工具、不进提示词。
1501
+ // • priority = 智能体调用 CLI 的建议顺序,数组顺序即优先级(首位最高)。
1502
+ //
1503
+ // 落盘位置:统一设置文件 {DSH_HOME}/cli-bridge-settings.json 的顶层 `_scheduling` 区。
1504
+ // 下划线前缀 = 非 provider 分区(registry 的 PROVIDER_IDS 永远不含它,逐分区语义不受影响),
1505
+ // 因此新代码写出的文件被旧包读取时整区被忽略 = 纯增量,向后兼容。
1506
+ //
1507
+ // 缺省语义(文件缺失/无该区):全部 provider 启用、按注册表顺序 —— 与调度特性上线前
1508
+ // 的行为逐字一致(读不到配置永远不等于关掉所有桥)。
1509
+ //
1510
+ // 沙箱纪律:与 bridge-kit 同源约束 —— 不得 import;Node 内建一律经
1511
+ // globalThis.process.getBuiltinModule + typeof 守卫(动态宿主沙箱可能没有 fs,
1512
+ // 那种环境读不到文件 → 走缺省全开,绝不炸 apply)。
1513
+
1514
+ /** 设置文件里调度区的键名(下划线前缀避开 provider id 命名空间)。 */
1515
+ const SCHEDULING_SECTION = '_scheduling'
1516
+
1517
+ /** 人类可读的默认注册顺序(调用建议顺序);enabled 缺省 = 全部启用。 */
1518
+ function defaultPriority(ids) {
1519
+ return (Array.isArray(ids) ? ids : []).slice()
1520
+ }
1521
+
1522
+ /**
1523
+ * 清洗任意来源的调度对象。
1524
+ * @param {unknown} raw 文件/POST 里的原始值(可能是任何东西,全部容错)
1525
+ * @param {string[]} ids 全量 provider id(顺序 = 缺省优先级)
1526
+ * @param {(id: string) => string} [labelOf] 展示名(仅用于诊断文本,不参与判定)
1527
+ */
1528
+ function normalizeScheduling(raw, ids, labelOf) {
1529
+ const all = Array.isArray(ids) ? ids.slice() : []
1530
+ const label = typeof labelOf === 'function' ? labelOf : ((id) => String(id))
1531
+ const enabled = all.slice()
1532
+ let priority = all.slice()
1533
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
1534
+ if (Array.isArray(raw.enabled)) {
1535
+ const seen = Object.create(null)
1536
+ const picked = []
1537
+ for (const x of raw.enabled) {
1538
+ const id = String(x == null ? '' : x)
1539
+ if (!id || all.indexOf(id) < 0 || seen[id]) continue
1540
+ seen[id] = true; picked.push(id)
1541
+ }
1542
+ // 空数组 = 用户真的想把全部关掉(而不是「没配」),保留该语义;
1543
+ // 非空但过滤后为空 = 写的全是未知 id(坏值)→ 回落全开(fail-open,
1544
+ // 绝不让一次手滑/陈旧配置把所有桥关掉)。
1545
+ if (raw.enabled.length === 0 || picked.length > 0) {
1546
+ enabled.length = 0
1547
+ Array.prototype.push.apply(enabled, picked)
1548
+ }
1549
+ }
1550
+ if (Array.isArray(raw.priority)) {
1551
+ const seen2 = Object.create(null)
1552
+ const ordered = []
1553
+ for (const x of raw.priority) {
1554
+ const id = String(x == null ? '' : x)
1555
+ if (!id || all.indexOf(id) < 0 || seen2[id]) continue
1556
+ seen2[id] = true; ordered.push(id)
1557
+ }
1558
+ for (const id of all) if (!seen2[id]) ordered.push(id) // 漏写的排在末尾
1559
+ if (ordered.length) priority = ordered
1560
+ }
1561
+ }
1562
+ const order = priority.filter((id) => enabled.indexOf(id) >= 0)
1563
+ for (const id of enabled) if (order.indexOf(id) < 0) order.push(id)
1564
+ return { enabled, priority, order }
1565
+ }
1566
+
1567
+ /** 展示文本:「CB → MIMO」或「全部启用 · CB → AGY → MIMO」。 */
1568
+ function describeScheduling(s, labelOf) {
1569
+ const st = s || {}
1570
+ const label = typeof labelOf === 'function' ? labelOf : ((id) => String(id))
1571
+ const order = Array.isArray(st.order) ? st.order : []
1572
+ const names = order.map(label).join(' → ')
1573
+ return names ? names : '(全部禁用)'
1574
+ }
1575
+
1576
+ // ── 读盘(四形态共用;无 fs 时安全返回 null)────────────────────────────────────
1577
+ // 名字带 sched 前缀:本文件会与 bridge-kit / provider 描述符【文本内联进同一个动态
1578
+ // 沙箱脚本作用域】(providers/codebuddy.mjs 已有 builtinModule),撞名即隐患。
1579
+ function schedBuiltinModule(name) {
1580
+ try {
1581
+ const proc = globalThis.process
1582
+ return (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule(name) : null
1583
+ } catch (e) { return null }
1584
+ }
1585
+
1586
+ function schedulingSettingsPath(env) {
1587
+ const e = env || (globalThis.process && globalThis.process.env) || {}
1588
+ try {
1589
+ if (e.CLI_BRIDGE_SETTINGS_FILE) return String(e.CLI_BRIDGE_SETTINGS_FILE)
1590
+ const nodePath = schedBuiltinModule('node:path')
1591
+ if (nodePath && e.DSH_HOME) return nodePath.join(String(e.DSH_HOME), 'cli-bridge-settings.json')
1592
+ } catch (e2) { }
1593
+ return null
1594
+ }
1595
+
1596
+ /**
1597
+ * 生效调度:文件 _scheduling > 缺省全开。
1598
+ * env/argv CLI_BRIDGE_PROVIDER(单面钉住,旧薄封装兼容语义)存在时视为最高优先 ——
1599
+ * 它代表「这个 MCP 进程只为这一家而启动」(部署者的显式意志,比面板勾选更具体、
1600
+ * 更靠近进程生命周期),调度不再削减它。
1601
+ * 宿主形态(preset / dynamic 桥接插件)应传 ignorePin:钉住只是 MCP 进程的启动参数,
1602
+ * 不该让全局 env 反过来把用户会话里的其他桥关掉。
1603
+ * @param {string[]} ids 全量 provider id
1604
+ * @param {{ env?: Record<string,string>, readFileSync?: Function, path?: string|null, ignorePin?: boolean }} [io]
1605
+ */
1606
+ function resolveScheduling(ids, io) {
1607
+ const o = io || {}
1608
+ const env = o.env || (globalThis.process && globalThis.process.env) || {}
1609
+ let raw = null
1610
+ try {
1611
+ const p = ('path' in o) ? o.path : schedulingSettingsPath(env)
1612
+ const nodeFs = o.readFileSync ? { readFileSync: o.readFileSync } : schedBuiltinModule('node:fs')
1613
+ if (p && nodeFs && typeof nodeFs.readFileSync === 'function') {
1614
+ raw = JSON.parse(nodeFs.readFileSync(p, 'utf8'))
1615
+ }
1616
+ } catch (e) { raw = null }
1617
+ const file = normalizeScheduling(raw && typeof raw === 'object' ? raw[SCHEDULING_SECTION] : null, ids)
1618
+ const pinned = o.ignorePin ? '' : String(env.CLI_BRIDGE_PROVIDER || '').trim()
1619
+ if (pinned && ids.indexOf(pinned) >= 0) {
1620
+ return Object.assign(file, {
1621
+ enabled: [pinned],
1622
+ order: [pinned],
1623
+ pinned: pinned,
1624
+ source: raw ? 'env:CLI_BRIDGE_PROVIDER+file' : 'env:CLI_BRIDGE_PROVIDER'
1625
+ })
1626
+ }
1627
+ return Object.assign(file, { pinned: '', source: raw ? 'file' : 'default' })
1628
+ }
1629
+
1630
+ /**
1631
+ * 桥接面门禁(preset / dynamic 的 apply() 首行用):本 provider 是否被调度启用。
1632
+ * fail-open —— 任何异常(无 fs、文件损坏、沙箱限制)都返回 true:读不到配置永远
1633
+ * 不等于关掉桥(否则一次文件损坏就把三条 CLI 全灭了)。ignorePin 固定 true:
1634
+ * 钉住语义属于 MCP 独立进程,宿主形态的全局 env 不该反向影响用户会话。
1635
+ */
1636
+ function isBridgeEnabled(selfId, ids, io) {
1637
+ try {
1638
+ const list = Array.isArray(ids) ? ids : []
1639
+ // 全量清单本身不可得(空/不含自己)= 无门禁语义,fail-open。
1640
+ if (!list.length || list.indexOf(String(selfId)) < 0) return true
1641
+ const o = Object.assign({ ignorePin: true }, io || {})
1642
+ return resolveScheduling(list, o).enabled.indexOf(String(selfId)) >= 0
1643
+ } catch (e) { return true }
1644
+ }
1645
+
1646
+
1647
+ const SCHED_ALL_IDS = ["agy","codebuddy","mimo","opencode"]
1648
+
1649
+
1650
+ return {
1651
+ inject: ['tools', 'subprocess', 'systemPrompt', 'timer'],
1652
+ apply(ctx) {
1653
+ // 调度门禁(0.2.0):设置面板关掉 codebuddy → 本形态不注册工具/政策段;fail-open。
1654
+ if (!isBridgeEnabled('codebuddy', SCHED_ALL_IDS)) return
1655
+ const subprocess = ctx.subprocess
1656
+ const planMode = ctx.get('planMode')
1657
+ const sandboxPolicy = ctx.get('sandboxPolicy')
1658
+ // 沙箱可能运行在无 process 的环境:typeof 守卫读 env(旧仓 dynamic 的等价面)。
1659
+ const env = (typeof process !== 'undefined' && process.env) ? process.env : {}
1660
+
1661
+ const provider = defineProvider({ env })
1662
+
1663
+ function publish(snapshot) {
1664
+ try {
1665
+ const collector = ctx.get('cliBridgeCollector')
1666
+ if (collector && typeof collector.mergeSnapshot === 'function') { collector.mergeSnapshot('codebuddy', snapshot); return }
1667
+ } catch (e) { /* fall through to legacy alias */ }
1668
+ try {
1669
+ const legacy = ctx.get('codebuddyCollector')
1670
+ if (legacy && typeof legacy.mergeSnapshot === 'function') legacy.mergeSnapshot(snapshot)
1671
+ } catch (e) { /* 灯数据尽力而为 */ }
1672
+ }
1673
+
1674
+ // 设置快照(apply 时一次,旧 dynamic 语义):统一文件 codebuddy 分区 >
1675
+ // 旧扁平文件(字段级迁移的只读兜底)> 默认。全程容错,失败不炸 apply。
1676
+ const settingsSnapshot = (function () {
1677
+ const def = provider.normalizeSettings(null)
1678
+ try {
1679
+ const proc = (typeof process !== 'undefined') ? process : null
1680
+ const nodeFs = proc && typeof proc.getBuiltinModule === 'function' ? proc.getBuiltinModule('node:fs') : null
1681
+ const nodePath = proc && typeof proc.getBuiltinModule === 'function' ? proc.getBuiltinModule('node:path') : null
1682
+ if (!nodeFs || typeof nodeFs.readFileSync !== 'function') return def
1683
+ const join = nodePath ? nodePath.join.bind(nodePath) : ((...p) => p.join('/'))
1684
+ const readJson = (p) => { try { return JSON.parse(nodeFs.readFileSync(p, 'utf8')) } catch (e) { return null } }
1685
+ const unifiedFile = env.CLI_BRIDGE_SETTINGS_FILE || (env.DSH_HOME ? join(env.DSH_HOME, 'cli-bridge-settings.json') : null)
1686
+ const unified = unifiedFile ? readJson(unifiedFile) : null
1687
+ if (unified && unified.codebuddy && typeof unified.codebuddy === 'object') return provider.normalizeSettings(unified.codebuddy)
1688
+ const legacyFile = env.CODEBUDDY_SETTINGS_FILE || (env.DSH_HOME ? join(env.DSH_HOME, 'codebuddy-bridge-settings.json') : null)
1689
+ const legacy = legacyFile ? readJson(legacyFile) : null
1690
+ if (legacy && typeof legacy === 'object') return provider.normalizeSettings(legacy)
1691
+ } catch (e) { /* 无 fs:默认值,端点走 CLI 自身 */ }
1692
+ return def
1693
+ })()
1694
+
1695
+ // exe:旧动态三面候选 verbatim(nodeExe 仅在有 process 时有真值,否则退回
1696
+ // 字符串 'node' —— 旧 dynamic 同款守卫)。
1697
+ async function resolveExe(backend, signal) {
1698
+ const nodeExe = (typeof process !== 'undefined' && process.execPath) || 'node'
1699
+ if (backend === 'workbuddy') {
1700
+ return [nodeExe, env.WORKBUDDY_BIN || 'C:\\Program Files\\WorkBuddy\\resources\\app.asar.unpacked\\cli\\bin\\codebuddy']
1701
+ }
1702
+ if (backend === 'codebuddy-en') {
1703
+ return [nodeExe, env.CODEBUDDY_EN_BIN || 'C:\\Program Files\\WorkBuddyAI\\resources\\app.asar.unpacked\\cli\\bin\\codebuddy']
1704
+ }
1705
+ try {
1706
+ const exe = await subprocess.resolveExecutable('codebuddy', undefined, signal)
1707
+ if (!/\.(cmd|bat)$/i.test(exe)) return exe
1708
+ } catch (e) {}
1709
+ const binPath = env.CODEBUDDY_BIN || ((env.APPDATA || 'C:\\Users\\lcl\\AppData\\Roaming') + '\\npm\\node_modules\\@tencent-ai\\codebuddy-code\\bin\\codebuddy')
1710
+ return [nodeExe, binPath]
1711
+ }
1712
+
1713
+ // 旧仓 CWD_FALLBACK 硬编码机器绝对路径:本形态保持旧 dynamic verbatim 行为
1714
+ // (Lead 定案:与旧 dynamic 逐字一致;CODEBUDDY_MCP_CWD 优先仅为 MCP 面语义,
1715
+ // 旧 dynamic 不读它)。
1716
+ function getCwdFallback() {
1717
+ if (sandboxPolicy && typeof sandboxPolicy.workspaceRoot === 'string' && sandboxPolicy.workspaceRoot) return sandboxPolicy.workspaceRoot
1718
+ if (typeof process !== 'undefined' && typeof process.cwd === 'function') { try { return process.cwd() } catch (e) {} }
1719
+ return 'C:\\Users\\lcl\\Desktop\\codebuddy-bridge'
1720
+ }
1721
+
1722
+ function planActiveFor(exec) {
1723
+ try {
1724
+ if (planMode && exec && exec.agent) {
1725
+ const st = planMode.get(exec.agent)
1726
+ return !!(st && st.active)
1727
+ }
1728
+ } catch (e) {}
1729
+ return false
1730
+ }
1731
+
1732
+ provider.attach({
1733
+ ctx,
1734
+ subprocess,
1735
+ resolveExe,
1736
+ getCwdFallback,
1737
+ getPreferredBackend: function () { return settingsSnapshot.preferredBackend || provider.defaultBackend },
1738
+ getSettings: function () { return settingsSnapshot },
1739
+ planActiveFor,
1740
+ publish
1741
+ })
1742
+
1743
+ const OUTPUT_SCHEMA = { type: 'object', additionalProperties: true }
1744
+
1745
+ harness.registerTool(ctx, harness.defineTool({
1746
+ name: provider.tools.run.name,
1747
+ description: provider.tools.run.description,
1748
+ parameters: provider.tools.run.inputSchema,
1749
+ output: { schema: OUTPUT_SCHEMA, render: function (args, value) { return renderResult(value, provider) } },
1750
+ execute: function (args, exec) { return provider.runToolLogic(args, exec) }
1751
+ }))
1752
+ harness.registerTool(ctx, harness.defineTool({
1753
+ name: provider.tools.continue.name,
1754
+ description: provider.tools.continue.description,
1755
+ parameters: provider.tools.continue.inputSchema,
1756
+ output: { schema: OUTPUT_SCHEMA, render: function (args, value) { return renderResult(value, provider) } },
1757
+ execute: function (args, exec) { return provider.continueToolLogic(args, exec) }
1758
+ }))
1759
+ harness.registerTool(ctx, harness.defineTool({
1760
+ name: provider.tools.status.name,
1761
+ description: provider.tools.status.description,
1762
+ parameters: provider.tools.status.inputSchema,
1763
+ output: { schema: OUTPUT_SCHEMA, render: function (args, value) { return renderStatus(value, provider) } },
1764
+ execute: function (args) { return provider.statusToolLogic(args) }
1765
+ }))
1766
+ for (const et of provider.extraTools) {
1767
+ harness.registerTool(ctx, harness.defineTool({
1768
+ name: et.name,
1769
+ description: et.description,
1770
+ parameters: et.inputSchema,
1771
+ output: { schema: OUTPUT_SCHEMA, render: function (args, value) { return et.render ? et.render(value) : renderResult(value, provider) } },
1772
+ execute: function (args, exec) { return et.logic(args, exec) }
1773
+ }))
1774
+ }
1775
+
1776
+ // 浏览器指示灯轮询的 RPC 面(旧 dynamic/handle('codebuddy_status') 语义)。
1777
+ harness.handle('codebuddy_status', function () { return provider.statusToolLogic({}) })
1778
+
1779
+ // 旧 policy 文案 verbatim(提示词连续性红线;name/order 沿用旧 preset 的 5,
1780
+ // 动态与 preset 两形态注入同一段全文,provider.policyText 单源)。
1781
+ ctx.systemPrompt.section({
1782
+ name: 'codebuddy:policy',
1783
+ order: 5,
1784
+ text: provider.policyText
1785
+ })
1786
+ }
1787
+ }