@luchenglong/cli-bridge 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/PROVIDER-CONTRACT.md +87 -5
  3. package/README.md +12 -11
  4. package/docs/HANDOVER.md +109 -15
  5. package/dynamic/agy.host.js +1 -1
  6. package/dynamic/claude.host.js +1858 -0
  7. package/dynamic/claude.host.template.mjs +108 -0
  8. package/dynamic/codebuddy.host.js +1427 -1379
  9. package/dynamic/codex.host.js +1930 -0
  10. package/dynamic/codex.host.template.mjs +137 -0
  11. package/dynamic/mimo.host.js +721 -515
  12. package/dynamic/mimo.host.template.mjs +2 -0
  13. package/dynamic/opencode.host.js +741 -585
  14. package/dynamic/opencode.host.template.mjs +2 -0
  15. package/home-plugin/cli-bridge-indicator/lib/client.js +74 -90
  16. package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
  17. package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +2 -3
  18. package/mcp/cli-bridge-mcp.mjs +4 -2
  19. package/mcp/providers/claude.mjs +231 -0
  20. package/mcp/providers/codebuddy.mjs +427 -427
  21. package/mcp/providers/codex.mjs +239 -0
  22. package/package.json +6 -2
  23. package/preset/claude-first/agent.cordis.yml +136 -0
  24. package/preset/claude-first/cli-bridge.mjs +131 -0
  25. package/preset/claude-first/plugin-list.mjs +28 -0
  26. package/preset/claude-first/preset.yml +3 -0
  27. package/preset/codex-first/agent.cordis.yml +136 -0
  28. package/preset/codex-first/cli-bridge.mjs +160 -0
  29. package/preset/codex-first/plugin-list.mjs +28 -0
  30. package/preset/codex-first/preset.yml +3 -0
  31. package/providers/claude-exe.mjs +112 -0
  32. package/providers/claude-models.mjs +338 -0
  33. package/providers/claude-policy.mjs +37 -0
  34. package/providers/claude.mjs +561 -0
  35. package/providers/codebuddy-catalog.mjs +367 -599
  36. package/providers/codebuddy-consts.mjs +72 -72
  37. package/providers/codebuddy-models-source.mjs +254 -0
  38. package/providers/codebuddy-models-tool.mjs +106 -106
  39. package/providers/codebuddy-schemas.mjs +46 -46
  40. package/providers/codebuddy.mjs +600 -578
  41. package/providers/codex-cli.mjs +405 -0
  42. package/providers/codex-models.mjs +107 -0
  43. package/providers/codex.mjs +585 -0
  44. package/providers/mimo-pure.mjs +270 -111
  45. package/providers/mimo.mjs +453 -517
  46. package/providers/opencode-exe.mjs +190 -63
  47. package/providers/opencode.mjs +553 -587
  48. package/providers/registry.mjs +25 -1
  49. package/scripts/build.mjs +261 -262
  50. package/scripts/verify.mjs +8 -2
