@luchenglong/cli-bridge 0.2.9 → 0.2.11
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 +73 -0
- package/PROVIDER-CONTRACT.md +56 -5
- package/docs/HANDOVER.md +109 -15
- package/dynamic/claude.host.js +362 -106
- 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-endpoint.mjs +132 -0
- package/providers/claude-models.mjs +141 -101
- package/providers/claude.mjs +88 -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.11",
|
|
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
|
},
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// providers/claude-endpoint.mjs — claude 的端点解析(第三片)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要这一层(真实事故,2026-10-20):
|
|
4
|
+
// 用户用 cc-switch 管理 Claude 配置,其「proxy 模式」把 Claude 指向第三方网关
|
|
5
|
+
// (例:ANTHROPIC_BASE_URL=https://opencode.ai/zen/go)。该网关要求自家 session 头
|
|
6
|
+
// (`x-opencode-session` / MissingSessionID),claude CLI 不会发 → **直连必然 400**。
|
|
7
|
+
// 能用是因为 cc-switch 在本机起了一个转发代理,由它补头/认证:
|
|
8
|
+
// 实测 POST http://127.0.0.1:15721/v1/messages → 200;同一配置直连 → 400 MissingSessionID。
|
|
9
|
+
// 所以「把 base_url 换成 cc-switch 代理」不是降级,而是这套配置**唯一可行**的访问方式。
|
|
10
|
+
//
|
|
11
|
+
// 注入方式:`--settings <json>` 覆盖 env.ANTHROPIC_BASE_URL,**不改用户的
|
|
12
|
+
// ~/.claude/settings.json**(那是 cc-switch 的领地,改它会在下次切换时被覆盖,也可能
|
|
13
|
+
// 破坏用户的 GUI 会话)。
|
|
14
|
+
//
|
|
15
|
+
// 探活采用「起一次 node 子进程做 HTTP GET」:buildArgv/meta 都是同步回调,Node 无同步
|
|
16
|
+
// HTTP。结果按 URL 缓存 30s,避免每次调用都起进程。探不到就如实回落直连(并在诊断里
|
|
17
|
+
// 说明"代理未响应"),绝不再假装已接管。
|
|
18
|
+
|
|
19
|
+
/** 默认的 cc-switch 本地代理地址(其 proxy_config.listen_port 实测为 15721)。 */
|
|
20
|
+
export const DEFAULT_PROXY_URL = 'http://127.0.0.1:15721'
|
|
21
|
+
|
|
22
|
+
/** 端点模式:auto = 代理活着就用它;proxy = 强制;direct = 强制直连。 */
|
|
23
|
+
export const ENDPOINT_MODES = ['auto', 'proxy', 'direct']
|
|
24
|
+
|
|
25
|
+
const PROXY_TTL_MS = 30 * 1000
|
|
26
|
+
let proxyCache = { url: '', at: 0, alive: false, detail: '' }
|
|
27
|
+
|
|
28
|
+
/** 允许测试注入探测结果,避免每个用例都起子进程。 */
|
|
29
|
+
export function resetProxyCache() { proxyCache = { url: '', at: 0, alive: false, detail: '' } }
|
|
30
|
+
|
|
31
|
+
function builtin(name) {
|
|
32
|
+
const p = globalThis.process
|
|
33
|
+
try { return (p && typeof p.getBuiltinModule === 'function') ? p.getBuiltinModule(name) : null } catch (e) { return null }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 探活子脚本:对 url 发 GET,任何 HTTP 响应都算活着(404 也算 —— cc-switch 根路径就是 404)。 */
|
|
37
|
+
const PROBE_SCRIPT = [
|
|
38
|
+
"const http=require('http'),https=require('https');",
|
|
39
|
+
"const u=process.argv[1];",
|
|
40
|
+
"const m=/^https:/i.test(u)?https:http;",
|
|
41
|
+
"const r=m.get(u,{timeout:1500},(res)=>{res.resume();console.log('ALIVE '+res.statusCode);process.exit(0)});",
|
|
42
|
+
"r.on('error',(e)=>{console.log('DEAD '+(e.code||e.message));process.exit(0)});",
|
|
43
|
+
"r.on('timeout',()=>{console.log('DEAD timeout');try{r.destroy()}catch(e){};process.exit(0)});"
|
|
44
|
+
].join('')
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 同步探活(带 30s 缓存)。返回 { alive, detail }。
|
|
48
|
+
* 起 `node -e` 子进程是唯一能在同步回调里做真实 HTTP 的办法;代价约 100-250ms,
|
|
49
|
+
* 且被 URL 维度缓存 —— 只在「首次」或「缓存过期」时付。
|
|
50
|
+
*/
|
|
51
|
+
export function detectProxySync(url, nowMs) {
|
|
52
|
+
const target = String(url || '').trim()
|
|
53
|
+
if (!target) return { alive: false, detail: 'no url' }
|
|
54
|
+
const at = Number(nowMs) || Date.now()
|
|
55
|
+
if (proxyCache.url === target && at - proxyCache.at < PROXY_TTL_MS) {
|
|
56
|
+
return { alive: proxyCache.alive, detail: proxyCache.detail }
|
|
57
|
+
}
|
|
58
|
+
const child = builtin('node:child_process')
|
|
59
|
+
const proc = globalThis.process
|
|
60
|
+
let alive = false
|
|
61
|
+
let detail = 'no child_process'
|
|
62
|
+
if (child && typeof child.spawnSync === 'function' && proc && proc.execPath) {
|
|
63
|
+
try {
|
|
64
|
+
const r = child.spawnSync(proc.execPath, ['-e', PROBE_SCRIPT, target], {
|
|
65
|
+
encoding: 'utf8', timeout: 4000, windowsHide: true,
|
|
66
|
+
env: Object.assign({}, proc.env)
|
|
67
|
+
})
|
|
68
|
+
const out = String((r && r.stdout) || '')
|
|
69
|
+
alive = /^ALIVE\b/m.test(out)
|
|
70
|
+
detail = out.trim().split(/\r?\n/).pop() || (r && r.error && (r.error.code || r.error.message)) || 'unknown'
|
|
71
|
+
} catch (e) { detail = 'probe threw: ' + String(e && e.message || e) }
|
|
72
|
+
}
|
|
73
|
+
proxyCache = { url: target, at, alive, detail }
|
|
74
|
+
return { alive, detail }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 读 ~/.claude/settings.json 里配置的 ANTHROPIC_BASE_URL(只读,不写)。 */
|
|
78
|
+
export function readConfiguredBaseUrl(env, configDir) {
|
|
79
|
+
const fs = builtin('node:fs')
|
|
80
|
+
const path = builtin('node:path')
|
|
81
|
+
if (!fs || !path) return ''
|
|
82
|
+
const dir = configDir || ((env && (env.CLAUDE_CONFIG_DIR || env.CLAUDE_HOME)) || '')
|
|
83
|
+
if (!dir) return ''
|
|
84
|
+
try {
|
|
85
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, 'settings.json'), 'utf8'))
|
|
86
|
+
const block = raw && raw.env && typeof raw.env === 'object' ? raw.env : {}
|
|
87
|
+
return typeof block.ANTHROPIC_BASE_URL === 'string' ? block.ANTHROPIC_BASE_URL.trim() : ''
|
|
88
|
+
} catch (e) { return '' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 解析最终端点。
|
|
93
|
+
* @returns {{ mode: 'proxy'|'direct', baseUrl: string, proxyUrl: string, proxyAlive: boolean,
|
|
94
|
+
* configuredBaseUrl: string, note: string, error?: string }}
|
|
95
|
+
*/
|
|
96
|
+
export function resolveEndpoint(settings, env, opts) {
|
|
97
|
+
const o = opts || {}
|
|
98
|
+
const s = settings && typeof settings === 'object' ? settings : {}
|
|
99
|
+
const mode = ENDPOINT_MODES.indexOf(String(s.endpointMode || '')) >= 0 ? String(s.endpointMode) : 'auto'
|
|
100
|
+
const proxyUrl = String(s.proxyUrl || DEFAULT_PROXY_URL).trim() || DEFAULT_PROXY_URL
|
|
101
|
+
const configuredBaseUrl = readConfiguredBaseUrl(env, o.configDir)
|
|
102
|
+
|
|
103
|
+
if (mode === 'direct') {
|
|
104
|
+
return { mode: 'direct', baseUrl: configuredBaseUrl, proxyUrl, proxyAlive: false, configuredBaseUrl, note: '强制直连(endpointMode=direct)' }
|
|
105
|
+
}
|
|
106
|
+
const det = o.probe ? o.probe(proxyUrl) : detectProxySync(proxyUrl)
|
|
107
|
+
if (det.alive) {
|
|
108
|
+
return {
|
|
109
|
+
mode: 'proxy', baseUrl: proxyUrl, proxyUrl, proxyAlive: true, configuredBaseUrl,
|
|
110
|
+
note: '经 cc-switch 本地代理 ' + proxyUrl + '(配置的端点 ' + (configuredBaseUrl || '(未读到)') + ' 需要代理补头)'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (mode === 'proxy') {
|
|
114
|
+
return {
|
|
115
|
+
mode: 'proxy', baseUrl: proxyUrl, proxyUrl, proxyAlive: false, configuredBaseUrl,
|
|
116
|
+
note: '强制走代理但探测失败:' + det.detail,
|
|
117
|
+
error: 'cc-switch 本地代理 ' + proxyUrl + ' 无响应(' + det.detail + ')。请启动 cc-switch(含其本地代理),' +
|
|
118
|
+
'或把「端点模式」改为 direct 直连,或在「代理地址」里填实际端口。'
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
mode: 'direct', baseUrl: configuredBaseUrl, proxyUrl, proxyAlive: false, configuredBaseUrl,
|
|
123
|
+
note: '直连配置的端点 ' + (configuredBaseUrl || '(未读到)') + '(本地代理 ' + proxyUrl + ' 未响应:' + det.detail + ')。' +
|
|
124
|
+
'若该端点来自 cc-switch 的 proxy 模式,请启动 cc-switch —— 否则会被网关以缺 session 头拒绝。'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 把端点覆盖编成 claude 的 `--settings` JSON(只覆盖 base_url,不动其它键)。 */
|
|
129
|
+
export function overlaySettingsJson(baseUrl) {
|
|
130
|
+
if (!baseUrl) return ''
|
|
131
|
+
return JSON.stringify({ env: { ANTHROPIC_BASE_URL: String(baseUrl) } })
|
|
132
|
+
}
|
|
@@ -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.`
|
|
@@ -158,8 +209,15 @@ export function readConfiguredModels(env) {
|
|
|
158
209
|
}
|
|
159
210
|
|
|
160
211
|
/** 探测 argv(不含 exe 前缀):固定参数集,绝不拼装用户 prompt。 */
|
|
161
|
-
|
|
162
|
-
|
|
212
|
+
/**
|
|
213
|
+
* 探测用的 argv。`baseUrl` 非空时用 `--settings` 覆盖 ANTHROPIC_BASE_URL ——
|
|
214
|
+
* 这是 cc-switch proxy 模式唯一可行的访问方式(直连第三方网关缺 session 头必 400)。
|
|
215
|
+
* 只覆盖 base_url,不碰用户的 ~/.claude/settings.json。
|
|
216
|
+
*/
|
|
217
|
+
export function probeArgv(baseUrl) {
|
|
218
|
+
const argv = ['-p', 'hi', '--model', PROBE_MODEL, '--output-format', 'json', '--no-session-persistence']
|
|
219
|
+
if (baseUrl) argv.push('--settings', JSON.stringify({ env: { ANTHROPIC_BASE_URL: String(baseUrl) } }))
|
|
220
|
+
return argv
|
|
163
221
|
}
|
|
164
222
|
|
|
165
223
|
/** 探测失败时的可操作文案(区分「没找到 CLI」与「CLI 跑了但服务端没报清单」)。 */
|
|
@@ -171,68 +229,8 @@ function probeFailureText(reasons, tried) {
|
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
/**
|
|
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 报文。
|
|
232
|
+
* 异步探测(唯一探测实现:MCP / preset / dynamic 三形态 + 面板「拉取可用模型」共用):
|
|
233
|
+
* 起一次真实 CLI 调用,解析 400 报文。
|
|
236
234
|
* 铁律:只认探测结果;失败 = ok:false + 可操作错误,绝不回落到配置文件/静态表。
|
|
237
235
|
* @param {object} env
|
|
238
236
|
* @param {{binPath?: string, timeoutMs?: number}} [opts]
|
|
@@ -258,7 +256,7 @@ export function probeSupportedModels(env, opts) {
|
|
|
258
256
|
}
|
|
259
257
|
const exe = probeList[idx++]
|
|
260
258
|
const spec = splitExeSpec(exe)
|
|
261
|
-
const argv = spec.prefixArgs.concat(probeArgv())
|
|
259
|
+
const argv = spec.prefixArgs.concat(probeArgv(o.baseUrl))
|
|
262
260
|
let c
|
|
263
261
|
try {
|
|
264
262
|
const spawnCmd = spec.shell ? [spec.cmd].concat(argv).map(quoteShellArg).join(' ') : spec.cmd
|
|
@@ -269,20 +267,33 @@ export function probeSupportedModels(env, opts) {
|
|
|
269
267
|
let out = ''
|
|
270
268
|
let err = ''
|
|
271
269
|
const timer = setTimeout(() => { try { c.kill() } catch (e) { } }, Math.max(5000, Math.min(Number(o.timeoutMs) || 30000, 90000)))
|
|
270
|
+
// 一个候选只允许结算一次:spawn 失败时 'error' 与 'close' 会先后都触发,
|
|
271
|
+
// 两个回调各调一次 attempt() 会把后面的候选整个跳过(idx 多走一格)——
|
|
272
|
+
// 实测后果是「binPath 填错 → 明明 CLAUDE_BIN 里那个能跑,探测却直接失败」。
|
|
273
|
+
let settled = false
|
|
274
|
+
const nextCandidate = (reason) => {
|
|
275
|
+
if (settled) return
|
|
276
|
+
settled = true
|
|
277
|
+
clearTimeout(timer)
|
|
278
|
+
if (reason) reasons.push(reason)
|
|
279
|
+
attempt()
|
|
280
|
+
}
|
|
272
281
|
c.stdout.on('data', (d) => { out += d; if (out.length > 400000) out = out.slice(-200000) })
|
|
273
282
|
c.stderr.on('data', (d) => { err += d; if (err.length > 200000) err = err.slice(-100000) })
|
|
274
283
|
c.on('error', (e) => {
|
|
275
|
-
|
|
276
|
-
reasons.push(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
|
|
277
|
-
attempt()
|
|
284
|
+
nextCandidate(exe + ': ' + String((e && e.code) || (e && e.message) || 'spawn error'))
|
|
278
285
|
})
|
|
279
286
|
c.on('close', (code) => {
|
|
280
|
-
|
|
287
|
+
if (settled) return
|
|
281
288
|
const got = extractProbeIds(out, err)
|
|
282
|
-
if (got.ids && got.ids.length) {
|
|
289
|
+
if (got.ids && got.ids.length) {
|
|
290
|
+
settled = true
|
|
291
|
+
clearTimeout(timer)
|
|
292
|
+
resolve({ ok: true, ids: got.ids, mode: 'account-live', exe })
|
|
293
|
+
return
|
|
294
|
+
}
|
|
283
295
|
const head = String(got.errorText || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
284
|
-
|
|
285
|
-
attempt()
|
|
296
|
+
nextCandidate(exe + ': exit ' + code + ',未解析出可用模型清单' + (head ? ' — ' + head : ''))
|
|
286
297
|
})
|
|
287
298
|
}
|
|
288
299
|
attempt()
|
|
@@ -296,39 +307,68 @@ export function probeSupportedModels(env, opts) {
|
|
|
296
307
|
* @param {object} env
|
|
297
308
|
* @param {{binPath?: string, timeoutMs?: number, refresh?: boolean}} [opts]
|
|
298
309
|
*/
|
|
310
|
+
/**
|
|
311
|
+
* 列出可用模型 —— **合并两个来源**,而不是只认服务端报文。
|
|
312
|
+
*
|
|
313
|
+
* 为什么必须合并(真实事故):服务端「无效模型触发 400」只回吐**基础模型名**。
|
|
314
|
+
* 实测 DeepSeek 的 Anthropic 兼容端点报 `deepseek-flash, deepseek-v4-pro`,
|
|
315
|
+
* 但用户实际配置在用的 `deepseek-v4-pro[1M]` / `deepseek-v4-flash` 逐个实测都是 200 ——
|
|
316
|
+
* 它们只是带上下文窗口后缀/命名变体,服务端报文不列。若只信报文,用户会看到一份
|
|
317
|
+
* 「比我配的还少」的清单,等于给错答案。
|
|
318
|
+
*
|
|
319
|
+
* 因此:configured(`~/.claude/settings.json` 的 env,cc-switch 等工具写入的**最终生效**
|
|
320
|
+
* 配置)+ account-live(服务端报文)取并集,逐项标注来源;configured 排在前面。
|
|
321
|
+
*
|
|
322
|
+
* 且 configured **不依赖探测成功** —— 端点若根本不支持 400 技巧(如某些代理对任何
|
|
323
|
+
* 模型都先校验 session),我们仍能把用户配置的模型给出来,而不是返回空。
|
|
324
|
+
*/
|
|
299
325
|
export async function listAccountModels(env, opts) {
|
|
300
326
|
const o = opts || {}
|
|
301
|
-
const key =
|
|
327
|
+
const key = modelsCacheKey(env, o.binPath)
|
|
302
328
|
if (o.refresh) modelsCache.delete(key)
|
|
303
329
|
const probe = await cachedModels(key, () => probeSupportedModels(env, o))
|
|
304
330
|
const cfg = readConfiguredModels(env)
|
|
305
331
|
const configured = cfg.models || []
|
|
306
|
-
|
|
332
|
+
|
|
333
|
+
// 大小写不敏感 + 忽略 [1m]/[1M] 这类后缀后做去重键,避免同一模型重复占两行。
|
|
334
|
+
const dk = (id) => String(id).trim().toLowerCase().replace(/\[[^\]]*\]$/, '')
|
|
335
|
+
const seen = new Set()
|
|
336
|
+
const models = []
|
|
337
|
+
for (const m of configured) {
|
|
338
|
+
const k = dk(m.id)
|
|
339
|
+
if (seen.has(k)) continue
|
|
340
|
+
seen.add(k)
|
|
341
|
+
models.push({ id: m.id, source: 'configured', configuredAs: m.key })
|
|
342
|
+
}
|
|
343
|
+
const probeIds = probe && probe.ok ? (probe.ids || []) : []
|
|
344
|
+
for (const id of probeIds) {
|
|
345
|
+
const k = dk(id)
|
|
346
|
+
if (seen.has(k)) continue
|
|
347
|
+
seen.add(k)
|
|
348
|
+
models.push({ id, source: 'account-live', configuredAs: null })
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!models.length) {
|
|
307
352
|
return {
|
|
308
|
-
ok: false,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
error: String(probe.error || 'probe failed'),
|
|
313
|
-
diagnostic: {
|
|
314
|
-
configured: configured,
|
|
315
|
-
settingsPath: cfg.path,
|
|
316
|
-
note: 'ANTHROPIC_*_MODEL 只是用户配置的别名映射,不是服务端可用全集;因此不作为 models 返回。'
|
|
317
|
-
}
|
|
353
|
+
ok: false, source: 'none', models: [], count: 0,
|
|
354
|
+
error: String((probe && probe.error) || 'no configured models and probe returned nothing') +
|
|
355
|
+
(cfg.error ? ';settings.json: ' + cfg.error : ''),
|
|
356
|
+
diagnostic: { configured, settingsPath: cfg.path, probeError: (probe && probe.error) || null }
|
|
318
357
|
}
|
|
319
358
|
}
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
359
|
+
// source 汇总:两个来源都有效时标注合并,便于面板/工具如实显示。
|
|
360
|
+
const source = (configured.length && probeIds.length) ? 'configured+account-live'
|
|
361
|
+
: (configured.length ? 'configured' : 'account-live')
|
|
323
362
|
return {
|
|
324
363
|
ok: true,
|
|
325
|
-
source
|
|
326
|
-
models
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
364
|
+
source,
|
|
365
|
+
models,
|
|
366
|
+
count: models.length,
|
|
367
|
+
ext: { exe: (probe && probe.exe) || null, mode: (probe && probe.mode) || null },
|
|
368
|
+
diagnostic: {
|
|
369
|
+
settingsPath: cfg.path,
|
|
370
|
+
configured,
|
|
371
|
+
probeError: (probe && !probe.ok) ? String(probe.error || 'probe failed') : null
|
|
372
|
+
}
|
|
333
373
|
}
|
|
334
374
|
}
|
package/providers/claude.mjs
CHANGED
|
@@ -21,15 +21,29 @@
|
|
|
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, claudeConfigDir } from './claude-models.mjs'
|
|
25
|
+
import { ENDPOINT_MODES, DEFAULT_PROXY_URL, resolveEndpoint, overlaySettingsJson, readConfiguredBaseUrl } from './claude-endpoint.mjs'
|
|
25
26
|
// 单行 re-export(build 内联时整行剥离;多行形态风险见 build.mjs 的 stripModuleSyntax 注释)。
|
|
26
|
-
export { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, PROBE_MODEL, parseSupportedModels, probeSupportedModels, readConfiguredModels, listAccountModels, resetModelsCache }
|
|
27
|
+
export { claudeExeCandidates, claudePathHint, resolveExePrefix, splitExeSpec, PROBE_MODEL, parseSupportedModels, probeSupportedModels, readCachedModels, readConfiguredModels, listAccountModels, resetModelsCache }
|
|
27
28
|
|
|
28
29
|
function nowIso() { return new Date().toISOString() }
|
|
29
30
|
|
|
30
31
|
// 权限模式的用户可见取值(auto 由 DSH plan 状态决定;其余直通 CLI)。
|
|
31
32
|
const MODE_ENUM = ['auto', 'plan', 'accept-edits', 'bypass']
|
|
32
33
|
|
|
34
|
+
// 手动拉取(契约 §3.2)的探测时限与失败指引。
|
|
35
|
+
// 失败文案必须「可操作」:面板只会把 error 原样显示给用户,没写下一步等于没帮上忙。
|
|
36
|
+
const REFRESH_TIMEOUT_MS = 30000
|
|
37
|
+
const REFRESH_HINT = '排查顺序:① 确认设置面板的「claude 可执行文件路径」或 CLAUDE_BIN 指向真实 claude(原生 .exe 优先);② 确认 ~/.claude/settings.json 的 env 里 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN 指向可用端点(该面若未登录,先在终端跑一次 claude 完成登录);③ 再点一次「拉取可用模型」。'
|
|
38
|
+
|
|
39
|
+
/** 缓存年龄的短文案(面板 note 用;只描述事实,不假装新鲜)。 */
|
|
40
|
+
function ageText(ms) {
|
|
41
|
+
const min = Math.floor(Math.max(0, Number(ms) || 0) / 60000)
|
|
42
|
+
if (min < 1) return '刚刚拉取'
|
|
43
|
+
if (min < 60) return min + ' 分钟前拉取'
|
|
44
|
+
return Math.floor(min / 60) + ' 小时前拉取'
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
// ── 三工具 schema ─────────────────────────────────────────────────────────────
|
|
34
48
|
const RUN_PARAMS = {
|
|
35
49
|
type: 'object',
|
|
@@ -179,6 +193,12 @@ export default function defineProvider(deps = {}) {
|
|
|
179
193
|
else argv.push('--output-format', 'json')
|
|
180
194
|
if (stream && args.includePartialMessages) argv.push('--include-partial-messages')
|
|
181
195
|
const timeoutSec = clampInt(args.timeoutSec, 600, 10, 3600)
|
|
196
|
+
// 端点:cc-switch 的 proxy 模式把 Claude 指向第三方网关,而该网关要求自家 session 头
|
|
197
|
+
// (claude CLI 不会发)——**直连必然 400**,必须经 cc-switch 本地代理转发。
|
|
198
|
+
// 这里用 --settings 只覆盖 ANTHROPIC_BASE_URL,绝不改写用户的 ~/.claude/settings.json。
|
|
199
|
+
const ep = resolveEndpoint(currentSettings(), deps.env || {}, { probe: o.probeProxy, configDir: claudeConfigDir(deps.env || {}) })
|
|
200
|
+
if (ep.error) { const e = new Error(ep.error); e.code = 'ENDPOINT_UNAVAILABLE'; throw e }
|
|
201
|
+
if (ep.mode === 'proxy' && ep.baseUrl) argv.push('--settings', overlaySettingsJson(ep.baseUrl))
|
|
182
202
|
|
|
183
203
|
// 权限:auto → 跟随 DSH plan 状态;plan 用 CLI 的 plan 模式,其余走 bypass
|
|
184
204
|
//(-p 没有 TTY,任何交互批准都会挂到超时 —— 与 agy/codebuddy/opencode 同语义)。
|
|
@@ -404,12 +424,20 @@ export default function defineProvider(deps = {}) {
|
|
|
404
424
|
}
|
|
405
425
|
return out
|
|
406
426
|
},
|
|
427
|
+
/**
|
|
428
|
+
* 设置面板元数据(契约 §3.2):**只读缓存、绝不探测**。
|
|
429
|
+
* 面板是同步回调,而探测要起子进程打网络(3-5s)——放在这里会把首屏拖慢,且探不到时
|
|
430
|
+
* 用户无法区分「没模型」与「没拉到」。故探测改为用户点「拉取可用模型」→
|
|
431
|
+
* settings.refreshModels() 显式触发;成功后写回同一份缓存,这里下次渲染直接命中。
|
|
432
|
+
*/
|
|
407
433
|
meta: (env2) => {
|
|
408
434
|
const e = env2 && typeof env2 === 'object' ? env2 : {}
|
|
409
435
|
const s = currentSettings()
|
|
410
|
-
const
|
|
411
|
-
const options = (
|
|
436
|
+
const cached = readCachedModels(e, { binPath: s.binPath })
|
|
437
|
+
const options = (cached ? cached.ids : []).map((id) => ({ value: id, label: id }))
|
|
412
438
|
const exeCandidates = claudeExeCandidates(e, '')
|
|
439
|
+
// 端点实际走向(proxy / direct)——面板上如实显示,用户不用猜为什么请求失败。
|
|
440
|
+
const epInfo = resolveEndpoint(s, e, { configDir: claudeConfigDir(e) })
|
|
413
441
|
return {
|
|
414
442
|
titleSuffix: 'Claude Code',
|
|
415
443
|
fields: [
|
|
@@ -422,19 +450,73 @@ export default function defineProvider(deps = {}) {
|
|
|
422
450
|
{
|
|
423
451
|
key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
|
|
424
452
|
options,
|
|
453
|
+
// 面板据此渲染「手动拉取」按钮(契约 §3.2);meta() 自身永不探测。
|
|
454
|
+
refresh: { label: '拉取可用模型' },
|
|
425
455
|
placeholder: '留空 = 由 claude 自身配置(~/.claude/settings.json)决定',
|
|
426
456
|
note: options.length
|
|
427
|
-
? '共 ' + options.length + '
|
|
428
|
-
: '
|
|
457
|
+
? '共 ' + options.length + ' 个服务端权威可用模型(' + ageText(cached.ageMs) + ',源:' + cached.mode + ')。点「拉取可用模型」重新获取;调用时传 model 可覆盖本项。'
|
|
458
|
+
: '还没拉取过(或上次没拉到):点「拉取可用模型」向端点取权威清单;空表示尚未拉取 / 该面未登录。可直接手动输入模型名 —— 配置里的 ANTHROPIC_*_MODEL 只是别名映射,不作为候选展示。'
|
|
429
459
|
},
|
|
430
460
|
{
|
|
431
461
|
key: 'maxBudgetUsd', label: '单次最大花费(美元)', type: 'text',
|
|
432
462
|
placeholder: '留空 = 不限制',
|
|
433
463
|
note: '非空时自动附加 --max-budget-usd;调用参数 maxBudgetUsd 可覆盖。'
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
key: 'endpointMode', label: '端点模式', type: 'select',
|
|
467
|
+
options: [
|
|
468
|
+
{ value: 'auto', label: 'auto —— 代理活着就走代理,否则直连' },
|
|
469
|
+
{ value: 'proxy', label: 'proxy —— 强制走本地代理' },
|
|
470
|
+
{ value: 'direct', label: 'direct —— 强制直连配置的端点' }
|
|
471
|
+
],
|
|
472
|
+
note: epInfo.note + '。cc-switch 的 proxy 模式把 Claude 指向第三方网关,该网关要求自家 session 头而 claude CLI 不会发:直连必然 400,必须经 cc-switch 本地代理转发。'
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
key: 'proxyUrl', label: '本地代理地址', type: 'text',
|
|
476
|
+
placeholder: DEFAULT_PROXY_URL,
|
|
477
|
+
note: 'cc-switch 的本地代理默认监听 127.0.0.1:15721。若你改过端口,填这里。桥接用 --settings 只覆盖 ANTHROPIC_BASE_URL,**不会改写** ~/.claude/settings.json。'
|
|
434
478
|
}
|
|
435
479
|
]
|
|
436
480
|
}
|
|
437
481
|
},
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 手动拉取(契约 §3.2):面板「拉取可用模型」按钮 → keystone
|
|
485
|
+
* POST /cli-bridge/settings/models → 这里。这是本 provider 唯一的面板探测入口。
|
|
486
|
+
*
|
|
487
|
+
* 复用 listAccountModels(异步 probeSupportedModels + 配置诊断),不写第二套探测;
|
|
488
|
+
* 其成功路径已把权威清单写回 modelsCache,随后 meta() 直接命中(无需再探测)。
|
|
489
|
+
* 铁律:失败 = ok:false + 可操作错误 + 空 ids,绝不返回 {ok:true, ids:[]};
|
|
490
|
+
* 也绝不把 settings.json 里的别名映射冒充成服务端可用全集。
|
|
491
|
+
*/
|
|
492
|
+
async refreshModels(settings, env2) {
|
|
493
|
+
const e = (env2 && typeof env2 === 'object') ? env2 : env
|
|
494
|
+
const s = descriptor.settings.normalize(settings)
|
|
495
|
+
// 探测必须走**同一个端点**:cc-switch proxy 模式下直连第三方网关会 400,那样探测
|
|
496
|
+
// 永远拿不到东西(但配置清单仍应给出,见 listAccountModels 的合并逻辑)。
|
|
497
|
+
const ep = resolveEndpoint(s, e, { configDir: claudeConfigDir(e) })
|
|
498
|
+
const got = await listAccountModels(e, {
|
|
499
|
+
binPath: s.binPath,
|
|
500
|
+
baseUrl: ep.mode === 'proxy' ? ep.baseUrl : '',
|
|
501
|
+
timeoutMs: REFRESH_TIMEOUT_MS,
|
|
502
|
+
refresh: true
|
|
503
|
+
})
|
|
504
|
+
if (!got.ok) {
|
|
505
|
+
return { ok: false, ids: [], source: got.source || 'none', error: String(got.error || 'probe failed') + ' ' + REFRESH_HINT + ' 端点:' + ep.note }
|
|
506
|
+
}
|
|
507
|
+
const ids = (got.models || []).map((m) => m.id).filter(Boolean)
|
|
508
|
+
if (!ids.length) {
|
|
509
|
+
return { ok: false, ids: [], source: got.source || 'none', error: '探测声称成功但未返回任何模型 id(内部不一致)。' + REFRESH_HINT }
|
|
510
|
+
}
|
|
511
|
+
// labels 标出「来自配置」还是「服务端报文补充」,面板可据此区分来源。
|
|
512
|
+
const labels = {}
|
|
513
|
+
for (const m of got.models) {
|
|
514
|
+
labels[m.id] = m.id + (m.source === 'configured'
|
|
515
|
+
? ' [' + String(m.configuredAs || 'settings.json') + ']'
|
|
516
|
+
: ' [服务端清单]')
|
|
517
|
+
}
|
|
518
|
+
return { ok: true, ids, labels, source: got.source || 'configured', endpoint: ep.note }
|
|
519
|
+
},
|
|
438
520
|
diagnose: (settings, env2) => {
|
|
439
521
|
const s = settings && typeof settings === 'object' ? settings : {}
|
|
440
522
|
const e = env2 && typeof env2 === 'object' ? env2 : {}
|