@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.
- package/CHANGELOG.md +91 -0
- package/PROVIDER-CONTRACT.md +87 -5
- package/README.md +12 -11
- package/docs/HANDOVER.md +109 -15
- package/dynamic/agy.host.js +1 -1
- package/dynamic/claude.host.js +1858 -0
- package/dynamic/claude.host.template.mjs +108 -0
- package/dynamic/codebuddy.host.js +1427 -1379
- package/dynamic/codex.host.js +1930 -0
- package/dynamic/codex.host.template.mjs +137 -0
- package/dynamic/mimo.host.js +721 -515
- package/dynamic/mimo.host.template.mjs +2 -0
- package/dynamic/opencode.host.js +741 -585
- package/dynamic/opencode.host.template.mjs +2 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +74 -90
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +48 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +2 -3
- package/mcp/cli-bridge-mcp.mjs +4 -2
- package/mcp/providers/claude.mjs +231 -0
- package/mcp/providers/codebuddy.mjs +427 -427
- package/mcp/providers/codex.mjs +239 -0
- package/package.json +6 -2
- package/preset/claude-first/agent.cordis.yml +136 -0
- package/preset/claude-first/cli-bridge.mjs +131 -0
- package/preset/claude-first/plugin-list.mjs +28 -0
- package/preset/claude-first/preset.yml +3 -0
- package/preset/codex-first/agent.cordis.yml +136 -0
- package/preset/codex-first/cli-bridge.mjs +160 -0
- package/preset/codex-first/plugin-list.mjs +28 -0
- package/preset/codex-first/preset.yml +3 -0
- package/providers/claude-exe.mjs +112 -0
- package/providers/claude-models.mjs +338 -0
- package/providers/claude-policy.mjs +37 -0
- package/providers/claude.mjs +561 -0
- 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 +405 -0
- package/providers/codex-models.mjs +107 -0
- package/providers/codex.mjs +585 -0
- 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/providers/registry.mjs +25 -1
- package/scripts/build.mjs +261 -262
- package/scripts/verify.mjs +8 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// providers/claude-models.mjs — claude 模型清单探测(账号实时权威源,纯逻辑 + 最小 spawn)。
|
|
2
|
+
//
|
|
3
|
+
// 从 providers/claude.mjs 拆出(规则 §26:单文件 600 行红线)。build 会把
|
|
4
|
+
// providers/claude-*.mjs 按字典序内联进 dynamic 沙箱同一作用域;ESM 形态走 import。
|
|
5
|
+
// ⚠️ 注释里绝不写出口令标记字面量。
|
|
6
|
+
//
|
|
7
|
+
// 为什么必须「探测」而不是读配置(本机 Claude Code 2.0.76 + 第三方 Anthropic 兼容端点实测):
|
|
8
|
+
// 传一个故意无效的模型名 → 服务端返回 400,CLI 把报文塞进 result 事件:
|
|
9
|
+
// {"type":"result","subtype":"success","is_error":true,
|
|
10
|
+
// "result":"API Error: 400 {\"error\":{\"message\":\"The supported API model names are
|
|
11
|
+
// deepseek-flash, deepseek-v4-pro, but you passed __clb_probe_invalid__.\"}}"}
|
|
12
|
+
// → 这条 "The supported API model names are X, Y" 就是该端点/该 key 的权威可用清单。
|
|
13
|
+
// ~/.claude/settings.json 的 env.ANTHROPIC_DEFAULT_*_MODEL 只是「别名映射」,
|
|
14
|
+
// 不是服务端可用全集 —— 红线:绝不把它冒充成可用清单,也绝不静默返回空数组。
|
|
15
|
+
//
|
|
16
|
+
// 缓存与「谁探测」(契约 §3.2 手动拉取):
|
|
17
|
+
// • settings.meta() 是同步回调,且**不再自动探测** —— 它只调 readCachedModels() 读这里的
|
|
18
|
+
// 缓存(纯读、不 spawn、不联网);没命中就留空,由 note 提示用户点「拉取可用模型」。
|
|
19
|
+
// • 探测只有一条路:settings.refreshModels() → 异步 probeSupportedModels()(用户显式触发)。
|
|
20
|
+
// 成功后经 cachedModels() 写回同一个缓存,下次 meta() 直接命中 —— 面板首屏不再被拖慢。
|
|
21
|
+
// • 5 分钟 TTL 只约束**工具调用**(claude_models 的重复调用);面板展示的是「上次拉取到的
|
|
22
|
+
// 权威清单」,过期也照旧展示,但 note 会标注拉取时间(宁可显式标旧,也不让下拉莫名清空)。
|
|
23
|
+
|
|
24
|
+
import { claudeExeCandidates, claudeProbeCandidates, splitExeSpec, quoteShellArg } from './claude-exe.mjs'
|
|
25
|
+
|
|
26
|
+
// 故意无效的探测模型名(服务端必然拒绝;保持带前缀,便于日志辨认来源)。
|
|
27
|
+
export const PROBE_MODEL = '__clb_probe_invalid__'
|
|
28
|
+
export const MODELS_TTL_MS = 5 * 60 * 1000
|
|
29
|
+
|
|
30
|
+
// 探测结果缓存:key(后端/端点标识) → { at, value }
|
|
31
|
+
const modelsCache = new Map()
|
|
32
|
+
|
|
33
|
+
/** 清空探测缓存(测试与「模型变更后刷新」用)。 */
|
|
34
|
+
export function resetModelsCache() { modelsCache.clear() }
|
|
35
|
+
|
|
36
|
+
// 缓存键 = 候选可执行文件集合(同一台机器可能切换 CLAUDE_BIN / 面板路径,键必须跟着变,
|
|
37
|
+
// 否则会把「上一个 exe 的探测结果」错配给新 exe)。
|
|
38
|
+
function candidateKey(env, binPath) {
|
|
39
|
+
const list = claudeProbeCandidates(env, binPath)
|
|
40
|
+
return list.length ? list.join('|') : 'claude'
|
|
41
|
+
}
|
|
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
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 缓存包装(异步工具路径):TTL 内直接返回上次成功结果;失败不写缓存。
|
|
55
|
+
* @param {string} key
|
|
56
|
+
* @param {() => Promise<object>|object} load
|
|
57
|
+
* @param {number} [nowMs]
|
|
58
|
+
*/
|
|
59
|
+
export function cachedModels(key, load, nowMs) {
|
|
60
|
+
const at = Number(nowMs) || Date.now()
|
|
61
|
+
const hit = modelsCache.get(key)
|
|
62
|
+
if (hit && at - hit.at < MODELS_TTL_MS) return hit.value
|
|
63
|
+
return Promise.resolve().then(load).then((v) => {
|
|
64
|
+
if (isUsableProbe(v)) modelsCache.set(key, { at, value: v })
|
|
65
|
+
return v
|
|
66
|
+
})
|
|
67
|
+
}
|
|
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
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 从 400 报文里解析「服务端支持的模型名」。
|
|
107
|
+
* 主格式(实测):`The supported API model names are A, B, but you passed Z.`
|
|
108
|
+
* 兼容:句尾直接是句号(无 "but you passed")、JSON 转义形态、大小写差异。
|
|
109
|
+
* @param {string} text
|
|
110
|
+
* @returns {string[]|null} 无该锚点返回 null(调用方必须据此报错,不得伪造清单)
|
|
111
|
+
*/
|
|
112
|
+
export function parseSupportedModels(text) {
|
|
113
|
+
const s = String(text || '')
|
|
114
|
+
if (!s) return null
|
|
115
|
+
const anchorRe = /the supported API model names are\s+/i
|
|
116
|
+
const m = anchorRe.exec(s)
|
|
117
|
+
if (!m) return null
|
|
118
|
+
const tail = s.slice(m.index + m[0].length)
|
|
119
|
+
// ① 有 "but you passed" → 这是清单的确定右边界(模型名里可以有 '.',不能按句点切)。
|
|
120
|
+
let body = ''
|
|
121
|
+
const end = /,?\s*but you passed/i.exec(tail)
|
|
122
|
+
if (end) body = tail.slice(0, end.index)
|
|
123
|
+
else {
|
|
124
|
+
// ② 无右边界锚点:截到引号/换行/行尾,再 chomp 单个句点。
|
|
125
|
+
const stop = tail.search(/["\r\n]/)
|
|
126
|
+
body = (stop >= 0 ? tail.slice(0, stop) : tail).replace(/[.\s]+$/, '')
|
|
127
|
+
}
|
|
128
|
+
const ids = []
|
|
129
|
+
for (const raw of body.split(/\s*,\s*|\s+and\s+/i)) {
|
|
130
|
+
const id = String(raw || '').trim().replace(/^["'`[]+|["'`\]]+$/g, '')
|
|
131
|
+
if (!id) continue
|
|
132
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:@/+-]*$/.test(id)) continue
|
|
133
|
+
if (id === PROBE_MODEL) continue
|
|
134
|
+
if (ids.indexOf(id) < 0) ids.push(id)
|
|
135
|
+
}
|
|
136
|
+
return ids.length ? ids : null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 从探测进程的完整输出里挖出模型清单。
|
|
141
|
+
* CLI 的 stdout 是 result 事件 JSON(is_error:true);它的 result 字段里才是 400 报文,
|
|
142
|
+
* 且报文自身是 JSON 字符串(转义)。顺序:raw 文本 → 每行 JSON 的 result 字段。
|
|
143
|
+
* @param {string} stdoutText
|
|
144
|
+
* @param {string} [stderrText]
|
|
145
|
+
* @returns {{ids: string[]|null, errorText: string, sawResult: boolean}}
|
|
146
|
+
*/
|
|
147
|
+
export function extractProbeIds(stdoutText, stderrText) {
|
|
148
|
+
const out = String(stdoutText || '')
|
|
149
|
+
const err = String(stderrText || '')
|
|
150
|
+
const lines = out.split(/\r?\n/).filter(Boolean)
|
|
151
|
+
let errorText = ''
|
|
152
|
+
let sawResult = false
|
|
153
|
+
for (const ln of lines) {
|
|
154
|
+
const t = ln.trim()
|
|
155
|
+
if (!t.startsWith('{')) continue
|
|
156
|
+
let obj
|
|
157
|
+
try { obj = JSON.parse(t) } catch (e) { continue }
|
|
158
|
+
if (!obj || obj.type !== 'result') continue
|
|
159
|
+
sawResult = true
|
|
160
|
+
if (typeof obj.result === 'string' && obj.result) errorText = obj.result
|
|
161
|
+
else if (obj.is_error && typeof obj.error === 'string') errorText = obj.error
|
|
162
|
+
}
|
|
163
|
+
if (!errorText) errorText = err || out
|
|
164
|
+
const ids = parseSupportedModels(errorText) || parseSupportedModels(out) || parseSupportedModels(err)
|
|
165
|
+
return { ids: ids || null, errorText: errorText.slice(0, 800), sawResult }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** ~/.claude/settings.json 路径(CLAUDE_CONFIG_DIR 可覆盖配置根,本机实测存在)。 */
|
|
169
|
+
export function claudeConfigDir(env) {
|
|
170
|
+
const e = env || {}
|
|
171
|
+
if (e.CLAUDE_CONFIG_DIR) return String(e.CLAUDE_CONFIG_DIR)
|
|
172
|
+
const home = e.USERPROFILE || e.HOME || ''
|
|
173
|
+
if (!home) return ''
|
|
174
|
+
const path = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:path') : null } catch (x) { return null } })()
|
|
175
|
+
if (!path || typeof path.join !== 'function') return ''
|
|
176
|
+
return path.join(home, '.claude')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 读 ~/.claude/settings.json 的 env 块里「用户配置的模型名」。
|
|
181
|
+
* ⚠️ 这是别名映射(ANTHROPIC_DEFAULT_OPUS_MODEL 之类),**不是**服务端可用全集:
|
|
182
|
+
* 只作为诊断/解释线索返回,绝不塞进 ok:true 的模型清单。
|
|
183
|
+
* @returns {{ok: boolean, path: string, models: Array<{id: string, key: string}>, error: string}}
|
|
184
|
+
*/
|
|
185
|
+
export function readConfiguredModels(env) {
|
|
186
|
+
const dir = claudeConfigDir(env)
|
|
187
|
+
if (!dir) return { ok: false, path: '', models: [], error: 'cannot resolve home directory (no USERPROFILE/HOME/CLAUDE_CONFIG_DIR)' }
|
|
188
|
+
const fs = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:fs') : null } catch (x) { return null } })()
|
|
189
|
+
const path = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:path') : null } catch (x) { return null } })()
|
|
190
|
+
if (!fs || !path) return { ok: false, path: '', models: [], error: 'no fs capability in this host form' }
|
|
191
|
+
const file = path.join(dir, 'settings.json')
|
|
192
|
+
let raw
|
|
193
|
+
try { raw = JSON.parse(fs.readFileSync(file, 'utf8')) } catch (e) {
|
|
194
|
+
return { ok: false, path: file, models: [], error: 'cannot read ' + file + ': ' + String(e && e.message || e) }
|
|
195
|
+
}
|
|
196
|
+
const envBlock = raw && raw.env && typeof raw.env === 'object' ? raw.env : {}
|
|
197
|
+
const models = []
|
|
198
|
+
const seen = Object.create(null)
|
|
199
|
+
for (const key of Object.keys(envBlock)) {
|
|
200
|
+
if (!/^(ANTHROPIC_(MODEL|DEFAULT_[A-Z]+_MODEL|DEFAULT_[A-Z]+_MODEL_NAME|SMALL_FAST_MODEL))$/i.test(key)) continue
|
|
201
|
+
const v = envBlock[key]
|
|
202
|
+
if (typeof v !== 'string' || !v.trim()) continue
|
|
203
|
+
const id = v.trim()
|
|
204
|
+
if (seen[id]) continue
|
|
205
|
+
seen[id] = true
|
|
206
|
+
models.push({ id, key })
|
|
207
|
+
}
|
|
208
|
+
return { ok: true, path: file, models, error: '' }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 探测 argv(不含 exe 前缀):固定参数集,绝不拼装用户 prompt。 */
|
|
212
|
+
export function probeArgv() {
|
|
213
|
+
return ['-p', 'hi', '--model', PROBE_MODEL, '--output-format', 'json', '--no-session-persistence']
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 探测失败时的可操作文案(区分「没找到 CLI」与「CLI 跑了但服务端没报清单」)。 */
|
|
217
|
+
function probeFailureText(reasons, tried) {
|
|
218
|
+
const head = '无法获取服务端权威模型清单(已尝试探测 ' + tried + ' 个候选可执行文件)'
|
|
219
|
+
const body = reasons.join('; ')
|
|
220
|
+
return head + (body ? ': ' + body : '') +
|
|
221
|
+
'。请确认 claude CLI 已安装且端点/凭据可用(claude 在 ~/.claude/settings.json 的 env 里读 ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN),或在设置面板填写正确的可执行文件路径后重试。'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 异步探测(唯一探测实现:MCP / preset / dynamic 三形态 + 面板「拉取可用模型」共用):
|
|
226
|
+
* 起一次真实 CLI 调用,解析 400 报文。
|
|
227
|
+
* 铁律:只认探测结果;失败 = ok:false + 可操作错误,绝不回落到配置文件/静态表。
|
|
228
|
+
* @param {object} env
|
|
229
|
+
* @param {{binPath?: string, timeoutMs?: number}} [opts]
|
|
230
|
+
* @returns {Promise<{ok: boolean, ids: string[], mode: string, exe?: string, error?: string}>}
|
|
231
|
+
*/
|
|
232
|
+
export function probeSupportedModels(env, opts) {
|
|
233
|
+
const o = opts || {}
|
|
234
|
+
const child = (function () { try { const p = globalThis.process; return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule('node:child_process') : null } catch (x) { return null } })()
|
|
235
|
+
if (!child || typeof child.spawn !== 'function') {
|
|
236
|
+
return Promise.resolve({ ok: false, ids: [], mode: 'account-live', error: 'no spawn capability in this host form' })
|
|
237
|
+
}
|
|
238
|
+
const cands = claudeProbeCandidates(env, o.binPath)
|
|
239
|
+
const probeList = cands.length ? cands : ['claude']
|
|
240
|
+
const proc = globalThis.process
|
|
241
|
+
const env2 = Object.assign({}, (proc && proc.env) || {})
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
let idx = 0
|
|
244
|
+
const reasons = []
|
|
245
|
+
const attempt = () => {
|
|
246
|
+
if (idx >= probeList.length) {
|
|
247
|
+
resolve({ ok: false, ids: [], mode: 'account-live', error: probeFailureText(reasons, probeList.length) })
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
const exe = probeList[idx++]
|
|
251
|
+
const spec = splitExeSpec(exe)
|
|
252
|
+
const argv = spec.prefixArgs.concat(probeArgv())
|
|
253
|
+
let c
|
|
254
|
+
try {
|
|
255
|
+
const spawnCmd = spec.shell ? [spec.cmd].concat(argv).map(quoteShellArg).join(' ') : spec.cmd
|
|
256
|
+
c = child.spawn(spawnCmd, spec.shell ? [] : argv, {
|
|
257
|
+
stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: !!spec.shell, env: env2
|
|
258
|
+
})
|
|
259
|
+
} catch (e) { reasons.push(exe + ': spawn threw ' + String(e && e.message || e)); attempt(); return }
|
|
260
|
+
let out = ''
|
|
261
|
+
let err = ''
|
|
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
|
+
}
|
|
274
|
+
c.stdout.on('data', (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) })
|
|
275
|
+
c.stderr.on('data', (d) => { err += d; if (err.length > 200000) err = err.slice(-100000) })
|
|
276
|
+
c.on('error', (e) => {
|
|
277
|
+
nextCandidate(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
|
|
278
|
+
})
|
|
279
|
+
c.on('close', (code) => {
|
|
280
|
+
if (settled) return
|
|
281
|
+
const got = extractProbeIds(out, err)
|
|
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
|
+
}
|
|
288
|
+
const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
289
|
+
nextCandidate(exe + ': exit ' + code + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
attempt()
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 组装 claude_models 的返回值(三形态共用;工具逻辑与 MCP 面共用这一份,杜绝两处漂移)。
|
|
298
|
+
* 成功 = 服务端清单(source: account-live);失败 = ok:false + configured 诊断线索
|
|
299
|
+
* (配置里那串名字只作「你可能想用哪个」的提示,明确标注非权威)。
|
|
300
|
+
* @param {object} env
|
|
301
|
+
* @param {{binPath?: string, timeoutMs?: number, refresh?: boolean}} [opts]
|
|
302
|
+
*/
|
|
303
|
+
export async function listAccountModels(env, opts) {
|
|
304
|
+
const o = opts || {}
|
|
305
|
+
const key = modelsCacheKey(env, o.binPath)
|
|
306
|
+
if (o.refresh) modelsCache.delete(key)
|
|
307
|
+
const probe = await cachedModels(key, () => probeSupportedModels(env, o))
|
|
308
|
+
const cfg = readConfiguredModels(env)
|
|
309
|
+
const configured = cfg.models || []
|
|
310
|
+
if (!probe.ok) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
source: 'account-live',
|
|
314
|
+
models: [],
|
|
315
|
+
count: 0,
|
|
316
|
+
error: String(probe.error || 'probe failed'),
|
|
317
|
+
diagnostic: {
|
|
318
|
+
configured: configured,
|
|
319
|
+
settingsPath: cfg.path,
|
|
320
|
+
note: 'ANTHROPIC_*_MODEL 只是用户配置的别名映射,不是服务端可用全集;因此不作为 models 返回。'
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const ids = probe.ids.slice()
|
|
325
|
+
const byId = new Map(configured.map((m) => [m.id, m]))
|
|
326
|
+
const byIdLoose = new Map(configured.map((m) => [String(m.id).toLowerCase().replace(/\[.*\]$/, ''), m]))
|
|
327
|
+
return {
|
|
328
|
+
ok: true,
|
|
329
|
+
source: 'account-live',
|
|
330
|
+
models: ids.map((id) => {
|
|
331
|
+
const hit = byId.get(id) || byIdLoose.get(String(id).toLowerCase())
|
|
332
|
+
return { id, source: 'account-live', configuredAs: hit ? hit.key : null }
|
|
333
|
+
}),
|
|
334
|
+
count: ids.length,
|
|
335
|
+
ext: { exe: probe.exe || null, mode: probe.mode },
|
|
336
|
+
diagnostic: { settingsPath: cfg.path, configured: configured }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// providers/claude-policy.mjs — claude 提示词策略段(preset 与 dynamic 两种宿主唯一来源)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独成片:同一段策略文本要在 preset/claude-first/cli-bridge.mjs(ESM 导入)与
|
|
4
|
+
// dynamic/claude.host.template.mjs(沙箱内联,禁 import)两处出现;各写一份必然漂移
|
|
5
|
+
// (历史上 agy/opencode 两处文案已经出现过不一致)。本文件被 scripts/build.mjs 作为
|
|
6
|
+
// providers/claude-*.mjs 之一内联进沙箱同一作用域,故两形态共用同一个常量。
|
|
7
|
+
// ⚠️ 注释里绝不写出口令标记字面量(build 全局替换会把注释劈开)。
|
|
8
|
+
|
|
9
|
+
export const POLICY_TEXT = [
|
|
10
|
+
'claude-first execution policy (local Claude Code CLI bridge).',
|
|
11
|
+
'',
|
|
12
|
+
'Local agent CLI "claude" handles coding tasks under full DSH control (non-interactive -p mode, permissions auto-approved so claude never blocks on a prompt). By default, claude runs in the background and returns a jobId immediately; collect results via job_output and watch live progress via claude_status anytime.',
|
|
13
|
+
'',
|
|
14
|
+
'Decision table for tool selection:',
|
|
15
|
+
'- Single read/glob/grep or short (<30s) command -> Native tools.',
|
|
16
|
+
'- Implementing, editing, refactoring, debugging, builds, multi-file investigation -> claude_run.',
|
|
17
|
+
'- Follow-ups on the same topic -> claude_continue (with latest=true or sessionId; claude preserves session context via --resume).',
|
|
18
|
+
'- Final build/test verification of claude output -> Native tools.',
|
|
19
|
+
'',
|
|
20
|
+
'Context protocol (one topic = one claude session):',
|
|
21
|
+
'- First dispatch on a topic: provide the task with a compact CONTEXT preamble (point to paths, do not paste file contents):',
|
|
22
|
+
' CONTEXT: 2-6 lines — goal; repo root; key paths; decisions/constraints; current state.',
|
|
23
|
+
' TASK: the complete instruction.',
|
|
24
|
+
'- Follow-ups: call claude_continue and state only what is new.',
|
|
25
|
+
'',
|
|
26
|
+
'Execution & Fallback protocol:',
|
|
27
|
+
'- Dispatch defaults to background=true (set background=false only to block). Collect via job_output.',
|
|
28
|
+
'- mode=auto follows the DSH plan state: in plan mode claude runs with --permission-mode plan (read-only), otherwise permissions are bypassed so long edits/builds never stall on an approval prompt.',
|
|
29
|
+
'- On rate-limit or network failure, a confirmation dialog opens in BOTH foreground and background while in flight.',
|
|
30
|
+
'- If result has fallback=true (status FALLBACK_TO_DSH), the user chose DSH-local: finish with native tools and NEVER call claude again for this task.',
|
|
31
|
+
'- Never loop claude calls; never ask claude to call back into DSH.',
|
|
32
|
+
'',
|
|
33
|
+
'Model & credentials:',
|
|
34
|
+
'- Which model ids actually work is decided by the ENDPOINT, not by this bridge: call claude_models to read the authoritative server-side roster (it probes the endpoint), then pass model. Never trust ANTHROPIC_*_MODEL entries in ~/.claude/settings.json as the usable set — they are alias mappings only.',
|
|
35
|
+
'- Omit the model unless you have a reason: claude then uses its own configuration (or the model chosen in CLI Bridge settings). A rejected model surfaces as a run failure with the supported list in the diagnostic hint — fix the model, do not retry blindly.',
|
|
36
|
+
'- Credentials are owned by claude, not by this bridge: base URL and auth token live in ~/.claude/settings.json (env block). On auth/401 failures tell the user to check that file or re-login; do not retry in a loop.'
|
|
37
|
+
].join('\n')
|