@@ -1,599 +1,367 @@
1
- // providers/codebuddy-catalog.mjs — codebuddy 模型目录/积分/描述文件解析(纯逻辑)。
2
- //
3
- // 从 providers/codebuddy.mjs 拆出:主文件原 864 行远超 600 行红线(规则 §26)。
4
- // 只放「模型目录相关」纯逻辑:静态表、描述文件候选路径、模型 id 抽取、积分倍率、
5
- // 账号清单收割、目录渲染。不含 CLI 调用、状态机、凭据链。
6
- //
7
- // 沙箱纪律(与 providers/*.mjs 同):本文件会被 scripts/build.mjs 经模板顶部的
8
- // 第三个占位标记内联进同一个 dynamic 沙箱脚本作用域 —— 所以:
9
- // 1) 不得 import 任何模块(import 行会被 stripModuleSyntax 剥掉,函数体留下但
10
- // 依赖丢失 → 运行期 ReferenceError);node 内建走 cbCatalog* 守卫;
11
- // 2) 顶层标识符不得与 bridge-kit / codebuddy.mjs 剩余部分撞名(同一作用域重复
12
- // 声明 = 构建期 Identifier already declared);
13
- // 3) 不得出现 import.meta 字面量;
14
- // 4) ⚠️ 注释里绝不写出那个占位标记的字面量:build 做全局文本替换,注释中出现
15
- // 同样会被替换成正文代码,把注释从中间劈开 → 生成物语法崩。
16
-
17
- // ── 宿主能力注入(env / fs / path / sep)───────────────────────────────────
18
- // 本模块函数搬自主文件的闭包,拿不到那边的 env / fsRead / pathJoin / pathSep。
19
- // 由 codebuddy.mjs 在 defineProvider 里调 cbCatalogSetHost() 注入;未注入时退到
20
- // getBuiltinModule 守卫(沙箱里没有 → 返回 null / 默认分隔符,绝不抛)。
21
- // 名字一律带 cbCatalog_ 前缀:内联后与主文件的 fsRead/pathJoin 同处一个作用域。
22
- let cbCatalogEnv = {}
23
- let cbCatalogFs = null
24
- let cbCatalogPathSep = "/"
25
-
26
- export function cbCatalogSetHost(envObj, pathMod, fsMod) {
27
- if (envObj && typeof envObj === "object") cbCatalogEnv = envObj
28
- if (fsMod && typeof fsMod.readFileSync === "function") cbCatalogFs = fsMod
29
- if (pathMod && typeof pathMod.sep === "string") cbCatalogPathSep = pathMod.sep
30
- else if (/[A-Za-z]:[\\/]/.test(String((cbCatalogEnv && (cbCatalogEnv.USERPROFILE || cbCatalogEnv.APPDATA)) || ""))) cbCatalogPathSep = "\\"
31
- }
32
-
33
- export function cbCatalogRead(p, enc) {
34
- if (!p) return null
35
- try {
36
- if (cbCatalogFs && typeof cbCatalogFs.readFileSync === "function") return cbCatalogFs.readFileSync(p, enc || "utf8")
37
- const proc = globalThis.process
38
- const nf = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:fs") : null
39
- if (!nf || typeof nf.readFileSync !== "function") return null
40
- return nf.readFileSync(p, enc || "utf8")
41
- } catch (e) { return null }
42
- }
43
-
44
- export function cbCatalogPathJoin(...parts) {
45
- try {
46
- const proc = globalThis.process
47
- const np = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:path") : null
48
- if (np && typeof np.join === "function") return np.join(...parts)
49
- } catch (e) { }
50
- return parts.join("/")
51
- }
52
-
53
- // ── 以下常量与函数原样搬自 providers/codebuddy.mjs(行为 zero-change)────────
54
-
55
- export const BACKEND_MODEL_IDS = {
56
- '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'],
57
- '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'],
58
- // WorkBuddyAI 自制目录(product.json 顶层 37 项,按其 credits 排序:免费的在前)
59
- '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'],
60
- // WorkBuddy 国内桌面自制目录(product.json:applicationName=WorkBuddy)
61
- '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']
62
- }
63
-
64
- // 各后端真正生效的 product 描述文件位置(旧 core v1.6.1 verbatim)。
65
- export const PRODUCT_DESCRIPTOR_ENV = {
66
- 'codebuddy': { env: 'CODEBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
67
- 'codebuddy-intl': { env: 'CODEBUDDY_INTL_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
68
- 'codebuddy-en': { env: 'CODEBUDDY_EN_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy-ai' },
69
- 'workbuddy': { env: 'WORKBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy' }
70
- }
71
-
72
- // 倍率跨命名匹配(账号清单 id → 描述文件 id,旧 core verbatim)。
73
- export const CREDIT_ID_ALIASES = {
74
- 'kimi-k3-1': ['kimi-k3', 'kimi-k3-ioa'],
75
- 'hy3-x': ['hy3', 'hy3-ioa'],
76
- 'default': ['codewise-default-model-v2'],
77
- 'auto': ['hy3', 'hy3-ioa']
78
- }
79
-
80
- export 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.'
81
-
82
- export const BACKEND_ENUM = ['codebuddy', 'codebuddy-intl', 'codebuddy-ioa', 'codebuddy-international', 'codebuddy-en', 'workbuddy-en', 'workbuddy-ai', 'workbuddy']
83
-
84
- export function accountConfigCandidates(backend, io) {
85
- const e = (io && io.env) || cbCatalogEnv
86
- const spec = PRODUCT_DESCRIPTOR_ENV[backend]
87
- if (!spec || !spec.accountDir) return []
88
- const home = e.USERPROFILE || e.HOME
89
- if (!home) return []
90
- const base = cbCatalogPathJoin(home, spec.accountDir.split('/').join(cbCatalogPathSep))
91
- return [
92
- cbCatalogPathJoin(base, 'cache', 'acc-product-config-v3.json'),
93
- cbCatalogPathJoin(base, 'local_storage', 'entry_d43e96994f944cfb77961c2ea7d04605.info'),
94
- cbCatalogPathJoin(base, 'local_storage', 'wb_entry_d43e96994f944cfb77961c2ea7d04605.info')
95
- ]
96
- }
97
-
98
- export function productDescriptorCandidates(backend, io) {
99
- const e = (io && io.env) || cbCatalogEnv
100
- const spec = PRODUCT_DESCRIPTOR_ENV[backend]
101
- if (!spec) return []
102
- const out = []
103
- for (const p of accountConfigCandidates(backend, io)) out.push(p)
104
- if (e[spec.env]) out.push(e[spec.env])
105
- const rels = spec.fallbackRel ? [spec.rel, spec.fallbackRel] : [spec.rel]
106
- const push = (root) => { if (root) for (const r of rels) out.push(cbCatalogPathJoin(root, ...r.split('/'))) }
107
- if (backend === 'codebuddy' || backend === 'codebuddy-intl') {
108
- push(e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'npm', 'node_modules', '@tencent-ai', 'codebuddy-code') : null)
109
- } else if (backend === 'codebuddy-en') {
110
- push('C:\\Program Files\\WorkBuddyAI\\resources\\app.asar.unpacked\\cli')
111
- } else if (backend === 'workbuddy') {
112
- push('C:\\Program Files\\WorkBuddy\\resources\\app.asar.unpacked\\cli')
113
- }
114
- return out
115
- }
116
-
117
- export function normalizeProductConfig(raw) {
118
- try {
119
- const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
120
- if (!obj) return null
121
- if (Array.isArray(obj)) {
122
- // 数组形态:从后往前找第一个带 data 的条目(末尾最新)。
123
- for (let i = obj.length - 1; i >= 0; i--) {
124
- const e = obj[i]
125
- if (e && typeof e === 'object' && e.data && typeof e.data === 'object') return e.data
126
- }
127
- return null
128
- }
129
- if (typeof obj !== 'object') return null
130
- if (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.models) && !Array.isArray(obj.agents)) return obj.data
131
- return obj
132
- } catch (e) { return null }
133
- }
134
-
135
- export function extractModelIds(productJson) {
136
- try {
137
- const obj = normalizeProductConfig(productJson)
138
- if (!obj || typeof obj !== 'object') return null
139
- const pick = (arr) => {
140
- if (!Array.isArray(arr) || !arr.length) return null
141
- const ids = arr.map((m) => (typeof m === 'string' ? m : m && m.id)).filter((s) => typeof s === 'string' && s.trim())
142
- return ids.length ? [...new Set(ids)] : null
143
- }
144
- const agents = Array.isArray(obj.agents) ? obj.agents : []
145
- const cli = agents.find((a) => a && a.name === 'cli')
146
- const fromCli = cli ? pick(cli.models) : null
147
- const fromTop = pick(obj.models)
148
- if (fromCli && fromTop) return fromTop.length > fromCli.length ? fromTop : fromCli
149
- return fromCli || fromTop || null
150
- } catch (e) { return null }
151
- }
152
-
153
- export function parseCreditValue(raw) {
154
- if (typeof raw !== 'string') return null
155
- const s = raw.trim()
156
- let m = s.match(/^[xX×]\s*([0-9]+(?:\.[0-9]+)?)/)
157
- if (!m) m = s.match(/^([0-9]+(?:\.[0-9]+)?)\s*[xX×]/)
158
- if (!m) return null
159
- const n = Number(m[1])
160
- return Number.isFinite(n) ? n : null
161
- }
162
-
163
- export function formatCreditLabel(credits) {
164
- if (typeof credits !== 'number' || !Number.isFinite(credits)) return ''
165
- if (credits === 0) return '免费'
166
- return `x${credits.toFixed(2)}`
167
- }
168
-
169
- export function extractModelCredits(productJson) {
170
- try {
171
- const obj = normalizeProductConfig(productJson)
172
- if (!obj || typeof obj !== 'object' || !Array.isArray(obj.models)) return null
173
- const map = new Map()
174
- for (const m of obj.models) {
175
- if (!m || typeof m !== 'object') continue
176
- const id = typeof m.id === 'string' ? m.id.trim() : ''
177
- if (!id) continue
178
- const n = parseCreditValue(m.credits)
179
- if (n !== null) map.set(id, n)
180
- }
181
- return map.size ? map : null
182
- } catch (e) { return null }
183
- }
184
-
185
- export function lookupModelCredits(creditsMap, id) {
186
- if (!creditsMap || typeof id !== 'string') return null
187
- const norm = id.trim()
188
- const cands = [norm]
189
- if (norm.endsWith('-ioa')) cands.push(norm.slice(0, -4))
190
- else cands.push(norm + '-ioa')
191
- if (CREDIT_ID_ALIASES[norm]) cands.push(...CREDIT_ID_ALIASES[norm])
192
- for (const c of cands) {
193
- if (creditsMap.has(c)) {
194
- const v = creditsMap.get(c)
195
- if (typeof v === 'number') return v
196
- }
197
- }
198
- return null
199
- }
200
-
201
- export function backendModelCatalog(backend, io) {
202
- const known = BACKEND_MODEL_IDS[backend] || []
203
- const read = (io && io.readFileSync) || cbCatalogRead
204
- let fromFile = null
205
- let fromFileRich = null
206
- // 旧 v1.6.1 语义:第一份有内容的 + 第一份带促销的,优先后者。
207
- for (const p of productDescriptorCandidates(backend, io)) {
208
- let txt = null
209
- try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
210
- if (txt == null) continue
211
- const ids = extractModelIds(txt)
212
- if (ids && ids.length) {
213
- if (!fromFile) fromFile = ids
214
- const obj = normalizeProductConfig(txt)
215
- if (!fromFileRich && obj && Array.isArray(obj.modelPromotions)) fromFileRich = ids
216
- }
217
- if (fromFileRich) break
218
- }
219
- if (!fromFile) return known
220
- return fromFileRich || fromFile
221
- }
222
-
223
- // 服务端回吐的账号清单缓存(进程内,旧 core verbatim)。
224
- const ACCOUNT_CATALOG_CACHE = new Map()
225
- export function rememberAccountCatalog(backend, ids) {
226
- if (Array.isArray(ids) && ids.length) ACCOUNT_CATALOG_CACHE.set(backend, ids.slice())
227
- }
228
- export function getAccountCatalog(backend) {
229
- const v = ACCOUNT_CATALOG_CACHE.get(backend)
230
- return v && v.length ? v.slice() : null
231
- }
232
-
233
- // ── 实时模型清单(CLI 是一手权威来源,对齐 workbuddy-auth 的「权威白名单」思路)──
234
- //
235
- // 为什么不再以静态表/磁盘描述文件为主:实测本机 codebuddy CLI --
236
- // • 账号实时清单(用无效模型触发 400 报文)= 19 项
237
- // • CLI `--help` 声明的 roster = 19 项,与账号清单完全一致
238
- // • 我们原先读的 product.ioa.json 静态表 = 16 项,且 2 个 id 已过时
239
- // (minimax-m3 → minimax-m3-pay、kimi-k3-1 → kimi-k3-2),另缺 4 个新模型。
240
- // 静态表是「随包冻结」的,必然落后于 CLI,故只保留作倍率元数据与最后兜底。
241
- //
242
- // 两级实时来源(都不静默冒充权威数据,各带 source 标签):
243
- // A. account-live —— 起 CLI 用无效模型探测「该账号当前支持」的清单(需该面已登录,
244
- // 一次网络往返;对照 workbuddy-auth 的 agents[cli].models 白名单语义)。
245
- // B. cli-help —— 解析 `--help` 的 "Currently supported: (...)"(零网络、随 CLI
246
- // 版本自动更新;产品面级 roster)。
247
- // 静态表仅作 credit 元数据与 descriptor-static 兜底标签,绝不冒充实时。
248
-
249
- /** 解析 CLI `--help` 里声明的模型 roster。空/畸形输入返回 []。
250
- * ⚠️ 名册很长时 yargs 会在任意列**折行**(实测把 `minimax-m3-pay` 从中间断成
251
- * `minimax-` + `m3-pay`),若按行/按逗号直接切会丢项或产生残片(同一命令两次运行
252
- * 给出 18/19 两种结果)。故先去掉括号内所有空白再按逗号切:逗号是唯一分隔符,
253
- * 折行插入的空白一律视为噪声。 */
254
- export function parseHelpModels(helpText) {
255
- const m = /Currently supported:\s*\(([^)]+)\)/.exec(String(helpText || ''))
256
- if (!m) return []
257
- const out = []
258
- for (const raw of m[1].replace(/\s+/g, '').split(',')) {
259
- const id = raw.trim()
260
- if (id && /^[A-Za-z0-9_.\-]+$/.test(id) && out.indexOf(id) < 0) out.push(id)
261
- }
262
- return out
263
- }
264
-
265
- /** 解析 codebuddy CLI 的可执行候选(env > npm 全局 shim > PATH 名)。 */
266
- export function cbCatalogCliCandidates(env) {
267
- const e = env || cbCatalogEnv || {}
268
- const out = []
269
- const push = (p) => { if (p && out.indexOf(p) < 0) out.push(p) }
270
- if (e.CODEBUDDY_BIN) push(String(e.CODEBUDDY_BIN))
271
- const joinP = (a, b) => cbCatalogPathJoin(a, b)
272
- if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy.cmd'))
273
- if (e.APPDATA) push(joinP(joinP(e.APPDATA, 'npm'), 'codebuddy'))
274
- push('codebuddy')
275
- return out
276
- }
277
-
278
- const CLI_MODEL_CACHE = new Map() // backend → { at, mode, ids }
279
- const CLI_MODEL_TTL_MS = 5 * 60 * 1000
280
-
281
- // Windows .cmd/.bat 需要经 shell(Node 安全限制),但 `shell:true` + args 数组会触发
282
- // DEP0190(参数不转义)。这里统一做法:把命令与参数拼成【一个】字符串交给 shell,
283
- // 参数各自按 Windows 规则加引号 —— 既避开 DEP0190,又保证含空格参数不被拆开。
284
- function quoteWinArg(a) {
285
- const s = String(a)
286
- if (s === '') return '""'
287
- if (!/[\s"&|<>^]/.test(s)) return s
288
- return '"' + s.replace(/"/g, '\\"') + '"'
289
- }
290
- function cliCommandLine(exe, args) {
291
- return [exe].concat(args || []).map(quoteWinArg).join(' ')
292
- }
293
-
294
- /** 把候选拆成 { cmd, prefixArgs }:.mjs/.js/.cjs 目标经 node 包装(测试夹具与自定义
295
- * 入口都靠这条),其余原样。 */
296
- function splitExeSpec(exe) {
297
- if (/\.(mjs|cjs|js)$/i.test(String(exe))) {
298
- const proc = globalThis.process
299
- return { cmd: (proc && proc.execPath) ? proc.execPath : 'node', prefixArgs: [String(exe)] }
300
- }
301
- return { cmd: String(exe), prefixArgs: [] }
302
- }
303
-
304
- function cliSpawnSync(exe, args, opts) {
305
- const proc = globalThis.process
306
- const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
307
- if (!child || typeof child.spawnSync !== 'function') return null
308
- const base = Object.assign({
309
- encoding: 'utf8', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe']
310
- }, opts || {})
311
- const spec = splitExeSpec(exe)
312
- const argv = spec.prefixArgs.concat(args || [])
313
- const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
314
- if (isCmd) base.shell = true
315
- try {
316
- // shell 形态不带 args 数组(否则 DEP0190);非 shell 形态正常传数组。
317
- return isCmd
318
- ? child.spawnSync(cliCommandLine(spec.cmd, argv), base)
319
- : child.spawnSync(spec.cmd, argv, base)
320
- } catch (e) { return null }
321
- }
322
-
323
- /**
324
- * 同步取「CLI 声明的 roster」(零网络,随 CLI 版本走)。
325
- * @returns {{ok:boolean, ids:string[], mode:string, error?:string}}
326
- */
327
- export function fetchHelpModelsSync(env, opts) {
328
- const o = opts || {}
329
- const cands = cbCatalogCliCandidates(env)
330
- let lastErr = null
331
- for (const exe of cands) {
332
- const r = cliSpawnSync(exe, ['--help'], { timeout: Math.max(1000, Math.min(o.timeoutMs || 12000, 30000)) })
333
- if (!r || r.error) { lastErr = 'spawn failed: ' + String((r && r.error && r.error.message) || 'unavailable'); continue }
334
- const text = String(r.stdout || '') + String(r.stderr || '')
335
- const ids = parseHelpModels(text)
336
- if (ids.length) return { ok: true, ids, mode: 'cli-help', exe }
337
- lastErr = 'no roster in --help output'
338
- }
339
- return { ok: false, ids: [], mode: 'cli-help', error: lastErr || 'codebuddy CLI not found (set CODEBUDDY_BIN)' }
340
- }
341
-
342
- /**
343
- * 同步版的账号探测(供设置面板用 —— 面板必须同步返回)。
344
- * 走 spawnSync + 超时;结果进 5 分钟缓存,避免每次渲染都打一次网络。
345
- *
346
- * ⚠️ 为什么面板不能用 `--help` 名册顶替:实测本机 CLI 2.167.0 的 help 文本**本身
347
- * 就是过时的** —— 它列 18 项且含已不存在的 `minimax-m3` / `kimi-k3-1`,缺真实的
348
- * `glm-5.3-flashx` / `minimax-m3-pay` / `kimi-k3-2`;而账号探测给出 19 项且全部有效。
349
- * 也就是说「CLI 声明」≠「服务端实际可用」,拿它当兜底会直接把错误清单展示给用户。
350
- * 故这里宁可空列表 + 明确提示,也不展示已知错误的 roster。
351
- * @returns {{ok:boolean, ids:string[], mode:string, error?:string}}
352
- */
353
- export function probeAccountModelsSync(env, opts) {
354
- const o = opts || {}
355
- const cands = cbCatalogCliCandidates(env)
356
- const extraEnv = o.envExtra || {}
357
- const reasons = []
358
- const timeout = Math.max(3000, Math.min(o.timeoutMs || 12000, 30000))
359
- for (const exe of cands) {
360
- const r = cliSpawnSync(exe, ['-p', 'hi', '--model', '__clb_probe_invalid__', '--output-format', 'text'], {
361
- timeout,
362
- env: Object.assign({}, (globalThis.process && globalThis.process.env) || {}, extraEnv)
363
- })
364
- if (!r || r.error) { reasons.push(exe + ': ' + String((r && r.error && (r.error.code || r.error.message)) || 'spawn failed')); continue }
365
- const out = String(r.stdout || '') + String(r.stderr || '')
366
- const ids = parseAccountModels(out)
367
- if (ids && ids.length) return { ok: true, ids, mode: 'account-live', exe }
368
- const head = String(out).replace(/\s+/g, ' ').trim().slice(0, 140)
369
- reasons.push(exe + ': exit ' + r.status + ', no account roster' + (head ? ' — ' + head : ''))
370
- }
371
- return { ok: false, ids: [], mode: 'account-live', error: reasons.join('; ') || 'no CLI candidate to probe' }
372
- }
373
-
374
- /**
375
- * 异步探测「该账号当前支持」的清单:用无效模型触发 CLI 的 400 报文。
376
- * 对齐 workbuddy-auth 的权威白名单语义(服务器告诉我们这个账号能用什么)。
377
- * @returns {Promise<{ok:boolean, ids:string[], mode:string, error?:string}>}
378
- */
379
- export function probeAccountModels(env, opts) {
380
- const o = opts || {}
381
- const cands = cbCatalogCliCandidates(env)
382
- const proc = globalThis.process
383
- const child = (proc && typeof proc.getBuiltinModule === 'function') ? proc.getBuiltinModule('node:child_process') : null
384
- if (!child || typeof child.spawn !== 'function') {
385
- return Promise.resolve({ ok: false, ids: [], mode: 'account-live', error: 'no spawn capability in this host form' })
386
- }
387
- const extraEnv = o.envExtra || {}
388
- return new Promise((resolve) => {
389
- let idx = 0
390
- // 记录每个候选的真实结局:区分「压根没找到 CLI」与「CLI 跑了但没吐账号名录」
391
- // (后者通常是该面未登录 / 端点拒了)—— 报错文案必须能指导下一步,不能一律
392
- // 说 "CLI not found"(本轮就踩到:codebuddy-en 无 token 时被误报成找不到 CLI)。
393
- const reasons = []
394
- const attempt = () => {
395
- if (idx >= cands.length) {
396
- resolve({
397
- ok: false, ids: [], mode: 'account-live',
398
- error: reasons.length ? reasons.join('; ') : 'no CLI candidate to probe',
399
- notFound: reasons.every((r) => /not found|ENOENT|spawn/i.test(r))
400
- })
401
- return
402
- }
403
- const exe = cands[idx++]
404
- let c
405
- try {
406
- // .cmd/.bat 需经 shell(Windows 安全限制);.mjs/.js 经 node 包装。
407
- // 与 cliSpawnSync 同一规则:shell 形态把命令拼成单字符串(避免 DEP0190)。
408
- const spec = splitExeSpec(exe)
409
- const isCmd = /\.(cmd|bat)$/i.test(spec.cmd)
410
- const probeArgs = spec.prefixArgs.concat(['-p', 'hi', '--model', '__clb_probe_invalid__', '--output-format', 'text'])
411
- const opts = {
412
- stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
413
- env: Object.assign({}, proc.env, extraEnv)
414
- }
415
- c = isCmd
416
- ? child.spawn(cliCommandLine(spec.cmd, probeArgs), Object.assign({ shell: true }, opts))
417
- : child.spawn(spec.cmd, probeArgs, opts)
418
- } catch (e) { reasons.push(exe + ': spawn threw ' + String(e && e.message || e)); attempt(); return }
419
- let out = ''
420
- const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(o.timeoutMs || 30000, 90000)))
421
- const onData = (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) }
422
- c.stdout.on('data', onData)
423
- c.stderr.on('data', onData)
424
- c.on('error', (e) => {
425
- clearTimeout(timer)
426
- reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
427
- attempt()
428
- })
429
- c.on('close', (code) => {
430
- clearTimeout(timer)
431
- const ids = parseAccountModels(out)
432
- if (ids && ids.length) { resolve({ ok: true, ids, mode: 'account-live', exe }); return }
433
- const head = String(out).replace(/\s+/g, ' ').trim().slice(0, 160)
434
- reasons.push(exe + ': exit ' + code + ', no account roster' + (head ? ' — ' + head : ''))
435
- attempt()
436
- })
437
- }
438
- attempt()
439
- })
440
- }
441
-
442
- /** 缓存读写(面板每次渲染都会取,避免反复起进程)。 */
443
- export function cachedCliModels(backend, load, nowMs) {
444
- const at = Number(nowMs) || Date.now()
445
- const hit = CLI_MODEL_CACHE.get(backend)
446
- if (hit && at - hit.at < CLI_MODEL_TTL_MS) return hit
447
- const v = typeof load === 'function' ? load() : load
448
- if (v && v.ok && v.ids && v.ids.length) CLI_MODEL_CACHE.set(backend, { at, mode: v.mode, ids: v.ids.slice() })
449
- return v
450
- }
451
-
452
- /** 读统一设置文件里 codebuddy 分区的 preferredBackend(meta 需要它才知道列哪个面)。 */
453
- export function currentPreferredBackend() {
454
- try {
455
- const e = cbCatalogEnv || {}
456
- const p = cbCatalogRead(e.CLI_BRIDGE_SETTINGS_FILE || cbCatalogPathJoin(
457
- e.DSH_HOME || (e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'DSH Desktop', 'dsh-home') : ''), 'cli-bridge-settings.json'))
458
- if (!p) return null
459
- const all = JSON.parse(p)
460
- const v = all && all.codebuddy && all.codebuddy.preferredBackend
461
- return typeof v === 'string' && v ? v : null
462
- } catch (err) { return null }
463
- }
464
-
465
- // CLI 报错里解析「该账号当前支持的模型」(旧 core parseAccountModels verbatim)。
466
- export function parseAccountModels(text) {
467
- const s = String(text || '')
468
- const anchor = s.indexOf('Currently supported models for your account')
469
- if (anchor < 0) return null
470
- const tail = s.slice(anchor)
471
- const ids = []
472
- for (const line of tail.split(/\r?\n/)) {
473
- const m = /^\s*[-*]\s+(\S+)\s*$/.exec(line)
474
- if (!m) {
475
- if (ids.length) break
476
- continue
477
- }
478
- if (!ids.includes(m[1])) ids.push(m[1])
479
- }
480
- return ids.length ? ids : null
481
- }
482
-
483
- // 带倍率/授权的详细清单(无促销角标,旧 v1.6.1 的 0.1.0 子集;排序规则 verbatim:
484
- // 已授权在前 → 有倍率升序 → 无倍率殿后)。
485
- export function modelCatalogDetailed(backend, io) {
486
- const o = io || {}
487
- const fromFile = backendModelCatalog(backend, o)
488
- const acct = (Array.isArray(o.accountModels) && o.accountModels.length) ? o.accountModels : getAccountCatalog(backend)
489
- const ids = []
490
- const authorized = new Set()
491
- if (acct) for (const id of acct) { if (!ids.includes(id)) ids.push(id); authorized.add(id) }
492
- for (const id of fromFile) if (!ids.includes(id)) ids.push(id)
493
- const read = o.readFileSync || cbCatalogRead
494
- let creditsMap = null
495
- for (const p of productDescriptorCandidates(backend, io)) {
496
- let txt = null
497
- try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
498
- if (txt == null) continue
499
- const map = extractModelCredits(txt)
500
- if (map) { creditsMap = map; break }
501
- }
502
- const items = ids.map((id) => {
503
- const base = lookupModelCredits(creditsMap, id)
504
- const credits = base
505
- const hasCredits = typeof credits === 'number'
506
- const suffix = formatCreditLabel(credits)
507
- const marks = []
508
- if (suffix) marks.push(suffix)
509
- if (acct && !authorized.has(id)) marks.push('未授权')
510
- return {
511
- id,
512
- credits,
513
- baseCredits: base,
514
- free: hasCredits && credits === 0,
515
- hasCredits,
516
- authorized: acct ? authorized.has(id) : null,
517
- promotion: null,
518
- label: marks.length ? `${id} · ${marks.join(' · ')}` : id
519
- }
520
- })
521
- const rank = (x) => (x.authorized === false ? 1 : 0)
522
- const withC = items.filter((x) => x.hasCredits).sort((a, b) => rank(a) - rank(b) || a.credits - b.credits)
523
- const withoutC = items.filter((x) => !x.hasCredits).sort((a, b) => rank(a) - rank(b))
524
- return [...withC, ...withoutC]
525
- }
526
-
527
- export function formatModelCatalogText(backend, io) {
528
- return modelCatalogDetailed(backend, io).map((x) => x.label).join(', ')
529
- }
530
-
531
-
532
- export const BACKEND_AUTH_IDS = {
533
- 'codebuddy': 'Tencent-Cloud.coding-copilot',
534
- 'codebuddy-intl': 'Tencent-Cloud.coding-copilot',
535
- 'codebuddy-en': 'workbuddy-desktop-ai',
536
- 'workbuddy': 'workbuddy-desktop'
537
- }
538
-
539
- export const CREDENTIAL_KEYS = ['WORKBUDDY_TOKEN', 'WORKBUDDY_AI_TOKEN', 'CODEBUDDY_EN_TOKEN']
540
-
541
- // ── 凭据读取(搬自 codebuddy.mjs;fs/path 走 cbCatalog* 守卫,env 经 io 或注入)
542
- export function readBackendAuthDomain(backend, io) {
543
- const o = io || {}
544
- const id = BACKEND_AUTH_IDS[backend]
545
- if (!id) return null
546
- try {
547
- const read = o.readFileSync || cbCatalogRead
548
- if (typeof read !== 'function') return null
549
- const e = o.env || env
550
- let dir = o.dir
551
- if (!dir) {
552
- dir = e.CODEBUDDY_AUTH_DIR ||
553
- cbCatalogPathJoin(e.LOCALAPPDATA || 'C:\\Users\\lcl\\AppData\\Local', 'CodeBuddyExtension', 'Data', 'Public', 'auth')
554
- }
555
- const sep = String(dir).indexOf('\\') >= 0 ? '\\' : '/'
556
- const txt = read(dir + sep + id + '.info', 'utf8')
557
- if (txt == null) return null
558
- const raw = JSON.parse(txt)
559
- const d = raw && raw.auth && raw.auth.domain
560
- return typeof d === 'string' && d.trim() ? d.trim() : null
561
- } catch (e) { return null }
562
- }
563
-
564
- // DSH 凭据库第三通道(旧 core readDshWorkbuddyToken verbatim 语义;CODEBUDDY_
565
- // CREDENTIALS_DIR 覆盖测试夹具;fs 经 deps 注入)。
566
- export function readDshWorkbuddyToken(io) {
567
- const e = (io && io.env) || cbCatalogEnv
568
- const read = (io && io.readFileSync) || cbCatalogRead
569
- const overrideDir = e.CODEBUDDY_CREDENTIALS_DIR || null
570
- const home = overrideDir || e.DSH_HOME || null
571
- if (!home) return null
572
- const files = [home + '\\.credentials.yaml', home + '\\.env', home + '/.credentials.yaml', home + '/.env']
573
- for (const f of files) {
574
- const text = read(f)
575
- if (!text) continue
576
- for (const key of CREDENTIAL_KEYS) {
577
- const m = text.match(new RegExp('^[ \\t]*' + key + '[ \\t]*[:=][ \\t]*(.+)$', 'm'))
578
- if (!m) continue
579
- const v = m[1].trim().replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '').trim()
580
- // 只接受看起来是真实 token 的值(避免把 $VAR 引用或占位符当凭据)。
581
- if (v.length >= 40 && !v.startsWith('$') && !/^<.*>$/.test(v)) return v
582
- }
583
- }
584
- return null
585
- }
586
-
587
- // codebuddy-en 凭据链(旧 core verbatim):设置面板 > CODEBUDDY_AUTH_TOKEN >
588
- // DSH 凭据库。dynamic 沙箱无 fs → 第三通道自然缺席(旧 dynamic 本也有限)。
589
- export function resolveEnToken(settingToken, io) {
590
- const e = (io && io.env) || cbCatalogEnv
591
- const s = String(settingToken || '').trim()
592
- if (s) return { token: s, source: 'setting' }
593
- const envTok = e.CODEBUDDY_AUTH_TOKEN || ''
594
- if (String(envTok).trim()) return { token: String(envTok).trim(), source: 'env' }
595
- const fromStore = readDshWorkbuddyToken(io)
596
- if (fromStore) return { token: fromStore, source: 'dsh-store' }
597
- return { token: null, source: null }
598
- }
599
-
1
+ // providers/codebuddy-catalog.mjs — codebuddy 模型目录/积分/描述文件解析(纯逻辑)。
2
+ //
3
+ // 从 providers/codebuddy.mjs 拆出:主文件原 864 行远超 600 行红线(规则 §26)。
4
+ // 只放「模型目录相关」纯逻辑:静态表、描述文件候选路径、模型 id 抽取、积分倍率、
5
+ // 账号清单收割、目录渲染。不含 CLI 调用、状态机、凭据链。
6
+ //
7
+ // 沙箱纪律(与 providers/*.mjs 同):本文件会被 scripts/build.mjs 经模板顶部的
8
+ // 第三个占位标记内联进同一个 dynamic 沙箱脚本作用域 —— 所以:
9
+ // 1) 不得 import 任何模块(import 行会被 stripModuleSyntax 剥掉,函数体留下但
10
+ // 依赖丢失 → 运行期 ReferenceError);node 内建走 cbCatalog* 守卫;
11
+ // 2) 顶层标识符不得与 bridge-kit / codebuddy.mjs 剩余部分撞名(同一作用域重复
12
+ // 声明 = 构建期 Identifier already declared);
13
+ // 3) 不得出现 import.meta 字面量;
14
+ // 4) ⚠️ 注释里绝不写出那个占位标记的字面量:build 做全局文本替换,注释中出现
15
+ // 同样会被替换成正文代码,把注释从中间劈开 → 生成物语法崩。
16
+
17
+ // ── 宿主能力注入(env / fs / path / sep)───────────────────────────────────
18
+ // 本模块函数搬自主文件的闭包,拿不到那边的 env / fsRead / pathJoin / pathSep。
19
+ // 由 codebuddy.mjs 在 defineProvider 里调 cbCatalogSetHost() 注入;未注入时退到
20
+ // getBuiltinModule 守卫(沙箱里没有 → 返回 null / 默认分隔符,绝不抛)。
21
+ // 名字一律带 cbCatalog_ 前缀:内联后与主文件的 fsRead/pathJoin 同处一个作用域。
22
+ // export 供 codebuddy-models-source.mjs 读取:ESM live binding,cbCatalogSetHost 后自动同步。
23
+ export let cbCatalogEnv = {}
24
+ let cbCatalogFs = null
25
+ let cbCatalogPathSep = "/"
26
+
27
+ export function cbCatalogSetHost(envObj, pathMod, fsMod) {
28
+ if (envObj && typeof envObj === "object") cbCatalogEnv = envObj
29
+ if (fsMod && typeof fsMod.readFileSync === "function") cbCatalogFs = fsMod
30
+ if (pathMod && typeof pathMod.sep === "string") cbCatalogPathSep = pathMod.sep
31
+ else if (/[A-Za-z]:[\\/]/.test(String((cbCatalogEnv && (cbCatalogEnv.USERPROFILE || cbCatalogEnv.APPDATA)) || ""))) cbCatalogPathSep = "\\"
32
+ }
33
+
34
+ export function cbCatalogRead(p, enc) {
35
+ if (!p) return null
36
+ try {
37
+ if (cbCatalogFs && typeof cbCatalogFs.readFileSync === "function") return cbCatalogFs.readFileSync(p, enc || "utf8")
38
+ const proc = globalThis.process
39
+ const nf = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:fs") : null
40
+ if (!nf || typeof nf.readFileSync !== "function") return null
41
+ return nf.readFileSync(p, enc || "utf8")
42
+ } catch (e) { return null }
43
+ }
44
+
45
+ export function cbCatalogPathJoin(...parts) {
46
+ try {
47
+ const proc = globalThis.process
48
+ const np = (proc && typeof proc.getBuiltinModule === "function") ? proc.getBuiltinModule("node:path") : null
49
+ if (np && typeof np.join === "function") return np.join(...parts)
50
+ } catch (e) { }
51
+ return parts.join("/")
52
+ }
53
+
54
+ // ── 以下常量与函数原样搬自 providers/codebuddy.mjs(行为 zero-change)────────
55
+
56
+ export const BACKEND_MODEL_IDS = {
57
+ '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'],
58
+ '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'],
59
+ // WorkBuddyAI 自制目录(product.json 顶层 37 项,按其 credits 排序:免费的在前)
60
+ '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'],
61
+ // WorkBuddy 国内桌面自制目录(product.json:applicationName=WorkBuddy)
62
+ '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']
63
+ }
64
+
65
+ // 各后端真正生效的 product 描述文件位置(旧 core v1.6.1 verbatim)。
66
+ export const PRODUCT_DESCRIPTOR_ENV = {
67
+ 'codebuddy': { env: 'CODEBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
68
+ 'codebuddy-intl': { env: 'CODEBUDDY_INTL_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: null },
69
+ 'codebuddy-en': { env: 'CODEBUDDY_EN_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy-ai' },
70
+ 'workbuddy': { env: 'WORKBUDDY_MODELS_FILE', rel: 'product.ioa.json', fallbackRel: 'product.json', accountDir: '.workbuddy' }
71
+ }
72
+
73
+ // 倍率跨命名匹配(账号清单 id → 描述文件 id,旧 core verbatim)。
74
+ export const CREDIT_ID_ALIASES = {
75
+ 'kimi-k3-1': ['kimi-k3', 'kimi-k3-ioa'],
76
+ 'hy3-x': ['hy3', 'hy3-ioa'],
77
+ 'default': ['codewise-default-model-v2'],
78
+ 'auto': ['hy3', 'hy3-ioa']
79
+ }
80
+
81
+ export 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.'
82
+
83
+ export const BACKEND_ENUM = ['codebuddy', 'codebuddy-intl', 'codebuddy-ioa', 'codebuddy-international', 'codebuddy-en', 'workbuddy-en', 'workbuddy-ai', 'workbuddy']
84
+
85
+ export function accountConfigCandidates(backend, io) {
86
+ const e = (io && io.env) || cbCatalogEnv
87
+ const spec = PRODUCT_DESCRIPTOR_ENV[backend]
88
+ if (!spec || !spec.accountDir) return []
89
+ const home = e.USERPROFILE || e.HOME
90
+ if (!home) return []
91
+ const base = cbCatalogPathJoin(home, spec.accountDir.split('/').join(cbCatalogPathSep))
92
+ return [
93
+ cbCatalogPathJoin(base, 'cache', 'acc-product-config-v3.json'),
94
+ cbCatalogPathJoin(base, 'local_storage', 'entry_d43e96994f944cfb77961c2ea7d04605.info'),
95
+ cbCatalogPathJoin(base, 'local_storage', 'wb_entry_d43e96994f944cfb77961c2ea7d04605.info')
96
+ ]
97
+ }
98
+
99
+ export function productDescriptorCandidates(backend, io) {
100
+ const e = (io && io.env) || cbCatalogEnv
101
+ const spec = PRODUCT_DESCRIPTOR_ENV[backend]
102
+ if (!spec) return []
103
+ const out = []
104
+ for (const p of accountConfigCandidates(backend, io)) out.push(p)
105
+ if (e[spec.env]) out.push(e[spec.env])
106
+ const rels = spec.fallbackRel ? [spec.rel, spec.fallbackRel] : [spec.rel]
107
+ const push = (root) => { if (root) for (const r of rels) out.push(cbCatalogPathJoin(root, ...r.split('/'))) }
108
+ if (backend === 'codebuddy' || backend === 'codebuddy-intl') {
109
+ push(e.APPDATA ? cbCatalogPathJoin(e.APPDATA, 'npm', 'node_modules', '@tencent-ai', 'codebuddy-code') : null)
110
+ } else if (backend === 'codebuddy-en') {
111
+ push('C:\\Program Files\\WorkBuddyAI\\resources\\app.asar.unpacked\\cli')
112
+ } else if (backend === 'workbuddy') {
113
+ push('C:\\Program Files\\WorkBuddy\\resources\\app.asar.unpacked\\cli')
114
+ }
115
+ return out
116
+ }
117
+
118
+ export function normalizeProductConfig(raw) {
119
+ try {
120
+ const obj = typeof raw === 'string' ? JSON.parse(raw) : raw
121
+ if (!obj) return null
122
+ if (Array.isArray(obj)) {
123
+ // 数组形态:从后往前找第一个带 data 的条目(末尾最新)。
124
+ for (let i = obj.length - 1; i >= 0; i--) {
125
+ const e = obj[i]
126
+ if (e && typeof e === 'object' && e.data && typeof e.data === 'object') return e.data
127
+ }
128
+ return null
129
+ }
130
+ if (typeof obj !== 'object') return null
131
+ if (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.models) && !Array.isArray(obj.agents)) return obj.data
132
+ return obj
133
+ } catch (e) { return null }
134
+ }
135
+
136
+ export function extractModelIds(productJson) {
137
+ try {
138
+ const obj = normalizeProductConfig(productJson)
139
+ if (!obj || typeof obj !== 'object') return null
140
+ const pick = (arr) => {
141
+ if (!Array.isArray(arr) || !arr.length) return null
142
+ const ids = arr.map((m) => (typeof m === 'string' ? m : m && m.id)).filter((s) => typeof s === 'string' && s.trim())
143
+ return ids.length ? [...new Set(ids)] : null
144
+ }
145
+ const agents = Array.isArray(obj.agents) ? obj.agents : []
146
+ const cli = agents.find((a) => a && a.name === 'cli')
147
+ const fromCli = cli ? pick(cli.models) : null
148
+ const fromTop = pick(obj.models)
149
+ if (fromCli && fromTop) return fromTop.length > fromCli.length ? fromTop : fromCli
150
+ return fromCli || fromTop || null
151
+ } catch (e) { return null }
152
+ }
153
+
154
+ export function parseCreditValue(raw) {
155
+ if (typeof raw !== 'string') return null
156
+ const s = raw.trim()
157
+ let m = s.match(/^[xX×]\s*([0-9]+(?:\.[0-9]+)?)/)
158
+ if (!m) m = s.match(/^([0-9]+(?:\.[0-9]+)?)\s*[xX×]/)
159
+ if (!m) return null
160
+ const n = Number(m[1])
161
+ return Number.isFinite(n) ? n : null
162
+ }
163
+
164
+ export function formatCreditLabel(credits) {
165
+ if (typeof credits !== 'number' || !Number.isFinite(credits)) return ''
166
+ if (credits === 0) return '免费'
167
+ return `x${credits.toFixed(2)}`
168
+ }
169
+
170
+ export function extractModelCredits(productJson) {
171
+ try {
172
+ const obj = normalizeProductConfig(productJson)
173
+ if (!obj || typeof obj !== 'object' || !Array.isArray(obj.models)) return null
174
+ const map = new Map()
175
+ for (const m of obj.models) {
176
+ if (!m || typeof m !== 'object') continue
177
+ const id = typeof m.id === 'string' ? m.id.trim() : ''
178
+ if (!id) continue
179
+ const n = parseCreditValue(m.credits)
180
+ if (n !== null) map.set(id, n)
181
+ }
182
+ return map.size ? map : null
183
+ } catch (e) { return null }
184
+ }
185
+
186
+ export function lookupModelCredits(creditsMap, id) {
187
+ if (!creditsMap || typeof id !== 'string') return null
188
+ const norm = id.trim()
189
+ const cands = [norm]
190
+ if (norm.endsWith('-ioa')) cands.push(norm.slice(0, -4))
191
+ else cands.push(norm + '-ioa')
192
+ if (CREDIT_ID_ALIASES[norm]) cands.push(...CREDIT_ID_ALIASES[norm])
193
+ for (const c of cands) {
194
+ if (creditsMap.has(c)) {
195
+ const v = creditsMap.get(c)
196
+ if (typeof v === 'number') return v
197
+ }
198
+ }
199
+ return null
200
+ }
201
+
202
+ export function backendModelCatalog(backend, io) {
203
+ const known = BACKEND_MODEL_IDS[backend] || []
204
+ const read = (io && io.readFileSync) || cbCatalogRead
205
+ let fromFile = null
206
+ let fromFileRich = null
207
+ // 旧 v1.6.1 语义:第一份有内容的 + 第一份带促销的,优先后者。
208
+ for (const p of productDescriptorCandidates(backend, io)) {
209
+ let txt = null
210
+ try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
211
+ if (txt == null) continue
212
+ const ids = extractModelIds(txt)
213
+ if (ids && ids.length) {
214
+ if (!fromFile) fromFile = ids
215
+ const obj = normalizeProductConfig(txt)
216
+ if (!fromFileRich && obj && Array.isArray(obj.modelPromotions)) fromFileRich = ids
217
+ }
218
+ if (fromFileRich) break
219
+ }
220
+ if (!fromFile) return known
221
+ return fromFileRich || fromFile
222
+ }
223
+
224
+ // 服务端回吐的账号清单缓存(进程内,旧 core verbatim)。
225
+ const ACCOUNT_CATALOG_CACHE = new Map()
226
+ export function rememberAccountCatalog(backend, ids) {
227
+ if (Array.isArray(ids) && ids.length) ACCOUNT_CATALOG_CACHE.set(backend, ids.slice())
228
+ }
229
+ export function getAccountCatalog(backend) {
230
+ const v = ACCOUNT_CATALOG_CACHE.get(backend)
231
+ return v && v.length ? v.slice() : null
232
+ }
233
+
234
+ export function parseAccountModels(text) {
235
+ const s = String(text || '')
236
+ const anchor = s.indexOf('Currently supported models for your account')
237
+ if (anchor < 0) return null
238
+ const tail = s.slice(anchor)
239
+ const ids = []
240
+ for (const line of tail.split(/\r?\n/)) {
241
+ const m = /^\s*[-*]\s+(\S+)\s*$/.exec(line)
242
+ if (!m) {
243
+ if (ids.length) break
244
+ continue
245
+ }
246
+ if (!ids.includes(m[1])) ids.push(m[1])
247
+ }
248
+ return ids.length ? ids : null
249
+ }
250
+
251
+ // 带倍率/授权的详细清单(无促销角标,旧 v1.6.1 的 0.1.0 子集;排序规则 verbatim:
252
+ // 已授权在前 → 有倍率升序 → 无倍率殿后)。
253
+ export function modelCatalogDetailed(backend, io) {
254
+ const o = io || {}
255
+ const fromFile = backendModelCatalog(backend, o)
256
+ const acct = (Array.isArray(o.accountModels) && o.accountModels.length) ? o.accountModels : getAccountCatalog(backend)
257
+ const ids = []
258
+ const authorized = new Set()
259
+ if (acct) for (const id of acct) { if (!ids.includes(id)) ids.push(id); authorized.add(id) }
260
+ for (const id of fromFile) if (!ids.includes(id)) ids.push(id)
261
+ const read = o.readFileSync || cbCatalogRead
262
+ let creditsMap = null
263
+ for (const p of productDescriptorCandidates(backend, io)) {
264
+ let txt = null
265
+ try { txt = read ? read(p, 'utf8') : null } catch (e) { continue }
266
+ if (txt == null) continue
267
+ const map = extractModelCredits(txt)
268
+ if (map) { creditsMap = map; break }
269
+ }
270
+ const items = ids.map((id) => {
271
+ const base = lookupModelCredits(creditsMap, id)
272
+ const credits = base
273
+ const hasCredits = typeof credits === 'number'
274
+ const suffix = formatCreditLabel(credits)
275
+ const marks = []
276
+ if (suffix) marks.push(suffix)
277
+ if (acct && !authorized.has(id)) marks.push('未授权')
278
+ return {
279
+ id,
280
+ credits,
281
+ baseCredits: base,
282
+ free: hasCredits && credits === 0,
283
+ hasCredits,
284
+ authorized: acct ? authorized.has(id) : null,
285
+ promotion: null,
286
+ label: marks.length ? `${id} · ${marks.join(' · ')}` : id
287
+ }
288
+ })
289
+ const rank = (x) => (x.authorized === false ? 1 : 0)
290
+ const withC = items.filter((x) => x.hasCredits).sort((a, b) => rank(a) - rank(b) || a.credits - b.credits)
291
+ const withoutC = items.filter((x) => !x.hasCredits).sort((a, b) => rank(a) - rank(b))
292
+ return [...withC, ...withoutC]
293
+ }
294
+
295
+ export function formatModelCatalogText(backend, io) {
296
+ return modelCatalogDetailed(backend, io).map((x) => x.label).join(', ')
297
+ }
298
+
299
+
300
+ export const BACKEND_AUTH_IDS = {
301
+ 'codebuddy': 'Tencent-Cloud.coding-copilot',
302
+ 'codebuddy-intl': 'Tencent-Cloud.coding-copilot',
303
+ 'codebuddy-en': 'workbuddy-desktop-ai',
304
+ 'workbuddy': 'workbuddy-desktop'
305
+ }
306
+
307
+ export const CREDENTIAL_KEYS = ['WORKBUDDY_TOKEN', 'WORKBUDDY_AI_TOKEN', 'CODEBUDDY_EN_TOKEN']
308
+
309
+ // ── 凭据读取(搬自 codebuddy.mjs;fs/path 走 cbCatalog* 守卫,env 经 io 或注入)
310
+ export function readBackendAuthDomain(backend, io) {
311
+ const o = io || {}
312
+ const id = BACKEND_AUTH_IDS[backend]
313
+ if (!id) return null
314
+ try {
315
+ const read = o.readFileSync || cbCatalogRead
316
+ if (typeof read !== 'function') return null
317
+ const e = o.env || env
318
+ let dir = o.dir
319
+ if (!dir) {
320
+ dir = e.CODEBUDDY_AUTH_DIR ||
321
+ cbCatalogPathJoin(e.LOCALAPPDATA || 'C:\\Users\\lcl\\AppData\\Local', 'CodeBuddyExtension', 'Data', 'Public', 'auth')
322
+ }
323
+ const sep = String(dir).indexOf('\\') >= 0 ? '\\' : '/'
324
+ const txt = read(dir + sep + id + '.info', 'utf8')
325
+ if (txt == null) return null
326
+ const raw = JSON.parse(txt)
327
+ const d = raw && raw.auth && raw.auth.domain
328
+ return typeof d === 'string' && d.trim() ? d.trim() : null
329
+ } catch (e) { return null }
330
+ }
331
+
332
+ // DSH 凭据库第三通道(旧 core readDshWorkbuddyToken verbatim 语义;CODEBUDDY_
333
+ // CREDENTIALS_DIR 覆盖测试夹具;fs 经 deps 注入)。
334
+ export function readDshWorkbuddyToken(io) {
335
+ const e = (io && io.env) || cbCatalogEnv
336
+ const read = (io && io.readFileSync) || cbCatalogRead
337
+ const overrideDir = e.CODEBUDDY_CREDENTIALS_DIR || null
338
+ const home = overrideDir || e.DSH_HOME || null
339
+ if (!home) return null
340
+ const files = [home + '\\.credentials.yaml', home + '\\.env', home + '/.credentials.yaml', home + '/.env']
341
+ for (const f of files) {
342
+ const text = read(f)
343
+ if (!text) continue
344
+ for (const key of CREDENTIAL_KEYS) {
345
+ const m = text.match(new RegExp('^[ \\t]*' + key + '[ \\t]*[:=][ \\t]*(.+)$', 'm'))
346
+ if (!m) continue
347
+ const v = m[1].trim().replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '').trim()
348
+ // 只接受看起来是真实 token 的值(避免把 $VAR 引用或占位符当凭据)。
349
+ if (v.length >= 40 && !v.startsWith('$') && !/^<.*>$/.test(v)) return v
350
+ }
351
+ }
352
+ return null
353
+ }
354
+
355
+ // codebuddy-en 凭据链(旧 core verbatim):设置面板 > CODEBUDDY_AUTH_TOKEN >
356
+ // DSH 凭据库。dynamic 沙箱无 fs → 第三通道自然缺席(旧 dynamic 本也有限)。
357
+ export function resolveEnToken(settingToken, io) {
358
+ const e = (io && io.env) || cbCatalogEnv
359
+ const s = String(settingToken || '').trim()
360
+ if (s) return { token: s, source: 'setting' }
361
+ const envTok = e.CODEBUDDY_AUTH_TOKEN || ''
362
+ if (String(envTok).trim()) return { token: String(envTok).trim(), source: 'env' }
363
+ const fromStore = readDshWorkbuddyToken(io)
364
+ if (fromStore) return { token: fromStore, source: 'dsh-store' }
365
+ return { token: null, source: null }
366
+ }
367
+