@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/dynamic/codex.host.js
CHANGED
|
@@ -588,7 +588,7 @@ function createRunner(o) {
|
|
|
588
588
|
// - 只允许 import '../core/bridge-kit.mjs' 与同目录 codex-*.mjs 片文件(片文件由 build
|
|
589
589
|
// 自动内联进同一作用域;主文件超 600 行红线所以必须拆)。
|
|
590
590
|
// import { createStatusEngine, createRunner, clampInt, isLimited, endpointHost } from '../core/bridge-kit.mjs'
|
|
591
|
-
// import { codexSetHost, codexResolveExePrefix, codexExeCandidates, codexReadConfig, codexModelsEndpoint,
|
|
591
|
+
// import { codexSetHost, codexResolveExePrefix, codexExeCandidates, codexReadConfig, codexModelsEndpoint, codexCachedModelOptions, codexCachedLiveModels, codexLiveModels, codexCatalogModels, codexBuiltinModels, codexHomeDir } from './codex-cli.mjs'
|
|
592
592
|
// import { codexModelsTool } from './codex-models.mjs'
|
|
593
593
|
|
|
594
594
|
function nowIso() { return new Date().toISOString() }
|
|
@@ -598,6 +598,11 @@ const SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access']
|
|
|
598
598
|
const APPROVAL_POLICIES = ['untrusted', 'on-failure', 'on-request', 'never']
|
|
599
599
|
const REASONING_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh']
|
|
600
600
|
|
|
601
|
+
// 手动拉取(契约 §3.2):面板只把 error 原文显示给用户,所以失败文案必须自带下一步。
|
|
602
|
+
// codex-cli 没有 models 子命令,live 清单只能走端点 HTTP —— 排查顺序就围绕端点/凭据写。
|
|
603
|
+
const REFRESH_TIMEOUT_MS = 20000
|
|
604
|
+
const REFRESH_HINT = '排查顺序:① 看 ~/.codex/config.toml 的 [model_providers.<当前 provider>] 里 base_url / experimental_bearer_token(或 auth.json 的 OPENAI_BASE_URL / OPENAI_API_KEY)是否指向可用端点;② 可用 CODEX_BRIDGE_MODELS_URL / CODEX_BRIDGE_MODELS_TOKEN 临时覆盖端点与凭据;③ 再点一次「拉取可用模型」(codex_models 默认 source=live 读的是同一份清单)。'
|
|
605
|
+
|
|
601
606
|
// ── 三工具 schema ────────────────────────────────────────────────────────────
|
|
602
607
|
const RUN_PARAMS = {
|
|
603
608
|
type: 'object',
|
|
@@ -974,11 +979,12 @@ function defineProvider (deps = {}) {
|
|
|
974
979
|
approvalPolicy: APPROVAL_POLICIES.indexOf(approval) >= 0 ? approval : ''
|
|
975
980
|
}
|
|
976
981
|
},
|
|
977
|
-
//
|
|
978
|
-
//
|
|
982
|
+
// 面板是同步回调,且**不再自动探测**(契约 §3.2):defaultModel 的候选只来自
|
|
983
|
+
// 已缓存的 live 清单(codexCachedModelOptions 纯读,不 spawnSync、不发请求)。
|
|
984
|
+
// 探测由用户点「拉取可用模型」→ settings.refreshModels() 显式触发。
|
|
979
985
|
meta: (env2) => {
|
|
980
986
|
const e = env2 && typeof env2 === 'object' ? env2 : env
|
|
981
|
-
const opts =
|
|
987
|
+
const opts = codexCachedModelOptions(e)
|
|
982
988
|
const exeCandidates = codexExeCandidates(e)
|
|
983
989
|
return {
|
|
984
990
|
titleSuffix: 'Codex',
|
|
@@ -992,6 +998,8 @@ function defineProvider (deps = {}) {
|
|
|
992
998
|
{
|
|
993
999
|
key: 'defaultModel', label: '默认模型', type: 'select', allowCustom: true,
|
|
994
1000
|
options: opts.options,
|
|
1001
|
+
// 面板据此渲染「手动拉取」按钮(契约 §3.2);meta() 自身永不探测。
|
|
1002
|
+
refresh: { label: '拉取可用模型' },
|
|
995
1003
|
placeholder: '留空 = 由 ~/.codex/config.toml 的 model 决定',
|
|
996
1004
|
note: opts.note
|
|
997
1005
|
},
|
|
@@ -1019,6 +1027,39 @@ function defineProvider (deps = {}) {
|
|
|
1019
1027
|
]
|
|
1020
1028
|
}
|
|
1021
1029
|
},
|
|
1030
|
+
/**
|
|
1031
|
+
* 手动拉取(契约 §3.2):面板「拉取可用模型」按钮 → keystone
|
|
1032
|
+
* POST /cli-bridge/settings/models → 这里。这是本 provider 唯一的面板探测入口。
|
|
1033
|
+
*
|
|
1034
|
+
* 复用 codexLiveModels(工具面同一个实现:GET {base_url}/models + Bearer),不写第二套;
|
|
1035
|
+
* 成功时它已把 payload 写回 CODEX_MODEL_CACHE,随后 meta() 直接命中。
|
|
1036
|
+
* 铁律:失败 = ok:false + 可操作错误 + 空 ids;绝不返回 {ok:true, ids:[]},
|
|
1037
|
+
* 也绝不把 catalog/builtin(本地目录、非权威)混进这份「服务端可用清单」。
|
|
1038
|
+
*/
|
|
1039
|
+
async refreshModels(settings, env2) {
|
|
1040
|
+
const e = (env2 && typeof env2 === 'object') ? env2 : env
|
|
1041
|
+
const res = await codexLiveModels(e, {
|
|
1042
|
+
refresh: true,
|
|
1043
|
+
fetch: deps.fetch,
|
|
1044
|
+
timeoutMs: REFRESH_TIMEOUT_MS
|
|
1045
|
+
})
|
|
1046
|
+
if (!res.ok) {
|
|
1047
|
+
return { ok: false, ids: [], source: 'live', error: String(res.error || 'live model list unavailable') + ' ' + REFRESH_HINT }
|
|
1048
|
+
}
|
|
1049
|
+
const ids = []
|
|
1050
|
+
const labels = {}
|
|
1051
|
+
for (const m of res.models || []) {
|
|
1052
|
+
if (!m || !m.id || ids.indexOf(m.id) >= 0) continue
|
|
1053
|
+
ids.push(m.id)
|
|
1054
|
+
labels[m.id] = m.free ? m.id + ' [free]' : m.id
|
|
1055
|
+
}
|
|
1056
|
+
if (!ids.length) {
|
|
1057
|
+
// 拿到 2xx 却没有任何可用 id:显式失败,绝不把空清单当成功(本项目 0.2.6~0.2.9 的坑)。
|
|
1058
|
+
return { ok: false, ids: [], source: 'live', error: '端点 ' + String(res.url || '') + ' 返回 0 个可用模型 id(响应已解析但没有可用项)。' + REFRESH_HINT }
|
|
1059
|
+
}
|
|
1060
|
+
return { ok: true, ids, labels, source: 'live' }
|
|
1061
|
+
},
|
|
1062
|
+
|
|
1022
1063
|
diagnose: (settings, env2) => {
|
|
1023
1064
|
const s = settings && typeof settings === 'object' ? settings : {}
|
|
1024
1065
|
const e = env2 && typeof env2 === 'object' ? env2 : env
|
|
@@ -1394,10 +1435,14 @@ function codexRememberLive(url, payload) {
|
|
|
1394
1435
|
CODEX_MODEL_CACHE.payload = payload
|
|
1395
1436
|
}
|
|
1396
1437
|
|
|
1397
|
-
function codexFetchInit(token) {
|
|
1438
|
+
function codexFetchInit(token, signal) {
|
|
1398
1439
|
const headers = { accept: 'application/json' }
|
|
1399
1440
|
if (token) headers.authorization = 'Bearer ' + token
|
|
1400
|
-
|
|
1441
|
+
const init = { method: 'GET', headers }
|
|
1442
|
+
// 面板「拉取可用模型」是 HTTP 请求上的同步等待:端点卡住时必须有上界,
|
|
1443
|
+
// 否则设置页会一直转圈(无 signal 能力的宿主就照旧直接请求)。
|
|
1444
|
+
if (signal) init.signal = signal
|
|
1445
|
+
return init
|
|
1401
1446
|
}
|
|
1402
1447
|
|
|
1403
1448
|
function codexDescribeFetchError(status, body) {
|
|
@@ -1420,8 +1465,15 @@ async function codexLiveModels(env, opts) {
|
|
|
1420
1465
|
return { ok: false, source: 'live', models: [], url: ep.url, error: 'no fetch capability in this host form, so the live model list from ' + ep.url + ' cannot be read. Use source="catalog" (config.toml model_catalog_json) or source="builtin" (~/.codex/models.json) explicitly — they are local and are NOT authoritative.' }
|
|
1421
1466
|
}
|
|
1422
1467
|
let res
|
|
1423
|
-
|
|
1424
|
-
|
|
1468
|
+
// 取清单是设置面板的一次同步等待(点按钮 → HTTP → 这里):必须有上界。
|
|
1469
|
+
// 无 AbortController 的宿主退化为不带上界(照旧请求),不静默改变成功语义。
|
|
1470
|
+
const timeoutMs = Math.max(2000, Math.min(Number(o.timeoutMs) || 20000, 60000))
|
|
1471
|
+
const ctrl = (typeof AbortController === 'function') ? new AbortController() : null
|
|
1472
|
+
const timer = ctrl ? setTimeout(() => { try { ctrl.abort() } catch (err) { } }, timeoutMs) : null
|
|
1473
|
+
try { res = await f(ep.url, codexFetchInit(ep.token, ctrl ? ctrl.signal : null)) } catch (e) {
|
|
1474
|
+
return { ok: false, source: 'live', models: [], url: ep.url, error: 'GET ' + ep.url + ' failed (timeout ' + timeoutMs + 'ms): ' + String(e && e.message || e) + '. Check network/proxy, or set CODEX_BRIDGE_MODELS_URL to the provider catalogue endpoint.' }
|
|
1475
|
+
} finally {
|
|
1476
|
+
if (timer) clearTimeout(timer)
|
|
1425
1477
|
}
|
|
1426
1478
|
let body = ''
|
|
1427
1479
|
try { body = typeof res.text === 'function' ? await res.text() : JSON.stringify(res.json || {}) } catch (e) { body = '' }
|
|
@@ -1442,74 +1494,8 @@ async function codexLiveModels(env, opts) {
|
|
|
1442
1494
|
return payload
|
|
1443
1495
|
}
|
|
1444
1496
|
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
// 子进程只回吐原始 body,解析仍由上面的 parse 统一做(单一解析实现,避免漂移)。
|
|
1448
|
-
const CODEX_PROBE_SCRIPT = [
|
|
1449
|
-
'const url = process.env.CODEX_BRIDGE_PROBE_URL',
|
|
1450
|
-
'const token = process.env.CODEX_BRIDGE_PROBE_TOKEN || ""',
|
|
1451
|
-
'const ms = Number(process.env.CODEX_BRIDGE_PROBE_TIMEOUT || 8000)',
|
|
1452
|
-
'const headers = { accept: "application/json" }',
|
|
1453
|
-
'if (token) headers.authorization = "Bearer " + token',
|
|
1454
|
-
'const ctrl = new AbortController()',
|
|
1455
|
-
'const timer = setTimeout(() => { try { ctrl.abort() } catch (e) {} }, ms)',
|
|
1456
|
-
'fetch(url, { headers, signal: ctrl.signal })',
|
|
1457
|
-
' .then(async (r) => { const b = await r.text(); process.stdout.write(JSON.stringify({ status: r.status, body: b })) })',
|
|
1458
|
-
' .catch((e) => { process.stdout.write(JSON.stringify({ status: 0, error: String((e && e.message) || e) })) })',
|
|
1459
|
-
' .finally(() => clearTimeout(timer))'
|
|
1460
|
-
].join('\n')
|
|
1461
|
-
|
|
1462
|
-
/**
|
|
1463
|
-
* A 层(同步):设置面板是同步回调,而 HTTP 客户端没有同步 API —— 用一个 node 子进程
|
|
1464
|
-
* 跑上面的探针脚本,spawnSync 拿回 body。结果进 5 分钟缓存(面板反复渲染不至于反复起进程)。
|
|
1465
|
-
*/
|
|
1466
|
-
function codexLiveModelsSync(env, opts) {
|
|
1467
|
-
const o = opts || {}
|
|
1468
|
-
const ep = codexModelsEndpoint(env)
|
|
1469
|
-
if (!ep.ok) return { ok: false, source: 'live', models: [], error: ep.error }
|
|
1470
|
-
const cached = codexCachedLiveModels(env, o.nowMs)
|
|
1471
|
-
if (cached) return cached
|
|
1472
|
-
const proc = codexProcess()
|
|
1473
|
-
const child = codexBuiltin('node:child_process')
|
|
1474
|
-
if (!proc || !proc.execPath || !child || typeof child.spawnSync !== 'function') {
|
|
1475
|
-
return { ok: false, source: 'live', models: [], url: ep.url, error: 'no spawnSync capability to read ' + ep.url + ' synchronously' }
|
|
1476
|
-
}
|
|
1477
|
-
const r = child.spawnSync(proc.execPath, ['-e', CODEX_PROBE_SCRIPT], {
|
|
1478
|
-
encoding: 'utf8',
|
|
1479
|
-
timeout: Math.max(2000, Math.min(Number(o.timeoutMs) || 9000, 30000)),
|
|
1480
|
-
windowsHide: true,
|
|
1481
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
1482
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1483
|
-
env: Object.assign({}, proc.env, {
|
|
1484
|
-
CODEX_BRIDGE_PROBE_URL: ep.url,
|
|
1485
|
-
CODEX_BRIDGE_PROBE_TOKEN: ep.token,
|
|
1486
|
-
CODEX_BRIDGE_PROBE_TIMEOUT: String(Math.max(2000, Math.min(Number(o.timeoutMs) || 9000, 30000)) - 500)
|
|
1487
|
-
})
|
|
1488
|
-
})
|
|
1489
|
-
if (!r || r.error) {
|
|
1490
|
-
return { ok: false, source: 'live', models: [], url: ep.url, error: 'probe spawn failed: ' + String((r && r.error && (r.error.code || r.error.message)) || 'unknown') }
|
|
1491
|
-
}
|
|
1492
|
-
let out = null
|
|
1493
|
-
try { out = JSON.parse(String(r.stdout || '')) } catch (e) { out = null }
|
|
1494
|
-
if (!out) {
|
|
1495
|
-
return { ok: false, source: 'live', models: [], url: ep.url, error: 'probe produced no JSON (exit=' + r.status + '): ' + String(r.stderr || r.stdout || '').slice(0, 200) }
|
|
1496
|
-
}
|
|
1497
|
-
if (!out.status && out.error) return { ok: false, source: 'live', models: [], url: ep.url, error: 'GET ' + ep.url + ' failed: ' + String(out.error) }
|
|
1498
|
-
if (out.status < 200 || out.status >= 300) {
|
|
1499
|
-
return { ok: false, source: 'live', models: [], url: ep.url, error: 'GET ' + ep.url + ' → ' + codexDescribeFetchError(out.status, out.body) }
|
|
1500
|
-
}
|
|
1501
|
-
const parsed = codexParseModelsPayload(out.body, 'live')
|
|
1502
|
-
if (!parsed.ok) return { ok: false, source: 'live', models: [], url: ep.url, error: ep.url + ': ' + parsed.error }
|
|
1503
|
-
if (!parsed.models.length) {
|
|
1504
|
-
return { ok: false, source: 'live', models: [], url: ep.url, error: ep.url + ' returned 0 usable models (payload items=' + parsed.total + '). Refusing to report an empty list as authoritative.' }
|
|
1505
|
-
}
|
|
1506
|
-
const payload = {
|
|
1507
|
-
ok: true, source: 'live', models: parsed.models, count: parsed.models.length,
|
|
1508
|
-
url: ep.url, baseUrl: ep.baseUrl, providerName: ep.providerName, fetchedAt: Date.now()
|
|
1509
|
-
}
|
|
1510
|
-
codexRememberLive(ep.url, payload)
|
|
1511
|
-
return payload
|
|
1512
|
-
}
|
|
1497
|
+
// 子进程探针脚本已删除:面板不再自动探测(契约 §3.2 手动拉取)。live 清单只在
|
|
1498
|
+
// settings.refreshModels()(异步、用户点按钮)与时序工具 codex_models 里读取。
|
|
1513
1499
|
|
|
1514
1500
|
/** B 层:config.toml 的 model_catalog_json(相对 CODEX_HOME)。 */
|
|
1515
1501
|
function codexCatalogModels(env) {
|
|
@@ -1541,38 +1527,27 @@ function codexBuiltinModels(env) {
|
|
|
1541
1527
|
}
|
|
1542
1528
|
|
|
1543
1529
|
/**
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1530
|
+
* 面板下拉的候选池(契约 §3.2):**纯读缓存**,绝不 spawnSync、绝不发请求、绝不碰本地目录。
|
|
1531
|
+
* 探测由 settings.refreshModels() 显式触发(用户点「拉取可用模型」),成功后写回同一份
|
|
1532
|
+
* CODEX_MODEL_CACHE,这里下次渲染直接命中 —— 面板首屏因此不会被网络拖慢。
|
|
1533
|
+
*
|
|
1534
|
+
* 为什么不再回落到 catalog/builtin:那两层是本地元数据,不是服务端可用清单;把它们填进
|
|
1535
|
+
* 这个下拉等于让用户以为「服务端就这些」(0.2.6~0.2.9 反复踩的坑)。宁可空着并说清
|
|
1536
|
+
* 「点按钮拉取」,也不展示可能已经过时的非权威列表 —— 需要本地目录请用 codex_models
|
|
1537
|
+
* 的 source="catalog"/"builtin" 显式查询(那里逐项标注来源)。
|
|
1547
1538
|
*/
|
|
1548
|
-
function
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
if (live && live.ok && live.models.length) {
|
|
1552
|
-
return {
|
|
1553
|
-
options: live.models.map((m) => ({ value: m.id, label: m.id + (m.free ? ' [free]' : '') + ' [实时]' })),
|
|
1554
|
-
source: 'live', url: live.url, note: '实时清单来自 ' + live.url + '(' + live.count + ' 个,免费模型标 [free])。'
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
const cat = codexCatalogModels(env)
|
|
1558
|
-
if (cat.ok) {
|
|
1559
|
-
return {
|
|
1560
|
-
options: cat.models.map((m) => ({ value: m.id, label: m.id + ' [本地目录]' })),
|
|
1561
|
-
source: 'catalog', file: cat.file,
|
|
1562
|
-
note: '实时清单不可用(' + String((live && live.error) || '未探测') + ')。以下 ' + cat.count + ' 项来自 config.toml 的 model_catalog_json,仅代表 codex 本地目录,不是服务端权威清单。'
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
const builtin = codexBuiltinModels(env)
|
|
1566
|
-
if (builtin.ok) {
|
|
1539
|
+
function codexCachedModelOptions(env) {
|
|
1540
|
+
const live = codexCachedLiveModels(env)
|
|
1541
|
+
if (!live || !live.ok || !Array.isArray(live.models) || !live.models.length) {
|
|
1567
1542
|
return {
|
|
1568
|
-
options:
|
|
1569
|
-
|
|
1570
|
-
note: '实时清单与本地目录都不可用(' + String((cat && cat.error) || '') + ')。以下来自 ~/.codex/models.json 内置定义,不是服务端权威清单。'
|
|
1543
|
+
options: [], source: 'none', count: 0,
|
|
1544
|
+
note: '还没拉取过(或上次没拉到):点「拉取可用模型」向端点取权威清单;空表示尚未拉取 / 端点或 token 未配好。可直接手动输入模型 id。'
|
|
1571
1545
|
}
|
|
1572
1546
|
}
|
|
1573
1547
|
return {
|
|
1574
|
-
options: []
|
|
1575
|
-
|
|
1548
|
+
options: live.models.map((m) => ({ value: m.id, label: m.id + (m.free ? ' [free]' : '') })),
|
|
1549
|
+
source: 'live', count: live.count, url: live.url, fetchedAt: live.fetchedAt,
|
|
1550
|
+
note: '上次拉取到 ' + live.count + ' 个模型(源:' + live.url + ',免费标 [free])。点「拉取可用模型」重新获取。'
|
|
1576
1551
|
}
|
|
1577
1552
|
}
|
|
1578
1553
|
|