@luchenglong/cli-bridge 0.2.9 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/PROVIDER-CONTRACT.md +34 -1
- package/docs/HANDOVER.md +109 -15
- package/dynamic/claude.host.js +138 -81
- package/dynamic/codebuddy.host.js +1426 -1378
- package/dynamic/codex.host.js +78 -103
- package/dynamic/mimo.host.js +720 -514
- package/dynamic/mimo.host.template.mjs +2 -0
- package/dynamic/opencode.host.js +740 -584
- package/dynamic/opencode.host.template.mjs +2 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +590 -600
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +1 -1
- package/mcp/cli-bridge-mcp.mjs +3 -3
- package/mcp/providers/codebuddy.mjs +427 -427
- package/package.json +3 -3
- package/providers/claude-models.mjs +80 -76
- package/providers/claude.mjs +59 -6
- package/providers/codebuddy-catalog.mjs +367 -599
- package/providers/codebuddy-consts.mjs +72 -72
- package/providers/codebuddy-models-source.mjs +254 -0
- package/providers/codebuddy-models-tool.mjs +106 -106
- package/providers/codebuddy-schemas.mjs +46 -46
- package/providers/codebuddy.mjs +600 -578
- package/providers/codex-cli.mjs +33 -99
- package/providers/codex.mjs +45 -4
- package/providers/mimo-pure.mjs +270 -111
- package/providers/mimo.mjs +453 -517
- package/providers/opencode-exe.mjs +190 -63
- package/providers/opencode.mjs +553 -587
- package/scripts/build.mjs +261 -261
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luchenglong/cli-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "CLI Bridge for DeepSeek Harness (DSH): one package bridging six local coding-CLI agents — agy (Google Antigravity CLI), Tencent CodeBuddy Code, Xiaomi MiMo (MiMoCode), OpenCode, Claude Code, and OpenAI Codex CLI. Each provider keeps its own agent preset (cordis-agy / codebuddy-first / cordis-mimo / opencode-first / claude-first / codex-first), tools (<id>_run / <id>_continue / <id>_status, plus per-provider model listing), rate-limit/network fallback dialog, and zero-dependency MCP server, unified under a single home-level status light, one settings file, and one bundle patch. Merges agy-first-bridge, codebuddy-first-bridge and mimo-first-bridge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"./providers/codebuddy": "./providers/codebuddy.mjs",
|
|
21
21
|
"./providers/mimo": "./providers/mimo.mjs",
|
|
22
22
|
"./providers/opencode": "./providers/opencode.mjs",
|
|
23
|
-
"./providers/claude": "./providers/claude.mjs",
|
|
23
|
+
"./providers/claude": "./providers/claude.mjs",
|
|
24
24
|
"./providers/codex": "./providers/codex.mjs",
|
|
25
25
|
"./preset-agy": "./preset/agy-first/cli-bridge.mjs",
|
|
26
26
|
"./preset-codebuddy": "./preset/codebuddy-first/cli-bridge.mjs",
|
|
27
27
|
"./preset-mimo": "./preset/mimo-first/cli-bridge.mjs",
|
|
28
28
|
"./preset-opencode": "./preset/opencode-first/cli-bridge.mjs",
|
|
29
|
-
"./preset-claude": "./preset/claude-first/cli-bridge.mjs",
|
|
29
|
+
"./preset-claude": "./preset/claude-first/cli-bridge.mjs",
|
|
30
30
|
"./preset-codex": "./preset/codex-first/cli-bridge.mjs",
|
|
31
31
|
"./package.json": "./package.json"
|
|
32
32
|
},
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
// ~/.claude/settings.json 的 env.ANTHROPIC_DEFAULT_*_MODEL 只是「别名映射」,
|
|
14
14
|
// 不是服务端可用全集 —— 红线:绝不把它冒充成可用清单,也绝不静默返回空数组。
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
16
|
+
// 缓存与「谁探测」(契约 §3.2 手动拉取):
|
|
17
|
+
// • settings.meta() 是同步回调,且**不再自动探测** —— 它只调 readCachedModels() 读这里的
|
|
18
|
+
// 缓存(纯读、不 spawn、不联网);没命中就留空,由 note 提示用户点「拉取可用模型」。
|
|
19
|
+
// • 探测只有一条路:settings.refreshModels() → 异步 probeSupportedModels()(用户显式触发)。
|
|
20
|
+
// 成功后经 cachedModels() 写回同一个缓存,下次 meta() 直接命中 —— 面板首屏不再被拖慢。
|
|
21
|
+
// • 5 分钟 TTL 只约束**工具调用**(claude_models 的重复调用);面板展示的是「上次拉取到的
|
|
22
|
+
// 权威清单」,过期也照旧展示,但 note 会标注拉取时间(宁可显式标旧,也不让下拉莫名清空)。
|
|
18
23
|
|
|
19
24
|
import { claudeExeCandidates, claudeProbeCandidates, splitExeSpec, quoteShellArg } from './claude-exe.mjs'
|
|
20
25
|
|
|
@@ -22,11 +27,11 @@ import { claudeExeCandidates, claudeProbeCandidates, splitExeSpec, quoteShellArg
|
|
|
22
27
|
export const PROBE_MODEL = '__clb_probe_invalid__'
|
|
23
28
|
export const MODELS_TTL_MS = 5 * 60 * 1000
|
|
24
29
|
|
|
25
|
-
// 探测结果缓存:key(后端/端点标识) → { at,
|
|
30
|
+
// 探测结果缓存:key(后端/端点标识) → { at, value }
|
|
26
31
|
const modelsCache = new Map()
|
|
27
32
|
|
|
28
33
|
/** 清空探测缓存(测试与「模型变更后刷新」用)。 */
|
|
29
|
-
export function resetModelsCache() { modelsCache.clear()
|
|
34
|
+
export function resetModelsCache() { modelsCache.clear() }
|
|
30
35
|
|
|
31
36
|
// 缓存键 = 候选可执行文件集合(同一台机器可能切换 CLAUDE_BIN / 面板路径,键必须跟着变,
|
|
32
37
|
// 否则会把「上一个 exe 的探测结果」错配给新 exe)。
|
|
@@ -35,8 +40,18 @@ function candidateKey(env, binPath) {
|
|
|
35
40
|
return list.length ? list.join('|') : 'claude'
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
/** 缓存的键(面板读缓存与工具探测必须用同一把键,否则写进去读不出来)。 */
|
|
44
|
+
export function modelsCacheKey(env, binPath) {
|
|
45
|
+
return 'claude:' + candidateKey(env, binPath)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 缓存里只可能存「成功的权威清单」;失败绝不入缓存(否则会把坏状态固化)。 */
|
|
49
|
+
function isUsableProbe(v) {
|
|
50
|
+
return !!(v && v.ok === true && Array.isArray(v.ids) && v.ids.length)
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
/**
|
|
39
|
-
*
|
|
54
|
+
* 缓存包装(异步工具路径):TTL 内直接返回上次成功结果;失败不写缓存。
|
|
40
55
|
* @param {string} key
|
|
41
56
|
* @param {() => Promise<object>|object} load
|
|
42
57
|
* @param {number} [nowMs]
|
|
@@ -46,11 +61,47 @@ export function cachedModels(key, load, nowMs) {
|
|
|
46
61
|
const hit = modelsCache.get(key)
|
|
47
62
|
if (hit && at - hit.at < MODELS_TTL_MS) return hit.value
|
|
48
63
|
return Promise.resolve().then(load).then((v) => {
|
|
49
|
-
if (
|
|
64
|
+
if (isUsableProbe(v)) modelsCache.set(key, { at, value: v })
|
|
50
65
|
return v
|
|
51
66
|
})
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
/**
|
|
70
|
+
* 只读缓存(settings.meta 专用):纯读、不 spawn、不联网。
|
|
71
|
+
* 与 cachedModels 的区别:这里**不受 TTL 限制** —— 面板展示的是「上次真正拉取到的那份
|
|
72
|
+
* 服务端清单」,附 ageMs 让 note 如实标注新旧;空/过期与否都不该凭空清空用户看到的下拉。
|
|
73
|
+
*
|
|
74
|
+
* 为什么要「精确键未命中 → 取最近一次成功结果」:keystone 渲染面板时只把 env 传给
|
|
75
|
+
* meta(),**拿不到面板里保存的 binPath**(settingsView 里 createProvider 只带 env),
|
|
76
|
+
* 而缓存键含候选 exe 集合 → 用户一旦填过 binPath,精确键必然错开,下拉会在拉取成功后
|
|
77
|
+
* 仍然空着(看起来像按钮没起作用)。这不算降级:清单是 claude 自己 settings.json 里
|
|
78
|
+
* 端点+凭据裁决的,与走哪个候选 exe 无关(exe 只决定探测能否跑起来),且 note 会带上
|
|
79
|
+
* 「N 分钟前拉取 / 源 account-live」,用户看到的是真实的探测结果与真实的年龄。
|
|
80
|
+
* @param {object} env
|
|
81
|
+
* @param {{binPath?: string, nowMs?: number}} [opts]
|
|
82
|
+
* @returns {{ids: string[], mode: string, exe: (string|null), at: number, ageMs: number}|null}
|
|
83
|
+
*/
|
|
84
|
+
export function readCachedModels(env, opts) {
|
|
85
|
+
const o = opts || {}
|
|
86
|
+
const at = Number(o.nowMs) || Date.now()
|
|
87
|
+
const exact = modelsCache.get(modelsCacheKey(env, o.binPath))
|
|
88
|
+
let hit = isUsableProbe(exact && exact.value) ? exact : null
|
|
89
|
+
if (!hit) {
|
|
90
|
+
for (const entry of modelsCache.values()) {
|
|
91
|
+
if (!isUsableProbe(entry.value)) continue
|
|
92
|
+
if (!hit || entry.at > hit.at) hit = entry
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!hit) return null
|
|
96
|
+
return {
|
|
97
|
+
ids: hit.value.ids.slice(),
|
|
98
|
+
mode: hit.value.mode || 'account-live',
|
|
99
|
+
exe: hit.value.exe || null,
|
|
100
|
+
at: hit.at,
|
|
101
|
+
ageMs: Math.max(0, at - hit.at)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
54
105
|
/**
|
|
55
106
|
* 从 400 报文里解析「服务端支持的模型名」。
|
|
56
107
|
* 主格式(实测):`The supported API model names are A, B, but you passed Z.`
|
|
@@ -171,68 +222,8 @@ function probeFailureText(reasons, tried) {
|
|
|
171
222
|
}
|
|
172
223
|
|
|
173
224
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* @param {{binPath?: string, timeoutMs?: number}} [opts]
|
|
177
|
-
* @returns {{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}}
|
|
178
|
-
*/
|
|
179
|
-
export function probeSupportedModelsSync(env, opts) {
|
|
180
|
-
const o = opts || {}
|
|
181
|
-
const child = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:child_process') : null } catch (x) { return null } })()
|
|
182
|
-
if (!child || typeof child.spawnSync !== 'function') {
|
|
183
|
-
return { ok: false, ids: [], mode: 'account-live', error: 'no spawnSync capability in this host form' }
|
|
184
|
-
}
|
|
185
|
-
const cands = claudeProbeCandidates(env, o.binPath)
|
|
186
|
-
const probeList = cands.length ? cands : ['claude']
|
|
187
|
-
const timeout = Math.max(3000, Math.min(Number(o.timeoutMs) || 12000, 30000))
|
|
188
|
-
const reasons = []
|
|
189
|
-
const proc = globalThis.process
|
|
190
|
-
for (const exe of probeList) {
|
|
191
|
-
const spec = splitExeSpec(exe)
|
|
192
|
-
const argv = spec.prefixArgs.concat(probeArgv())
|
|
193
|
-
const cmd = spec.shell ? [spec.cmd].concat(argv).map(quoteShellArg).join(' ') : spec.cmd
|
|
194
|
-
const r = child.spawnSync(cmd, spec.shell ? [] : argv, {
|
|
195
|
-
encoding: 'utf8', timeout, windowsHide: true, shell: !!spec.shell,
|
|
196
|
-
env: Object.assign({}, (proc && proc.env) || {}, {})
|
|
197
|
-
})
|
|
198
|
-
if (!r || r.error) { reasons.push(exe + ': ' + String((r && r.error && (r.error.code || r.error.message)) || 'spawn failed')); continue }
|
|
199
|
-
const got = extractProbeIds(r.stdout, r.stderr)
|
|
200
|
-
if (got.ids && got.ids.length) return { ok: true, ids: got.ids, mode: 'account-live', exe }
|
|
201
|
-
const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
202
|
-
reasons.push(exe + ': exit ' + r.status + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
|
|
203
|
-
}
|
|
204
|
-
return { ok: false, ids: [], mode: 'account-live', error: probeFailureText(reasons, probeList.length) }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// 同步探测缓存(面板每次渲染都会取,避免反复 spawnSync 打网络)。
|
|
208
|
-
const syncModelsCache = new Map()
|
|
209
|
-
// 失败结果的反向缓存 TTL:成功缓存 5 分钟;失败只缓存 60 秒。
|
|
210
|
-
// 为什么必须有反向缓存:面板是同步回调,CLI 缺失/未登录时一次 spawnSync 可能阻塞数秒,
|
|
211
|
-
// 每次渲染都重探会让设置面板卡顿。60 秒足够让用户装好 CLI/登录后立刻看到新结果,
|
|
212
|
-
// 又不会把「当时读不到」长期固化成假象(状态照旧 ok:false + 可操作错误,语义不变)。
|
|
213
|
-
const NEGATIVE_TTL_MS = 60 * 1000
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* 带缓存的同步探测(settings.meta 专用;面板是同步回调,只能 spawnSync)。
|
|
217
|
-
* 成功/失败都进缓存(TTL 不同),失败语义保持不变:ok:false + 可操作错误。
|
|
218
|
-
* @returns {{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}}
|
|
219
|
-
*/
|
|
220
|
-
export function probeSupportedModelsSyncCached(env, opts) {
|
|
221
|
-
const o = opts || {}
|
|
222
|
-
const key = 'sync:claude:' + candidateKey(env, o.binPath)
|
|
223
|
-
const at = Date.now()
|
|
224
|
-
const hit = syncModelsCache.get(key)
|
|
225
|
-
if (hit) {
|
|
226
|
-
const ttl = hit.value && hit.value.ok ? MODELS_TTL_MS : NEGATIVE_TTL_MS
|
|
227
|
-
if (at - hit.at < ttl) return hit.value
|
|
228
|
-
}
|
|
229
|
-
const v = probeSupportedModelsSync(env, o)
|
|
230
|
-
syncModelsCache.set(key, { at, value: v })
|
|
231
|
-
return v
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* 异步探测(MCP / preset / dynamic 三形态共用):起一次真实 CLI 调用,解析 400 报文。
|
|
225
|
+
* 异步探测(唯一探测实现:MCP / preset / dynamic 三形态 + 面板「拉取可用模型」共用):
|
|
226
|
+
* 起一次真实 CLI 调用,解析 400 报文。
|
|
236
227
|
* 铁律:只认探测结果;失败 = ok:false + 可操作错误,绝不回落到配置文件/静态表。
|
|
237
228
|
* @param {object} env
|
|
238
229
|
* @param {{binPath?: string, timeoutMs?: number}} [opts]
|
|
@@ -269,20 +260,33 @@ export function probeSupportedModels(env, opts) {
|
|
|
269
260
|
let out = ''
|
|
270
261
|
let err = ''
|
|
271
262
|
const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(Number(o.timeoutMs) || 30000, 90000)))
|
|
263
|
+
// 一个候选只允许结算一次:spawn 失败时 'error' 与 'close' 会先后都触发,
|
|
264
|
+
// 两个回调各调一次 attempt() 会把后面的候选整个跳过(idx 多走一格)——
|
|
265
|
+
// 实测后果是「binPath 填错 → 明明 CLAUDE_BIN 里那个能跑,探测却直接失败」。
|
|
266
|
+
let settled = false
|
|
267
|
+
const nextCandidate = (reason) => {
|
|
268
|
+
if (settled) return
|
|
269
|
+
settled = true
|
|
270
|
+
clearTimeout(timer)
|
|
271
|
+
if (reason) reasons.push(reason)
|
|
272
|
+
attempt()
|
|
273
|
+
}
|
|
272
274
|
c.stdout.on('data', (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) })
|
|
273
275
|
c.stderr.on('data', (d) => { err += d; if (err.length > 200000) err = err.slice(-100000) })
|
|
274
276
|
c.on('error', (e) => {
|
|
275
|
-
|
|
276
|
-
reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
|
|
277
|
-
attempt()
|
|
277
|
+
nextCandidate(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
|
|
278
278
|
})
|
|
279
279
|
c.on('close', (code) => {
|
|
280
|
-
|
|
280
|
+
if (settled) return
|
|
281
281
|
const got = extractProbeIds(out, err)
|
|
282
|
-
if (got.ids && got.ids.length) {
|
|
282
|
+
if (got.ids && got.ids.length) {
|
|
283
|
+
settled = true
|
|
284
|
+
clearTimeout(timer)
|
|
285
|
+
resolve({ ok: true, ids: got.ids, mode: 'account-live', exe })
|
|
286
|
+
return
|
|
287
|
+
}
|
|
283
288
|
const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
284
|
-
|
|
285
|
-
attempt()
|
|
289
|
+
nextCandidate(exe + ': exit ' + code + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
|
|
286
290
|
})
|
|
287
291
|
}
|
|
288
292
|
attempt()
|
|
@@ -298,7 +302,7 @@ export function probeSupportedModels(env, opts) {
|
|
|
298
302
|
*/
|
|
299
303
|
export async function listAccountModels(env, opts) {
|
|
300
304
|
const o = opts || {}
|
|
301
|
-
const key =
|
|
305
|
+
const key = modelsCacheKey(env, o.binPath)
|
|
302
306
|
if (o.refresh) modelsCache.delete(key)
|
|
303
307
|
const probe = await cachedModels(key, () => probeSupportedModels(env, o))
|
|
304
308
|
const cfg = readConfiguredModels(env)
|
package/providers/claude.mjs
CHANGED
|
@@ -21,15 +21,28 @@
|
|
|
21
21
|
// - 模型清单只认服务端权威探测;拿不到 = ok:false + 可操作错误(绝不静默空数组/配置冒充)。
|
|
22
22
|
import { createStatusEngine, createRunner, clampInt, isLimited } from '../core/bridge-kit.mjs'
|
|
23
23
|
import { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, CLAUDE_BIN_ENV_VARS } from './claude-exe.mjs'
|
|
24
|
-
import { PROBE_MODEL, parseSupportedModels, probeSupportedModels,
|
|
24
|
+
import { PROBE_MODEL, parseSupportedModels, probeSupportedModels, readCachedModels, readConfiguredModels, listAccountModels, resetModelsCache } from './claude-models.mjs'
|
|
25
25
|
// 单行 re-export(build 内联时整行剥离;多行形态风险见 build.mjs 的 stripModuleSyntax 注释)。
|
|
26
|
-
export { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, PROBE_MODEL, parseSupportedModels, probeSupportedModels, readConfiguredModels, listAccountModels, resetModelsCache }
|
|
26
|
+
export { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, PROBE_MODEL, parseSupportedModels, probeSupportedModels, readCachedModels, readConfiguredModels, listAccountModels, resetModelsCache }
|
|
27
27
|
|
|
28
28
|
function nowIso() { return new Date().toISOString() }
|
|
29
29
|
|
|
30
30
|
// 权限模式的用户可见取值(auto 由 DSH plan 状态决定;其余直通 CLI)。
|
|
31
31
|
const MODE_ENUM = ['auto', 'plan', 'accept-edits', 'bypass']
|
|
32
32
|
|
|
33
|
+
// 手动拉取(契约 §3.2)的探测时限与失败指引。
|
|
34
|
+
// 失败文案必须「可操作」:面板只会把 error 原样显示给用户,没写下一步等于没帮上忙。
|
|
35
|
+
const REFRESH_TIMEOUT_MS = 30000
|
|
36
|
+
const REFRESH_HINT = '排查顺序:① 确认设置面板的「claude 可执行文件路径」或 CLAUDE_BIN 指向真实 claude(原生 .exe 优先);② 确认 ~/.claude/settings.json 的 env 里 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN 指向可用端点(该面若未登录,先在终端跑一次 claude 完成登录);③ 再点一次「拉取可用模型」。'
|
|
37
|
+
|
|
38
|
+
/** 缓存年龄的短文案(面板 note 用;只描述事实,不假装新鲜)。 */
|
|
39
|
+
function ageText(ms) {
|
|
40
|
+
const min = Math.floor(Math.max(0, Number(ms) || 0) / 60000)
|
|
41
|
+
if (min < 1) return '刚刚拉取'
|
|
42
|
+
if (min < 60) return min + ' 分钟前拉取'
|
|
43
|
+
return Math.floor(min / 60) + ' 小时前拉取'
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
// ── 三工具 schema ─────────────────────────────────────────────────────────────
|
|
34
47
|
const RUN_PARAMS = {
|
|
35
48
|
type: 'object',
|
|
@@ -404,11 +417,17 @@ export default function defineProvider(deps = {}) {
|
|
|
404
417
|
}
|
|
405
418
|
return out
|
|
406
419
|
},
|
|
420
|
+
/**
|
|
421
|
+
* 设置面板元数据(契约 §3.2):**只读缓存、绝不探测**。
|
|
422
|
+
* 面板是同步回调,而探测要起子进程打网络(3-5s)——放在这里会把首屏拖慢,且探不到时
|
|
423
|
+
* 用户无法区分「没模型」与「没拉到」。故探测改为用户点「拉取可用模型」→
|
|
424
|
+
* settings.refreshModels() 显式触发;成功后写回同一份缓存,这里下次渲染直接命中。
|
|
425
|
+
*/
|
|
407
426
|
meta: (env2) => {
|
|
408
427
|
const e = env2 && typeof env2 === 'object' ? env2 : {}
|
|
409
428
|
const s = currentSettings()
|
|
410
|
-
const
|
|
411
|
-
const options = (
|
|
429
|
+
const cached = readCachedModels(e, { binPath: s.binPath })
|
|
430
|
+
const options = (cached ? cached.ids : []).map((id) => ({ value: id, label: id }))
|
|
412
431
|
const exeCandidates = claudeExeCandidates(e, '')
|
|
413
432
|
return {
|
|
414
433
|
titleSuffix: 'Claude Code',
|
|
@@ -422,10 +441,12 @@ export default function defineProvider(deps = {}) {
|
|
|
422
441
|
{
|
|
423
442
|
key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
|
|
424
443
|
options,
|
|
444
|
+
// 面板据此渲染「手动拉取」按钮(契约 §3.2);meta() 自身永不探测。
|
|
445
|
+
refresh: { label: '拉取可用模型' },
|
|
425
446
|
placeholder: '留空 = 由 claude 自身配置(~/.claude/settings.json)决定',
|
|
426
447
|
note: options.length
|
|
427
|
-
? '共 ' + options.length + '
|
|
428
|
-
: '
|
|
448
|
+
? '共 ' + options.length + ' 个服务端权威可用模型(' + ageText(cached.ageMs) + ',源:' + cached.mode + ')。点「拉取可用模型」重新获取;调用时传 model 可覆盖本项。'
|
|
449
|
+
: '还没拉取过(或上次没拉到):点「拉取可用模型」向端点取权威清单;空表示尚未拉取 / 该面未登录。可直接手动输入模型名 —— 配置里的 ANTHROPIC_*_MODEL 只是别名映射,不作为候选展示。'
|
|
429
450
|
},
|
|
430
451
|
{
|
|
431
452
|
key: 'maxBudgetUsd', label: '单次最大花费(美元)', type: 'text',
|
|
@@ -435,6 +456,38 @@ export default function defineProvider(deps = {}) {
|
|
|
435
456
|
]
|
|
436
457
|
}
|
|
437
458
|
},
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 手动拉取(契约 §3.2):面板「拉取可用模型」按钮 → keystone
|
|
462
|
+
* POST /cli-bridge/settings/models → 这里。这是本 provider 唯一的面板探测入口。
|
|
463
|
+
*
|
|
464
|
+
* 复用 listAccountModels(异步 probeSupportedModels + 配置诊断),不写第二套探测;
|
|
465
|
+
* 其成功路径已把权威清单写回 modelsCache,随后 meta() 直接命中(无需再探测)。
|
|
466
|
+
* 铁律:失败 = ok:false + 可操作错误 + 空 ids,绝不返回 {ok:true, ids:[]};
|
|
467
|
+
* 也绝不把 settings.json 里的别名映射冒充成服务端可用全集。
|
|
468
|
+
*/
|
|
469
|
+
async refreshModels(settings, env2) {
|
|
470
|
+
const e = (env2 && typeof env2 === 'object') ? env2 : env
|
|
471
|
+
const s = descriptor.settings.normalize(settings)
|
|
472
|
+
const got = await listAccountModels(e, {
|
|
473
|
+
binPath: s.binPath,
|
|
474
|
+
timeoutMs: REFRESH_TIMEOUT_MS,
|
|
475
|
+
refresh: true
|
|
476
|
+
})
|
|
477
|
+
if (!got.ok) {
|
|
478
|
+
return { ok: false, ids: [], source: 'account-live', error: String(got.error || 'probe failed') + ' ' + REFRESH_HINT }
|
|
479
|
+
}
|
|
480
|
+
const ids = (got.models || []).map((m) => m.id).filter(Boolean)
|
|
481
|
+
if (!ids.length) {
|
|
482
|
+
// 走到这里说明探测自称成功却没给出任何 id —— 那是 bug,不是「无模型」,
|
|
483
|
+
// 必须显式失败,绝不能让面板显示一个空清单当成功。
|
|
484
|
+
return { ok: false, ids: [], source: 'account-live', error: '探测声称成功但未返回任何模型 id(内部不一致)。' + REFRESH_HINT }
|
|
485
|
+
}
|
|
486
|
+
// labels 只给「配置里还映射着某个 ANTHROPIC_*_MODEL」的项加注(纯展示,value 仍是 id)。
|
|
487
|
+
const labels = {}
|
|
488
|
+
for (const m of got.models) if (m.configuredAs) labels[m.id] = m.id + ' [' + m.configuredAs + ']'
|
|
489
|
+
return { ok: true, ids, labels, source: 'account-live' }
|
|
490
|
+
},
|
|
438
491
|
diagnose: (settings, env2) => {
|
|
439
492
|
const s = settings && typeof settings === 'object' ? settings : {}
|
|
440
493
|
const e = env2 && typeof env2 === 'object' ? env2 : {}
|