@ljwei-stak/dsh-model-router 0.18.2 → 0.20.0

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.
@@ -127,6 +127,7 @@ const rerunRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RerunReq
127
127
  ...(provider ? { provider, model } : {}),
128
128
  confirmOverBudget: request.confirmOverBudget === true,
129
129
  confirmWrite: request.confirmWrite === true,
130
+ ...(request.confirmUnsandboxed === undefined ? {} : { confirmUnsandboxed: request.confirmUnsandboxed === true }),
130
131
  ...(['api', 'subscription', 'cancel'].includes(request.subscriptionChoice) ? { subscriptionChoice: request.subscriptionChoice } : {}),
131
132
  }
132
133
  })
@@ -152,11 +153,22 @@ export function parseRunRequest(value) {
152
153
  throw new TypeError('budgetUsd must be a non-negative number')
153
154
  }
154
155
  const confirmed = Array.isArray(request.confirmedReasons) ? request.confirmedReasons : []
156
+ if (request.executor !== undefined && !['routed', 'official-team'].includes(request.executor)) throw new TypeError('executor is invalid')
157
+ if (request.executionMode !== undefined && !['read-only', 'workspace-write'].includes(request.executionMode)) throw new TypeError('executionMode is invalid')
158
+ if (request.executionEnvironment !== undefined && !['harness', 'cli-native'].includes(request.executionEnvironment)) throw new TypeError('executionEnvironment is invalid')
159
+ const officialTeam = request.executor === 'official-team'
160
+ if (officialTeam && (request.planMode !== 'team' || provider || model)) throw new TypeError('official-team requires a team plan without an explicit route')
161
+ if (!officialTeam && request.executionMode === 'workspace-write') throw new TypeError('workspace-write requires official-team')
162
+ if (request.executionEnvironment === 'cli-native' && (!officialTeam || request.executionMode !== 'workspace-write')) {
163
+ throw new TypeError('cli-native requires official-team with workspace-write')
164
+ }
155
165
  if (confirmed.length > RUN_CONFIRMATION_CODES.length || confirmed.some(code => !RUN_CONFIRMATION_CODES.includes(code))) {
156
166
  throw new TypeError('confirmedReasons contains an unknown reason')
157
167
  }
158
168
  return {
159
169
  task: request.task,
170
+ ...(officialTeam ? { executor: 'official-team', executionMode: request.executionMode ?? 'read-only',
171
+ ...(request.executionEnvironment === undefined ? {} : { executionEnvironment: request.executionEnvironment }) } : {}),
160
172
  ...(provider ? { provider, model } : {}),
161
173
  ...(['single', 'team'].includes(request.planMode) ? { planMode: request.planMode } : {}),
162
174
  ...(['economy', 'balanced', 'quality'].includes(request.preset) ? { preset: request.preset } : {}),
@@ -60,7 +60,7 @@ export const CLAUDE_UPDATE_ARGS = Object.freeze(['update'])
60
60
  * and script source). Refreshed on apply and live config updates, so a client
61
61
  * can only ever pick a method id — these addresses never arrive over the wire.
62
62
  */
63
- let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
63
+ let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true, zcodeInstallDir: '' }
64
64
 
