@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.
- package/.dsh-plugin/client.js +257 -57
- package/.dsh-plugin/index.mjs +233 -81
- package/.dsh-plugin/official-tools-remote-service.mjs +7 -3
- package/.dsh-plugin/shared/cli-terminal-protocol.mjs +15 -5
- package/.dsh-plugin/shared/cli-terminal.mjs +126 -18
- package/.dsh-plugin/shared/official-team-runtime.mjs +189 -49
- package/.dsh-plugin/shared/official-tool-executor.mjs +343 -70
- package/.dsh-plugin/shared/official-tool-registry.mjs +2 -2
- package/.dsh-plugin/shared/official-tools-remote.mjs +12 -0
- package/.dsh-plugin/shared/official-tools-runtime.mjs +55 -14
- package/.dsh-plugin/shared/run-ledger.mjs +51 -2
- package/.dsh-plugin/shared/task-executors.mjs +62 -18
- package/.dsh-plugin/shared/tool-health.mjs +24 -6
- package/.dsh-plugin/shared/tool-install-preferences.mjs +22 -0
- package/.dsh-plugin/shared/zcode-bundle.mjs +29 -8
- package/.dsh-plugin/shared/zcode-installer.mjs +2 -2
- package/CHANGELOG.md +29 -0
- package/README.md +22 -14
- package/README.zh.md +21 -13
- package/docs/INSTALLATION_GUIDE.zh.md +6 -6
- package/docs/WORKBENCH_USER_GUIDE.zh.md +36 -8
- package/docs/releases/0.20.0-validation.md +26 -0
- package/package.json +4 -3
|
@@ -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({
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
1625
|
+
const latest = await lookup(job.tool, { fresh: true }).catch(() => null)
|
|
1599
1626
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
1600
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
...(
|
|
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
|
|
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, {
|
|
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
|
|
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
|
|
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,
|