@luchenglong/cli-bridge 0.2.9 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/PROVIDER-CONTRACT.md +34 -1
- package/docs/HANDOVER.md +109 -15
- package/dynamic/claude.host.js +138 -81
- package/dynamic/codebuddy.host.js +1426 -1378
- package/dynamic/codex.host.js +78 -103
- package/dynamic/mimo.host.js +720 -514
- package/dynamic/mimo.host.template.mjs +2 -0
- package/dynamic/opencode.host.js +740 -584
- package/dynamic/opencode.host.template.mjs +2 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +590 -600
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +1 -1
- package/mcp/cli-bridge-mcp.mjs +3 -3
- package/mcp/providers/codebuddy.mjs +427 -427
- package/package.json +3 -3
- package/providers/claude-models.mjs +80 -76
- package/providers/claude.mjs +59 -6
- package/providers/codebuddy-catalog.mjs +367 -599
- package/providers/codebuddy-consts.mjs +72 -72
- package/providers/codebuddy-models-source.mjs +254 -0
- package/providers/codebuddy-models-tool.mjs +106 -106
- package/providers/codebuddy-schemas.mjs +46 -46
- package/providers/codebuddy.mjs +600 -578
- package/providers/codex-cli.mjs +33 -99
- package/providers/codex.mjs +45 -4
- package/providers/mimo-pure.mjs +270 -111
- package/providers/mimo.mjs +453 -517
- package/providers/opencode-exe.mjs +190 -63
- package/providers/opencode.mjs +553 -587
- package/scripts/build.mjs +261 -261
|
@@ -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
|
-
|
|
23
|
-
let
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'codebuddy
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'codebuddy
|
|
68
|
-
'codebuddy-
|
|
69
|
-
'
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
cbCatalogPathJoin(base, '
|
|
94
|
-
cbCatalogPathJoin(base, 'local_storage', '
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
return obj
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
return fromCli
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!m)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (credits
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
|