65
65
  export function setInstallPreferences(next) {
66
66
  if (next && typeof next === 'object') {
@@ -850,6 +850,22 @@ export async function probeToolWith(tool, runner, {
850
850
  return probe
851
851
  }
852
852
 
853
+ /** Reuse the concrete entry discovered by the installation probe for login checks. */
854
+ export async function loginLaunchForProbe(probe, {
855
+ workspace = process.cwd(),
856
+ fileExists = async path => (await stat(path).catch(() => null))?.isFile() === true,
857
+ findNode = async cwd => (await import('./official-tool-executor.mjs')).findNodeExecutable(cwd),
858
+ } = {}) {
859
+ if (probe?.installed !== true || !getOfficialTool(probe.id)) return null
860
+ const entry = probe.packageEntry || probe.executablePath
861
+ if (typeof entry !== 'string' || !isAbsolute(entry) || !(await fileExists(entry))) return null
862
+ if (/\.(?:mjs|cjs|js)$/i.test(entry)) {
863
+ const node = await findNode(workspace)
864
+ return node ? { file: node, args: [entry], useShell: false } : null
865
+ }
866
+ return { file: entry, args: [], useShell: IS_WINDOWS && /\.(?:cmd|bat)$/i.test(entry) }
867
+ }
868
+
853
869
  async function selectedPackageEntries(tool, { plan, preferences: selectedPreferences }) {
854
870
  const { findGlobalPackageEntries, officialToolEntrySelection } = await import('./official-tool-executor.mjs')
855
871
  const selection = officialToolEntrySelection(tool.id, { preferences: selectedPreferences, plan })
@@ -868,10 +884,13 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
868
884
  }
869
885
  if (tool.manager === 'signed-windows-installer') {
870
886
  let rejected = null
871
- const bundle = await discoverZCodeBundle({ onRejected: value => { rejected ??= value } })
887
+ const bundle = await discoverZCodeBundle({ installDir: (selectedPreferences ?? preferences).zcodeInstallDir,
888
+ onRejected: value => { rejected ??= value } })
872
889
  if (bundle) return { id: tool.id, installed: true, version: bundle.version, status: 'installed',
873
- detail: `${tool.probeNote} 安装目录:${bundle.root}`, bannerLine: `${tool.label} ${bundle.buildVersion}` }
874
- if (rejected) return { id: tool.id, installed: false, version: null, status: 'not-installed', detail: `检测到已安装的 ZCode ${rejected.version}(${rejected.root}):${rejected.rejected}` }
890
+ detail: `${tool.probeNote} 安装目录:${bundle.root}`, bannerLine: `${tool.label} 桌面版 ${bundle.buildVersion}`,
891
+ installDir: bundle.root, cliEntry: bundle.cliEntry, source: bundle.source }
892
+ if (rejected) return { id: tool.id, installed: false, version: null, status: 'not-installed',
893
+ detail: rejected.version ? `检测到 ZCode ${rejected.version}(${rejected.root}):${rejected.rejected}` : rejected.rejected }
875
894
  const other = (await unverifiedZCodeInstalls().catch(() => []))[0]
876
895
  return { id: tool.id, installed: false, version: null, status: 'not-installed',
877
896
  detail: other
@@ -885,7 +904,12 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
885
904
  ...selectedPreferences,
886
905
  allowScriptInstall: installMethodFor(tool, selectedPreferences.methods?.[tool.id])?.kind === 'script' ? true : selectedPreferences.allowScriptInstall,
887
906
  }, currentInstallOptions())
888
- } catch { /* a blocked saved method still allows normal detection */ }
907
+ } catch (error) {
908
+ if (usesConfiguredInstallPlan(tool.id, selectedPreferences)) {
909
+ return { id: tool.id, installed: false, version: null, status: 'probe-failed',
910
+ detail: `无法检查所选安装方式或目录:${String(error?.message ?? error).slice(0, 200)}` }
911
+ }
912
+ }
889
913
  }
890
914
  // npm and pnpm can write the same shim into a unified bin directory. Its
891
915
  // banner belongs to whichever manager wrote it last, not the selected copy.
@@ -1589,30 +1613,43 @@ async function runInstallJob(installPlan, job) {
1589
1613
  await finishVerifiedInstall(job, probe)
1590
1614
  }
1591
1615
 
