@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.
@@ -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, codexModelOptions, codexCachedLiveModels, codexLiveModels, codexCatalogModels, codexBuiltinModels, codexHomeDir } from './codex-cli.mjs'
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
- // 面板是同步回调:defaultModel 的候选走 codexModelOptions(实时清单优先,5 分钟
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 = codexModelOptions(e, { timeoutMs: 6000 })
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
- return { method: 'GET', headers }
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
- try { res = await f(ep.url, codexFetchInit(ep.token)) } catch (e) {
1424
- return { ok: false, source: 'live', models: [], url: ep.url, error: 'GET ' + ep.url + ' failed: ' + String(e && e.message || e) + '. Check network/proxy, or set CODEX_BRIDGE_MODELS_URL to the provider catalogue endpoint.' }
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
- // 子进程探针脚本:spawnSync 里不能 await,所以让子进程自己 async fetch 再打印。
1446
- // token/url 只经 env 传递(绝不进命令行,避免凭据出现在进程参数里被别的进程看到)。
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
- * 让用户至少有东西可选,同时 note 里说清「这不是服务端权威清单」。
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 codexModelOptions(env, opts) {
1549
- const o = opts || {}
1550
- const live = o.skipLive ? null : (codexCachedLiveModels(env) || codexLiveModelsSync(env, { timeoutMs: o.timeoutMs }))
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: builtin.models.map((m) => ({ value: m.id, label: m.id + ' [内置定义]' })),
1569
- source: 'builtin', file: builtin.file,
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: [], source: 'none',
1575
- note: '未能读取任何模型清单:' + String((live && live.error) || 'live unavailable') + ';可直接手动输入模型 id。'
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