1592
- async function runZCodeInstallJob(job) {
1616
+ export async function runZCodeInstallJob(job, {
1617
+ probe = probeToolWith, lookup = (...args) => latestVersions.lookup(...args),
1618
+ openInstaller = openZCodeInstaller, capture = runCapture,
1619
+ } = {}) {
1593
1620
  if (job.status !== 'running') return
1594
1621
  job.phase = 'preflight'
1595
1622
  probeCache.delete(job.tool.id)
1596
- const before = await probeToolWith(job.tool, defaultRunner)
1623
+ const before = await probe(job.tool, defaultRunner, { preferences: job.preferences, cacheMs: 0 })
1597
1624
  if (job.cancelRequested) { markCancelled(job); return }
1598
- const latest = await latestVersions.lookup(job.tool, { fresh: true }).catch(() => null)
1625
+ const latest = await lookup(job.tool, { fresh: true }).catch(() => null)
1599
1626
  if (job.cancelRequested) { markCancelled(job); return }
1600
- if (before.installed && latest?.version && (compareReleaseVersions(before.version, latest.version) ?? -1) >= 0) {
1627
+ const desktopOrder = before.installed && latest?.version ? compareReleaseVersions(before.version, latest.version) : null
1628
+ if (before.installed && desktopOrder === null) {
1629
+ job.status = 'failed'
1630
+ job.phase = 'done'
1631
+ job.finishedAt = new Date().toISOString()
1632
+ job.error = '无法核实 ZCode 最新桌面版本;保留当前安装,网络恢复后重试检查更新。'
1633
+ return
1634
+ }
1635
+ if (desktopOrder === 1 || (job.operation !== 'repair' && desktopOrder === 0)) {
1601
1636
  job.status = 'succeeded'
1602
1637
  job.phase = 'done'
1603
1638
  job.finishedAt = new Date().toISOString()
1604
1639
  job.postInstallProbe = before
1605
- pushLine(job, `— 已找到 ZCode ${before.version}(最新 ${latest.version}),无需重复打开安装器 —`)
1640
+ pushLine(job, desktopOrder === 1
1641
+ ? `— ZCode ${before.version} 高于下载页的 ${latest.version},不会打开旧版安装器降级;TUI 使用当前桌面内置入口 —`
1642
+ : `— 已找到 ZCode ${before.version}(最新 ${latest.version}),无需重复打开安装器;TUI 使用当前桌面内置入口 —`)
1606
1643
  return
1607
1644
  }
1608
- const prefix = await runCapture('npm', ['config', 'get', 'prefix'], {
1645
+ const prefix = await capture('npm', ['config', 'get', 'prefix'], {
1609
1646
  timeoutMs: 5_000, useShell: IS_WINDOWS, signal: job.controller.signal,
1610
1647
  })
1611
1648
  if (job.cancelRequested) { markCancelled(job); return }
1612
1649
  job.phase = 'downloading-and-verifying'
1613
1650
  let lastProgress = 0
1614
1651
  try {
1615
- const opened = await openZCodeInstaller({
1652
+ const opened = await openInstaller({
1616
1653
  latest,
1617
1654
  npmPrefix: prefix.ok ? prefix.stdout.trim() : undefined,
1618
1655
  signal: job.controller.signal,
@@ -1629,7 +1666,11 @@ async function runZCodeInstallJob(job) {
1629
1666
  job.finishedAt = new Date().toISOString()
1630
1667
  probeCache.delete(job.tool.id)
1631
1668
  pushLine(job, `已验证发布者签名并打开 ZCode ${opened.version} 官方安装器:${opened.installerPath}`)
1632
- pushLine(job, '请在原厂安装界面选择非 C 盘目录;完成后点击“重新检测”。')
1669
+ const desktopDirectory = before.installDir || (/[\\/]ZCode\.exe$/i.test(job.preferences.zcodeInstallDir ?? '')
1670
+ ? dirname(job.preferences.zcodeInstallDir) : job.preferences.zcodeInstallDir)
1671
+ pushLine(job, desktopDirectory
1672
+ ? `请在原厂安装界面选择原桌面安装目录 ${desktopDirectory};完成后重新体检,内置 TUI 随桌面版更新。`
1673
+ : '请在原厂安装界面选择独立的 ZCode 桌面目录;完成后保存该目录并重新体检,内置 TUI 随桌面版更新。')
1633
1674
  } catch (error) {
1634
1675
  if (job.cancelRequested || job.controller.signal.aborted) { markCancelled(job); return }
1635
1676
  job.status = 'failed'
@@ -1700,5 +1741,5 @@ export function resetForTests() {
1700
1741
  installJobs.clear()
1701
1742
  installChain = Promise.resolve()
1702
1743
  prefixPromise = null
1703
- preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
1744
+ preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true, zcodeInstallDir: '' }
1704
1745
  }
@@ -12,12 +12,30 @@ export const MAX_QUALITY_BIAS = 0.04
12
12
  const finite = value => typeof value === 'number' && Number.isFinite(value)
13
13
  const routeKey = (provider, model) => `${String(provider ?? '')}\u0000${String(model ?? '')}`
14
14
 
15
+ function modelIdentity(value) {
16
+ if (typeof value === 'string' && value.trim()) return value.trim()
17
+ if (value && typeof value === 'object' && !Array.isArray(value)
18
+ && typeof value.providerId === 'string' && value.providerId
19
+ && typeof value.modelId === 'string' && value.modelId) return `${value.providerId}/${value.modelId}`
20
+ return null
21
+ }
22
+
15
23
  /** USD cost of one package. Prefers a CLI-reported cost, else usage × configured price. */
16
24
  export function actualCost(result, pricing) {
17
25
  if (result && Object.hasOwn(result, 'reportedCostUsd') && finite(result.reportedCostUsd) && result.reportedCostUsd >= 0) {
18
26
  return { costUsd: result.reportedCostUsd, costSource: 'cli-reported' }
19
27
  }
20
28
  const usage = result && Object.hasOwn(result, 'usage') ? result.usage : null
29
+ if (usage && result.usageIncomplete === true) return { costUsd: null, costSource: 'usage-incomplete' }
30
+ if (usage && result.channel === 'official-cli') {
31
+ const actualModel = modelIdentity(result.actualModel) ?? modelIdentity(result.reportedModel)
32
+ if (!actualModel) return { costUsd: null, costSource: 'model-unknown' }
33
+ const pricedModel = pricing?.model ?? result.model
34
+ const provider = pricing?.provider ?? result.provider
35
+ const matches = [pricedModel, pricing?.cliModel, provider && pricedModel ? `${provider}/${pricedModel}` : null]
36
+ .some(value => typeof value === 'string' && value === actualModel)
37
+ if (!matches) return { costUsd: null, costSource: 'model-price-unverified' }
38
+ }
21
39
  if (!usage || !pricing || !Object.hasOwn(pricing, 'input') || !Object.hasOwn(pricing, 'output')
22
40
  || pricing.input === undefined || pricing.input === null || pricing.output === undefined || pricing.output === null) {
23
41
  return { costUsd: null, costSource: usage ? 'price-missing' : 'usage-missing' }
@@ -268,12 +286,25 @@ function storedPackage(planned, result, pricing, finishedAt, billingFor = billin
268
286
  channel: result?.channel ?? null,
269
287
  toolId: result?.toolId ?? null,
270
288
  credentialSource: result?.credentialSource ?? null,
271
- ...(result?.actualModel ? { actualModel: String(result.actualModel) } : {}),
289
+ ...(result?.channel === 'official-cli' ? {
290
+ requestedCliModel: result.requestedCliModel ?? null,
291
+ actualModel: modelIdentity(result.actualModel) ?? modelIdentity(result.reportedModel),
292
+ modelNotice: result.modelNotice ?? null,
293
+ } : {}),
294
+ ...(result?.channel !== 'official-cli' && result?.actualModel ? { actualModel: String(result.actualModel) } : {}),
295
+ ...(result?.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
296
+ ...(typeof result?.source === 'string' ? { source: result.source } : {}),
297
+ ...(typeof result?.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
298
+ ...(result?.usageIncomplete === true ? { usageIncomplete: true } : {}),
299
+ ...(typeof result?.executionEnvironment === 'string' ? { executionEnvironment: result.executionEnvironment } : {}),
300
+ ...(finite(result?.reportedCostUsd) && result.reportedCostUsd >= 0 ? { reportedCostUsd: result.reportedCostUsd } : {}),
272
301
  fallback: result?.fallback ?? null,
273
302
  ...(result?.billingMode ? { billingMode: String(result.billingMode) } : {}),
274
303
  ...(result?.billingSwitch ? { billingSwitch: { ...result.billingSwitch } } : {}),
275
304
  ...(result?.subscriptionRoute ? { subscriptionRoute: { provider: String(result.subscriptionRoute.provider ?? ''), model: String(result.subscriptionRoute.model ?? '') } } : {}),
276
305
  error: result?.ok ? null : (result?.error ?? null),
306
+ ...(!result?.ok && result?.diagnostic ? { diagnostic: String(result.diagnostic).slice(0, 2_000) } : {}),
307
+ ...(Number.isInteger(result?.exitCode) ? { exitCode: result.exitCode } : {}),
277
308
  answer: String(result?.answer ?? '').slice(0, MAX_STORED_ANSWER),
278
309
  answerTruncated: String(result?.answer ?? '').length > MAX_STORED_ANSWER,
279
310
  usage: result?.usage ?? null,
@@ -375,12 +406,21 @@ export function teamExecutionResults(plan, execution) {
375
406
  cancelled: result.status === 'cancelled',
376
407
  provider: result.provider,
377
408
  model: result.recommendedModel,
409
+ requestedCliModel: result.requestedCliModel ?? null,
378
410
  actualModel: result.actualModel ?? null,
411
+ ...(result.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
412
+ modelNotice: result.modelNotice ?? null,
413
+ ...(typeof result.source === 'string' ? { source: result.source } : {}),
414
+ ...(typeof result.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
415
+ ...(result.usageIncomplete === true ? { usageIncomplete: true } : {}),
416
+ ...(typeof result.executionEnvironment === 'string' ? { executionEnvironment: result.executionEnvironment } : {}),
379
417
  channel: 'official-cli',
380
418
  toolId: result.toolId ?? null,
381
419
  answer: String(result.finalText ?? ''),
382
420
  error: result.status === 'succeeded' ? null : String(result.error ?? (TEAM_FAILURE.has(result.status) ? result.status : '官方 CLI 未成功完成。')),
383
421
  stderrTail: result.outputTail ?? null,
422
+ diagnostic: result.outputTail ?? null,
423
+ exitCode: result.exitCode ?? null,
384
424
  ...(result.usage ? { usage: result.usage } : {}),
385
425
  ...(finite(result.reportedCostUsd) ? { reportedCostUsd: result.reportedCostUsd } : {}),
386
426
  }))
@@ -421,9 +461,18 @@ export function buildToolRunRecord({ toolId, toolLabel, provider, model, cliMode
421
461
  id: packageId, ok: succeeded, cancelled: result?.status === 'cancelled',
422
462
  provider: plan.directRoute.provider, model: plan.directRoute.model,
423
463
  channel: 'official-cli', toolId,
424
- actualModel: result?.reportedModel && typeof result.reportedModel === 'string' ? result.reportedModel : null,
464
+ requestedCliModel: Object.hasOwn(result ?? {}, 'requestedModel') ? result.requestedModel : cliModel,
465
+ actualModel: modelIdentity(result?.actualModel) ?? modelIdentity(result?.reportedModel),
466
+ ...(result?.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
467
+ modelNotice: result?.modelNotice ?? null,
468
+ ...(typeof result?.source === 'string' ? { source: result.source } : {}),
469
+ ...(typeof result?.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
470
+ ...(result?.usageIncomplete === true ? { usageIncomplete: true } : {}),
425
471
  answer: String(result?.finalText ?? ''),
426
472
  error: succeeded ? null : String(result?.error ?? result?.reason ?? result?.status ?? '官方 CLI 未成功完成。'),
473
+ ...(!succeeded && typeof result?.diagnostic === 'string' && result.diagnostic.trim()
474
+ ? { diagnostic: result.diagnostic } : {}),
475
+ ...(Number.isInteger(result?.exitCode) ? { exitCode: result.exitCode } : {}),
427
476
  ...(result?.usage ? { usage: result.usage } : {}),
428
477
  ...(finite(result?.reportedCostUsd) ? { reportedCostUsd: result.reportedCostUsd } : {}),
429
478
  }],
@@ -36,7 +36,7 @@ const BASE_ENV_KEYS = Object.freeze([
36
36
  'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'ComSpec',
37
37
  'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
38
38
  'PROGRAMDATA', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'TERM',
39
- 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
39
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
40
40
  'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS',
41
41
  ])
42
42
 
@@ -50,7 +50,9 @@ export const TASK_ADAPTERS = Object.freeze([
50
50
  executable: 'claude',
51
51
  portable: true,
52
52
  apiKeyEnv: 'ANTHROPIC_API_KEY',
53
- sessionEnv: Object.freeze(['CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR']),
53
+ sessionEnv: Object.freeze(['CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR', 'ANTHROPIC_AUTH_TOKEN',
54
+ 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL',
55
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL']),
54
56
  format: 'claude-json',
55
57
  buildArgs(modelId) {
56
58
  return [
@@ -193,17 +195,22 @@ async function checkedWorkspace(workspace) {
193
195
  }
194
196
 
195
197
  function cliModelFor(route, adapter) {
196
- const explicit = checkedModel(route?.cliModel)
197
- if (explicit) return explicit
198
- if (adapter?.id === 'claude-code' || adapter?.id === 'codex' || adapter?.id === 'gemini') {
199
- return checkedModel(route?.model)
198
+ // Catalog IDs are routing recommendations, not vendor CLI aliases. Omit
199
+ // the model argument unless the route explicitly binds a CLI model.
200
+ const value = route?.cliModel
201
+ if (value === undefined || value === null || value === '') return null
202
+ const explicit = checkedModel(value)
203
+ if (!explicit) throw new TypeError('cliModel must be a bounded model identifier')
204
+ if ((adapter?.id === 'minimax-code' || adapter?.id === 'mimo-code') && !explicit.includes('/')) {
205
+ throw new TypeError(`${adapter.id} cliModel requires provider/model`)
200
206
  }
201
- return null
207
+ if (adapter?.id === 'zcode') throw new TypeError('ZCode CLI does not support a per-call cliModel')
208
+ return explicit
202
209
  }
203
210
 
204
211
  /** Variables that switch a CLI from its account login to API-key billing. */
205
212
  const API_KEY_VARIABLES = Object.freeze({
206
- 'claude-code': ['ANTHROPIC_API_KEY'],
213
+ 'claude-code': ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
207
214
  codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
208
215
  gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
209
216
  })
@@ -324,7 +331,7 @@ function parseAdapterOutput(format, stdout) {
324
331
  const DIAGNOSTIC_CHARS = 400
325
332
 
326
333
  /** Shorten and scrub a CLI diagnostic before it reaches the user or the run ledger. */
327
- export function redactDiagnostic(value, secret = '') {
334
+ export function redactDiagnostic(value, secret = '', maxChars = DIAGNOSTIC_CHARS) {
328
335
  const scrubbed = redact(String(value ?? ''), secret)
329
336
  .replace(/sk-[A-Za-z0-9_-]{8,}/g, 'sk-[redacted]')
330
337
  .replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi, '$1[redacted]')
@@ -332,7 +339,10 @@ export function redactDiagnostic(value, secret = '') {
332
339
  .replace(/\b(api[_-]?key|token|secret|password)(["'\s:=]+)[^\s"',}]{6,}/gi, '$1$2[redacted]')
333
340
  .replace(/\s+/g, ' ')
334
341
  .trim()
335
- return scrubbed.length > DIAGNOSTIC_CHARS ? `${scrubbed.slice(0, DIAGNOSTIC_CHARS)}…` : scrubbed
342
+ const limit = Number.isInteger(maxChars) ? Math.max(100, Math.min(4_000, maxChars)) : DIAGNOSTIC_CHARS
343
+ if (scrubbed.length <= limit) return scrubbed
344
+ const head = Math.floor(limit / 3)
345
+ return `${scrubbed.slice(0, head)}…${scrubbed.slice(-(limit - head - 1))}`
336
346
  }
337
347
 
338
348
  function jsonLines(stdout) {
@@ -389,7 +399,9 @@ export function usageFromOutput(format, stdout = '') {
389
399
  const result = JSON.parse(stdout)
390
400
  const usage = result?.usage
391
401
  if (!usage || typeof usage !== 'object') return null
392
- const reported = Number(result.total_cost_usd)
402
+ const reported = result.total_cost_usd
403
+ const models = result.modelUsage && typeof result.modelUsage === 'object' && !Array.isArray(result.modelUsage)
404
+ ? Object.keys(result.modelUsage) : []
393
405
  return {
394
406
  usage: {
395
407
  inputTokens: count(usage.input_tokens),
@@ -397,7 +409,8 @@ export function usageFromOutput(format, stdout = '') {
397
409
  cacheReadTokens: count(usage.cache_read_input_tokens),
398
410
  cacheWriteTokens: count(usage.cache_creation_input_tokens),
399
411
  },
400
- ...(Number.isFinite(reported) && reported > 0 ? { reportedCostUsd: reported } : {}),
412
+ ...(Number.isFinite(reported) && reported >= 0 ? { reportedCostUsd: reported } : {}),
413
+ ...(models.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:/\[\]-]{0,199}$/.test(models[0]) ? { reportedModel: models[0] } : {}),
401
414
  }
402
415
  }
403
416
  if (format === 'codex-jsonl') {
@@ -442,26 +455,27 @@ export function usageFromEvents(format, value) {
442
455
  if (format === 'grok-end') {
443
456
  const usage = value?.usage
444
457
  if (!usage || typeof usage !== 'object') return null
445
- const reported = Number(value.total_cost_usd)
458
+ const reported = value.total_cost_usd
446
459
  return {
447
460
  usage: {
448
461
  inputTokens: count(usage.input_tokens), outputTokens: count(usage.output_tokens),
449
462
  cacheReadTokens: count(usage.cache_read_input_tokens), cacheWriteTokens: count(usage.cache_creation_input_tokens),
450
463
  },
451
- ...(value.cost_is_partial !== true && Number.isFinite(reported) && reported > 0 ? { reportedCostUsd: reported } : {}),
464
+ ...(value.cost_is_partial !== true && Number.isFinite(reported) && reported >= 0 ? { reportedCostUsd: reported } : {}),
452
465
  }
453
466
  }
454
467
  if (format === 'mimo-steps') {
455
468
  const parts = (Array.isArray(value) ? value : []).filter(part => part?.tokens && typeof part.tokens === 'object')
456
469
  if (!parts.length) return null
457
470
  const sum = pick => parts.reduce((total, part) => total + count(pick(part)), 0)
458
- const cost = parts.reduce((total, part) => total + (Number.isFinite(part.cost) && part.cost > 0 ? part.cost : 0), 0)
471
+ const costKnown = parts.every(part => Number.isFinite(part.cost) && part.cost >= 0)
472
+ const cost = costKnown ? parts.reduce((total, part) => total + part.cost, 0) : null
459
473
  return {
460
474
  usage: {
461
475
  inputTokens: sum(part => part.tokens.input), outputTokens: sum(part => part.tokens.output) + sum(part => part.tokens.reasoning),
462
476
  cacheReadTokens: sum(part => part.tokens.cache?.read), cacheWriteTokens: sum(part => part.tokens.cache?.write),
463
477
  },
464
- ...(cost > 0 ? { reportedCostUsd: Number(cost.toFixed(8)) } : {}),
478
+ ...(costKnown ? { reportedCostUsd: Number(cost.toFixed(8)) } : {}),
465
479
  }
466
480
  }
467
481
  if (format === 'minimax-result') {
@@ -648,11 +662,30 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
648
662
  }
649
663
  }
650
664
 
651
- function officialSuccess({ route, adapter, preference, answer, exitCode, version, credentialSource, usage = null }) {
665
+ function reportedCliModel(value) {
666
+ if (typeof value === 'string' && value.trim()) return value.trim()
667
+ if (value && typeof value === 'object' && !Array.isArray(value)
668
+ && typeof value.providerId === 'string' && value.providerId
669
+ && typeof value.modelId === 'string' && value.modelId) {
670
+ return `${value.providerId}/${value.modelId}`
671
+ }
672
+ return null
673
+ }
674
+
675
+ function officialSuccess({ route, adapter, preference, answer, exitCode, version, credentialSource, usage = null,
676
+ requestedCliModel = null, reportedModel = null, modelNotice = null, source, sandboxEnforcement }) {
677
+ const actualModel = reportedCliModel(reportedModel)
652
678
  return {
653
679
  ok: true,
654
680
  provider: route.provider,
655
681
  model: route.model,
682
+ recommendedModel: route.model,
683
+ requestedCliModel,
684
+ actualModel,
685
+ ...(reportedModel !== null && reportedModel !== undefined ? { reportedModel } : {}),
686
+ modelNotice: modelNotice ?? (!requestedCliModel
687
+ ? '本次使用厂商 CLI 已配置的默认模型;Harness 推荐模型 ID 未作为 CLI 模型参数。'
688
+ : actualModel ? null : '该 CLI 未返回可核验的实际模型 ID;请以厂商运行记录核对。'),
656
689
  preference,
657
690
  channel: 'official-cli',
658
691
  toolId: adapter.id,
@@ -664,8 +697,11 @@ function officialSuccess({ route, adapter, preference, answer, exitCode, version
664
697
  exitCode,
665
698
  version: version ?? null,
666
699
  credentialSource,
700
+ ...(typeof source === 'string' ? { source } : {}),
701
+ ...(typeof sandboxEnforcement === 'string' ? { sandboxEnforcement } : {}),
667
702
  ...(usage?.usage ? { usage: usage.usage } : {}),
668
- ...(usage?.reportedCostUsd ? { reportedCostUsd: usage.reportedCostUsd } : {}),
703
+ ...(Number.isFinite(usage?.reportedCostUsd) && usage.reportedCostUsd >= 0 ? { reportedCostUsd: usage.reportedCostUsd } : {}),
704
+ ...(usage?.usageIncomplete === true ? { usageIncomplete: true } : {}),
669
705
  }
670
706
  }
671
707
 
@@ -758,6 +794,13 @@ export async function executeAssignedTask({
758
794
  return officialSuccess({
759
795
  route, adapter, preference, answer: redact(verified.finalText, secret),
760
796
  exitCode: verified.exitCode ?? 0, version: null, credentialSource,
797
+ requestedCliModel: Object.hasOwn(verified, 'requestedModel') ? verified.requestedModel : modelId,
798
+ reportedModel: verified.reportedModel,
799
+ modelNotice: verified.modelNotice,
800
+ source: verified.source,
801
+ sandboxEnforcement: verified.sandboxEnforcement,
802
+ usage: { usage: verified.usage, reportedCostUsd: verified.reportedCostUsd,
803
+ usageIncomplete: verified.usageIncomplete },
761
804
  })
762
805
  }
763
806
  if (verified && verified.status !== 'unsupported') {
@@ -833,6 +876,7 @@ export async function executeAssignedTask({
833
876
  route, adapter, preference, answer, exitCode: run.exitCode,
834
877
  version: versionFromBanner(`${probe.stdout}\n${probe.stderr}`),
835
878
  credentialSource,
879
+ requestedCliModel: modelId,
836
880
  usage: usageFromOutput(adapter.format, run.stdout),
837
881
  })
838
882
  }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { OFFICIAL_TOOLS, getOfficialTool } from './official-tool-registry.mjs'
16
16
  import { compareReleaseVersions } from './latest-versions.mjs'
17
+ import { loginLaunchForProbe } from './official-tools-runtime.mjs'
17
18
 
18
19
  export const LOGIN_CHECK_TIMEOUT_MS = 5_000
19
20
  export const HEALTH_CACHE_MS = 10 * 60_000
@@ -34,7 +35,7 @@ export const LOGIN_GUIDES = Object.freeze({
34
35
 
35
36
  /** Environment variable names whose presence means a CLI can authenticate without a session. */
36
37
  const KEY_ENV = Object.freeze({
37
- 'claude-code': ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
38
+ 'claude-code': ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN'],
38
39
  codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
39
40
  gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
40
41
  'kimi-code': ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
@@ -92,7 +93,7 @@ function grokHomeHint(env) {
92
93
 
93
94
  /** Variables that make the CLI bill an API account rather than a subscription login. */
94
95
  const API_KEY_ENV = Object.freeze({
95
- 'claude-code': ['ANTHROPIC_API_KEY'],
96
+ 'claude-code': ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
96
97
  codex: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
97
98
  gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
98
99
  'kimi-code': ['KIMI_API_KEY', 'MOONSHOT_API_KEY'],
@@ -151,14 +152,30 @@ function guide(toolId) {
151
152
  * Login state for one installed tool: 'logged-in', 'logged-out', or 'unknown'.
152
153
  * `runner(executable, args, { timeoutMs })` resolves { ok, code, stdout, stderr, timedOut }.
153
154
  */
154
- export async function checkLogin(toolId, { runner, env = process.env, exists = async () => false, home = '', readAuthState = async () => null } = {}) {
155
+ export async function checkLogin(toolId, {
156
+ runner, env = process.env, exists = async () => false, home = '', readAuthState = async () => null,
157
+ probe, resolveLoginLaunch = loginLaunchForProbe,
158
+ } = {}) {
155
159
  const keyNames = (KEY_ENV[toolId] ?? []).filter(name => typeof env?.[name] === 'string' && env[name].trim())
156
160
  const viaKey = keyNames.length > 0 ? { state: 'logged-in', detail: `检测到环境变量 ${keyNames.join('、')}(未验证有效性)。`, source: 'environment',
157
161
  billing: apiKeyEnvPresent(toolId, env) ? 'api-key' : 'subscription',
158
162
  accountLogin: keyNames.some(name => !(API_KEY_ENV[toolId] ?? []).includes(name)) } : null
159
163
  const keyBilling = apiKeyEnvPresent(toolId, env) ? 'api-key' : null
164
+ // A health report must inspect the same npm/pnpm/native copy whose version
165
+ // was shown. Never replace a missing selected entry with a bare PATH command.
166
+ const statusCommand = async (command, args) => {
167
+ const launch = probe === undefined
168
+ ? { file: command, args: [] }
169
+ : await resolveLoginLaunch(probe)
170
+ if (!launch) return null
171
+ return runner(launch.file, [...launch.args, ...args], {
172
+ timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env),
173
+ ...(launch.useShell === undefined ? {} : { useShell: launch.useShell }),
174
+ })
175
+ }
160
176
  if (toolId === 'claude-code' && typeof runner === 'function') {
161
- const outcome = await runner('claude', ['auth', 'status', '--json'], { timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env) })
177
+ const outcome = await statusCommand('claude', ['auth', 'status', '--json'])
178
+ if (!outcome) return viaKey ?? { state: 'unknown', detail: '所选安装副本的登录检测入口不可用;未使用 PATH 中的其他 Claude Code。' }
162
179
  if (outcome.timedOut) return viaKey ?? { state: 'unknown', detail: '登录状态检测超时。' }
163
180
  try {
164
181
  const status = JSON.parse(outcome.stdout)
@@ -170,7 +187,8 @@ export async function checkLogin(toolId, { runner, env = process.env, exists = a
170
187
  return viaKey ?? { state: 'unknown', detail: '无法解析 claude auth status 输出。' }
171
188
  }
172
189
  if (toolId === 'codex' && typeof runner === 'function') {
173
- const outcome = await runner('codex', ['login', 'status'], { timeoutMs: LOGIN_CHECK_TIMEOUT_MS, env: sessionEnvironment(toolId, env) })
190
+ const outcome = await statusCommand('codex', ['login', 'status'])
191
+ if (!outcome) return viaKey ?? { state: 'unknown', detail: '所选安装副本的登录检测入口不可用;未使用 PATH 中的其他 Codex。' }
174
192
  if (outcome.timedOut) return viaKey ?? { state: 'unknown', detail: '登录状态检测超时。' }
175
193
  const output = `${outcome.stdout}\n${outcome.stderr}`
176
194
  if (/not logged in/i.test(output)) return viaKey ?? { state: 'logged-out', detail: 'codex login status 报告未登录。', source: 'cli' }
@@ -234,7 +252,7 @@ export async function checkToolHealth(probe, options = {}) {
234
252
  const tool = getOfficialTool(probe?.id)
235
253
  if (!tool) throw new TypeError('unknown official tool')
236
254
  const installed = probe.installed === true
237
- const login = installed ? await checkLogin(tool.id, options) : { state: 'unknown', detail: probe.status === 'not-installed' && /检测到已安装/.test(String(probe.detail ?? '')) ? String(probe.detail) : '尚未安装。' }
255
+ const login = installed ? await checkLogin(tool.id, { ...options, probe }) : { state: 'unknown', detail: probe.status === 'not-installed' && /检测到已安装/.test(String(probe.detail ?? '')) ? String(probe.detail) : '尚未安装。' }
238
256
  const { command, steps } = guide(tool.id)
239
257
  return {
240
258
  id: tool.id,
@@ -70,6 +70,26 @@ export function expandInstallDir(value, { home = '', env = {} } = {}) {
70
70
  return expanded
71
71
  }
72
72
 
73
+ /** ZCode's existing desktop bundle is selected independently of npm's prefix. */
74
+ export function normalizeZCodeInstallDir(value, options = {}) {
75
+ const input = String(value ?? '')
76
+ if (/[\x00-\x1f]/.test(input)) throw new Error('ZCode 安装位置包含无效字符。')
77
+ const raw = input.trim().replace(/^"(.*)"$/, '$1')
78
+ if (!raw) return ''
79
+ let path
80
+ try { path = expandInstallDir(raw, options) } catch (error) {
81
+ throw new Error(`ZCode 安装位置无效:${error.message}`)
82
+ }
83
+ // Discovery and Authenticode verification operate on local Windows files.
84
+ if (!/^[A-Za-z]:[\\/]/.test(path)) {
85
+ throw new Error('ZCode 安装位置必须是 Windows 本地绝对目录或 ZCode.exe 路径。')
86
+ }
87
+ if (/\.exe[\\/]*$/i.test(path) && !/[\\/]ZCode\.exe[\\/]*$/i.test(path)) {
88
+ throw new Error('ZCode 程序路径必须指向官方 ZCode.exe。')
89
+ }
90
+ return path.replace(/[\\/]+$/, '')
91
+ }
92
+
73
93
  /** Only https mirrors are accepted; a plain-http script source can be tampered with in transit. */
74
94
  export function normalizeSourceUrl(value, subject) {
75
95
  const raw = String(value ?? '').trim()
@@ -116,6 +136,7 @@ function keepKnownToolIds(raw, known, subject) {
116
136
  export function parseInstallPreferences(config = {}, { tools = [], home = '', env = {} } = {}) {
117
137
  const known = new Set(tools.map(tool => tool.id))
118
138
  const installDir = expandInstallDir(valueOf(config, 'toolInstallDir', ''), { home, env })
139
+ const zcodeInstallDir = normalizeZCodeInstallDir(valueOf(config, 'zcodeInstallDir', ''), { home, env })
119
140
  const registry = normalizeSourceUrl(valueOf(config, 'toolNpmRegistry', DEFAULT_NPM_REGISTRY), 'npm 源') || DEFAULT_NPM_REGISTRY
120
141
  const methodRaw = keepKnownToolIds(
121
142
  parseJsonObject(valueOf(config, 'toolInstallMethodsJson', '{}'), '安装方式覆盖'), known, '安装方式覆盖')
@@ -134,6 +155,7 @@ export function parseInstallPreferences(config = {}, { tools = [], home = '', en
134
155
  }
135
156
  return {
136
157
  installDir,
158
+ zcodeInstallDir,
137
159
  registry,
138
160
  methods,
139
161
  scriptUrls,