@ljwei-stak/dsh-model-router 0.18.1 → 0.19.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 +210 -43
- package/.dsh-plugin/index.mjs +14 -7
- 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-tool-executor.mjs +7 -7
- package/.dsh-plugin/shared/official-tool-registry.mjs +20 -3
- package/.dsh-plugin/shared/official-tools-runtime.mjs +169 -24
- package/.dsh-plugin/shared/task-executors.mjs +52 -3
- package/.dsh-plugin/shared/tool-install-preferences.mjs +42 -1
- package/.dsh-plugin/shared/zcode-bundle.mjs +29 -8
- package/.dsh-plugin/shared/zcode-installer.mjs +2 -2
- package/CHANGELOG.md +21 -0
- package/README.md +10 -6
- package/README.zh.md +10 -6
- package/docs/INSTALLATION_GUIDE.zh.md +12 -12
- package/docs/WORKBENCH_USER_GUIDE.zh.md +13 -7
- package/package.json +2 -2
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import {
|
|
27
27
|
expandInstallDir,
|
|
28
28
|
resolveInstallLocation,
|
|
29
|
+
resolveLegacyScriptLocation,
|
|
29
30
|
resolveInstallPlan,
|
|
30
31
|
resolveUninstallPlan,
|
|
31
32
|
} from './tool-install-preferences.mjs'
|
|
@@ -59,7 +60,7 @@ export const CLAUDE_UPDATE_ARGS = Object.freeze(['update'])
|
|
|
59
60
|
* and script source). Refreshed on apply and live config updates, so a client
|
|
60
61
|
* can only ever pick a method id — these addresses never arrive over the wire.
|
|
61
62
|
*/
|
|
62
|
-
let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
|
|
63
|
+
let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true, zcodeInstallDir: '' }
|
|
63
64
|
|
|
64
65
|
export function setInstallPreferences(next) {
|
|
65
66
|
if (next && typeof next === 'object') {
|
|
@@ -308,16 +309,22 @@ async function fetchOfficialMiniMaxInstaller(signal) {
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
export function minimaxOfficialInstallDir(selectedPreferences = {}, home = homedir(), existingRoot = '') {
|
|
311
|
-
return selectedPreferences.installDir
|
|
312
|
+
return selectedPreferences.installDir
|
|
313
|
+
? resolveInstallPlan(getOfficialTool('minimax-code'), 'script-powershell', selectedPreferences, { platform: 'win32', home }).installDir
|
|
314
|
+
: existingRoot || join(home, '.minimax-code')
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
/** Keep a discovered native installation in place when its legacy updater runs. */
|
|
315
318
|
export function miniMaxFallbackPlan(selectedPreferences, beforeProbe, prefix, options = currentInstallOptions()) {
|
|
316
319
|
const installDir = minimaxOfficialInstallDir(selectedPreferences, options.home,
|
|
317
320
|
beforeProbe?.installed ? beforeProbe.installDir : '')
|
|
318
|
-
const executionPreferences = { ...selectedPreferences,
|
|
321
|
+
const executionPreferences = { ...selectedPreferences,
|
|
319
322
|
methods: { ...selectedPreferences.methods, 'minimax-code': 'script-powershell' } }
|
|
320
|
-
|
|
323
|
+
const plan = resolveInstallPlan(getOfficialTool('minimax-code'), 'script-powershell', selectedPreferences, options)
|
|
324
|
+
// installDir is already the discovered exclusive native root or the resolved
|
|
325
|
+
// child of the configured base. Never reinterpret it as another shared base.
|
|
326
|
+
return { plan: { ...plan, installDir, binDir: installDir,
|
|
327
|
+
env: { ...plan.env, MCODE_INSTALL_DIR: installDir } }, executionPreferences }
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
@@ -330,6 +337,7 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
|
330
337
|
pushLine(job, reason || '— npm 原生依赖未就绪;改用 MiniMax 官方 Windows 安装脚本(安装最新版)—')
|
|
331
338
|
const { plan, executionPreferences } = miniMaxFallbackPlan(job.preferences, job.beforeProbe, '')
|
|
332
339
|
const installDir = plan.installDir
|
|
340
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
333
341
|
await mkdir(installDir, { recursive: true })
|
|
334
342
|
prefixRoot = await realpath(installDir)
|
|
335
343
|
const script = await fetchOfficialMiniMaxInstaller(job.controller.signal)
|
|
@@ -354,6 +362,7 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
|
354
362
|
+ 'Import-Module -Name $env:MODEL_ROUTER_UTILITY_MODULE -ErrorAction Stop; '
|
|
355
363
|
+ '& $env:MODEL_ROUTER_MINIMAX_INSTALL_SCRIPT'
|
|
356
364
|
job.phase = 'installing'
|
|
365
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
357
366
|
const outcome = await runCapture(powershell,
|
|
358
367
|
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command], {
|
|
359
368
|
timeoutMs: INSTALL_TIMEOUT_MS, signal: job.controller.signal,
|
|
@@ -378,7 +387,7 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
|
378
387
|
probeCache.delete(job.tool.id)
|
|
379
388
|
const probe = await probeToolWith(job.tool, defaultRunner, { preferences: executionPreferences, plan, cacheMs: 0 })
|
|
380
389
|
job.postInstallProbe = probe
|
|
381
|
-
const readiness = await trustedExecutionReadiness(job.tool.id, { preferences: executionPreferences })
|
|
390
|
+
const readiness = await trustedExecutionReadiness(job.tool.id, { preferences: executionPreferences, plan })
|
|
382
391
|
if (!probe.installed || !readiness.ready) {
|
|
383
392
|
throw new Error(`官方安装器完成,但执行入口未通过 npm registry 摘要核验:${readiness.reason ?? probe.detail}`)
|
|
384
393
|
}
|
|
@@ -516,6 +525,95 @@ export function vendorScriptInvocation(plan, scriptPath) {
|
|
|
516
525
|
return { file: plan.shell === 'sh' ? 'sh' : 'bash', args: [scriptPath, ...directoryArgs] }
|
|
517
526
|
}
|
|
518
527
|
|
|
528
|
+
/** Resolve an existing ancestor too, so a missing child under a junction is checked. */
|
|
529
|
+
async function canonicalInstallPath(path) {
|
|
530
|
+
let current = resolve(path)
|
|
531
|
+
const suffix = []
|
|
532
|
+
for (;;) {
|
|
533
|
+
try { return join(await realpath(current), ...suffix) }
|
|
534
|
+
catch (error) {
|
|
535
|
+
if (error.code !== 'ENOENT') throw error
|
|
536
|
+
const parent = dirname(current)
|
|
537
|
+
if (parent === current) throw error
|
|
538
|
+
suffix.unshift(current.slice(parent.length).replace(/^[\\/]+/, ''))
|
|
539
|
+
current = parent
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** A vendor owns its target; its cleanup must never touch a shared npm prefix. */
|
|
545
|
+
export async function assertVendorInstallDirectory(tool, plan, selectedPreferences = {}, options = currentInstallOptions()) {
|
|
546
|
+
const directory = plan.installDir || plan.binDir
|
|
547
|
+
if (!directory) throw new Error('官方脚本安装目录无法确定,已停止安装。')
|
|
548
|
+
const canonical = await canonicalInstallPath(directory)
|
|
549
|
+
if (canonical.split(/[\\/]+/).some(part => ['node_modules', '.pnpm'].includes(part.toLowerCase()))) {
|
|
550
|
+
throw new Error('官方脚本不能写入 npm/pnpm 的包目录或共享存储目录。')
|
|
551
|
+
}
|
|
552
|
+
if (canonical === dirname(canonical) || canonical === await canonicalInstallPath(homedir())) {
|
|
553
|
+
throw new Error('官方脚本不能在磁盘根目录或用户主目录中执行。')
|
|
554
|
+
}
|
|
555
|
+
let codexOwnedRelease = false
|
|
556
|
+
if (tool.id === 'codex' && plan.shell === 'powershell') {
|
|
557
|
+
// The official Windows script keeps releases under the user's existing
|
|
558
|
+
// CODEX_HOME. CODEX_INSTALL_DIR selects only the visible bin junction.
|
|
559
|
+
// Respect that documented layout without redirecting the user's config.
|
|
560
|
+
const codexHome = options.env?.CODEX_HOME || join(options.home || homedir(), '.codex')
|
|
561
|
+
const homeRoot = await canonicalInstallPath(codexHome)
|
|
562
|
+
const releases = join(homeRoot, 'packages', 'standalone', 'releases')
|
|
563
|
+
const member = relative(releases, canonical)
|
|
564
|
+
const expectedVisible = !selectedPreferences.installDir || resolve(directory) === resolve(join(selectedPreferences.installDir, plan.method.installDirSubdir))
|
|
565
|
+
codexOwnedRelease = Boolean(expectedVisible && member && member !== '..' && !member.startsWith(`..${sep}`) && !isAbsolute(member))
|
|
566
|
+
}
|
|
567
|
+
if (selectedPreferences.installDir && plan.method?.supportsInstallDir) {
|
|
568
|
+
const base = await canonicalInstallPath(selectedPreferences.installDir)
|
|
569
|
+
const child = relative(base, canonical)
|
|
570
|
+
if (!codexOwnedRelease && (!child || child === '..' || child.startsWith(`..${sep}`) || isAbsolute(child))) {
|
|
571
|
+
throw new Error('官方脚本必须使用统一安装根目录下的工具专属子目录;已拒绝覆盖共享目录。')
|
|
572
|
+
}
|
|
573
|
+
const owner = plan.method.installDirSubdir?.split(/[\\/]+/)[0]
|
|
574
|
+
const ownedRoot = owner ? join(base, owner) : null
|
|
575
|
+
const ownedChild = ownedRoot ? relative(ownedRoot, canonical) : '..'
|
|
576
|
+
if (!codexOwnedRelease && (ownedChild === '..' || ownedChild.startsWith(`..${sep}`) || isAbsolute(ownedChild))) {
|
|
577
|
+
throw new Error('官方脚本的目录联接必须留在本工具专属目录中,不能指向其他工具。')
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// These fixed/default bin installers replace only their own launcher, not
|
|
581
|
+
// another CLI's runtime directory. Sharing ~/.local/bin is intentional.
|
|
582
|
+
if (plan.method?.sharedDefaultBin && (!selectedPreferences.installDir || !plan.method.supportsInstallDir)) return
|
|
583
|
+
const targetStat = await stat(directory).catch(error => {
|
|
584
|
+
if (error.code === 'ENOENT') return null
|
|
585
|
+
throw error
|
|
586
|
+
})
|
|
587
|
+
if (targetStat && !targetStat.isDirectory()) throw new Error('官方脚本安装目标不是目录。')
|
|
588
|
+
const packageText = await readFile(join(directory, 'package.json'), 'utf8').catch(error => {
|
|
589
|
+
if (error.code === 'ENOENT') return null
|
|
590
|
+
throw error
|
|
591
|
+
})
|
|
592
|
+
if (packageText) {
|
|
593
|
+
let name
|
|
594
|
+
try { name = JSON.parse(packageText).name } catch { throw new Error('安装目录已有无法核验归属的 package.json,已停止运行官方脚本。') }
|
|
595
|
+
const ownPackage = tool.package || (tool.id === 'stepcode' ? '@step-harness/coding-agent' : '')
|
|
596
|
+
if (!name || name !== ownPackage) throw new Error('安装目录已有其他工具或项目的 package.json,已停止运行官方脚本。')
|
|
597
|
+
}
|
|
598
|
+
for (const other of OFFICIAL_TOOLS) {
|
|
599
|
+
if (other.id === tool.id) continue
|
|
600
|
+
const commandDirectories = [...new Set([directory, plan.binDir].filter(Boolean))]
|
|
601
|
+
const candidates = commandDirectories.flatMap(dir => (other.probeExecutables ?? []).flatMap(name =>
|
|
602
|
+
[name, `${name}.exe`, `${name}.cmd`, `${name}.ps1`].map(file => join(dir, file))))
|
|
603
|
+
if (other.package) {
|
|
604
|
+
candidates.push(join(directory, 'node_modules', ...other.package.split('/'), 'package.json'))
|
|
605
|
+
candidates.push(join(directory, 'lib', 'node_modules', ...other.package.split('/'), 'package.json'))
|
|
606
|
+
}
|
|
607
|
+
for (const path of candidates) {
|
|
608
|
+
const found = await stat(path).catch(error => {
|
|
609
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null
|
|
610
|
+
throw error
|
|
611
|
+
})
|
|
612
|
+
if (found?.isFile()) throw new Error(`安装目录中已有 ${other.label} 的文件,已拒绝运行可能替换其资源的官方脚本。请使用独立目录。`)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
519
617
|
/**
|
|
520
618
|
* Install one tool from its vendor script: download, verify, stage, run the
|
|
521
619
|
* staged copy. The binary lands in a directory the plugin appends to its own
|
|
@@ -527,6 +625,7 @@ async function runVendorScriptInstallJob(job, plan) {
|
|
|
527
625
|
let staging = null
|
|
528
626
|
try {
|
|
529
627
|
job.phase = 'preflight'
|
|
628
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
530
629
|
const before = await probeToolWith(job.tool, defaultRunner, { plan, preferences: job.preferences, cacheMs: 0 })
|
|
531
630
|
job.beforeProbe = before
|
|
532
631
|
const latestBefore = before.installed ? await latestVersions.lookup(job.tool, { fresh: true }).catch(() => null) : null
|
|
@@ -548,6 +647,7 @@ async function runVendorScriptInstallJob(job, plan) {
|
|
|
548
647
|
await writeFile(scriptPath, isPowerShell ? `\uFEFF${source}` : source, 'utf8')
|
|
549
648
|
const invocation = vendorScriptInvocation(plan, scriptPath)
|
|
550
649
|
job.phase = 'installing'
|
|
650
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
551
651
|
pushLine(job, `$ ${plan.label}${invocation.args.length > 1 ? ` ${invocation.args.slice(1).join(' ')}` : ''}`)
|
|
552
652
|
const outcome = await runCapture(invocation.file, invocation.args, {
|
|
553
653
|
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
@@ -768,10 +868,13 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
|
|
|
768
868
|
}
|
|
769
869
|
if (tool.manager === 'signed-windows-installer') {
|
|
770
870
|
let rejected = null
|
|
771
|
-
const bundle = await discoverZCodeBundle({
|
|
871
|
+
const bundle = await discoverZCodeBundle({ installDir: (selectedPreferences ?? preferences).zcodeInstallDir,
|
|
872
|
+
onRejected: value => { rejected ??= value } })
|
|
772
873
|
if (bundle) return { id: tool.id, installed: true, version: bundle.version, status: 'installed',
|
|
773
|
-
detail: `${tool.probeNote} 安装目录:${bundle.root}`, bannerLine: `${tool.label} ${bundle.buildVersion}
|
|
774
|
-
|
|
874
|
+
detail: `${tool.probeNote} 安装目录:${bundle.root}`, bannerLine: `${tool.label} 桌面版 ${bundle.buildVersion}`,
|
|
875
|
+
installDir: bundle.root, cliEntry: bundle.cliEntry, source: bundle.source }
|
|
876
|
+
if (rejected) return { id: tool.id, installed: false, version: null, status: 'not-installed',
|
|
877
|
+
detail: rejected.version ? `检测到 ZCode ${rejected.version}(${rejected.root}):${rejected.rejected}` : rejected.rejected }
|
|
775
878
|
const other = (await unverifiedZCodeInstalls().catch(() => []))[0]
|
|
776
879
|
return { id: tool.id, installed: false, version: null, status: 'not-installed',
|
|
777
880
|
detail: other
|
|
@@ -827,6 +930,22 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
|
|
|
827
930
|
bannerLine: banner.split('\n').find(Boolean)?.slice(0, 200) ?? '' }
|
|
828
931
|
}
|
|
829
932
|
}
|
|
933
|
+
const legacy = resolveLegacyScriptLocation(tool, selectedPlan.method, selectedPreferences, currentInstallOptions())
|
|
934
|
+
if (legacy) {
|
|
935
|
+
for (const executable of tool.probeExecutables) {
|
|
936
|
+
const names = IS_WINDOWS ? [`${executable}.exe`, `${executable}.cmd`, executable] : [executable]
|
|
937
|
+
for (const name of names) {
|
|
938
|
+
const target = join(legacy.dir, name)
|
|
939
|
+
if (!(await fileExists(target))) continue
|
|
940
|
+
// Discovery is read-only. The old root can never satisfy a new job's
|
|
941
|
+
// target probe or become a script update/uninstall destination.
|
|
942
|
+
return { id: tool.id, installed: false, version: null, status: 'legacy-install',
|
|
943
|
+
installDir: selectedPlan.installDir, binDir: selectedPlan.binDir,
|
|
944
|
+
legacyExecutablePath: target,
|
|
945
|
+
detail: `发现旧共享目录中的 ${tool.label}:${target}。请安装到独立目录 ${selectedPlan.installDir};旧文件会保留,不能用旧目录覆盖其他工具。` }
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
830
949
|
return { id: tool.id, installed: false, version: null, status: 'not-installed',
|
|
831
950
|
installDir: selectedPlan.installDir, binDir: selectedPlan.binDir,
|
|
832
951
|
detail: `未在所选安装目录 ${selectedPlan.binDir} 中找到官方命令。` }
|
|
@@ -1006,6 +1125,7 @@ function jobView(job) {
|
|
|
1006
1125
|
exitCode: job.exitCode,
|
|
1007
1126
|
timedOut: job.timedOut,
|
|
1008
1127
|
error: job.error,
|
|
1128
|
+
executionReadiness: job.executionReadiness ?? null,
|
|
1009
1129
|
outputTail: [...job.outputTail],
|
|
1010
1130
|
outputTruncated: job.outputTruncated,
|
|
1011
1131
|
postInstallProbe: job.postInstallProbe,
|
|
@@ -1272,8 +1392,10 @@ export function installEnvironment(plan, base = process.env) {
|
|
|
1272
1392
|
return env
|
|
1273
1393
|
}
|
|
1274
1394
|
|
|
1275
|
-
/**
|
|
1276
|
-
async function finishVerifiedInstall(job, probe
|
|
1395
|
+
/** Installation and version must pass; verify execution only where an adapter supports it. */
|
|
1396
|
+
export async function finishVerifiedInstall(job, probe, {
|
|
1397
|
+
lookupLatest = tool => latestVersions.lookup(tool), readinessFor = trustedExecutionReadiness,
|
|
1398
|
+
} = {}) {
|
|
1277
1399
|
job.postInstallProbe = probe
|
|
1278
1400
|
if (job.cancelRequested) { markCancelled(job); return true }
|
|
1279
1401
|
if (!probe.installed) {
|
|
@@ -1282,14 +1404,15 @@ async function finishVerifiedInstall(job, probe) {
|
|
|
1282
1404
|
job.error = `安装命令已完成,但重探测失败:${probe.detail}`
|
|
1283
1405
|
return true
|
|
1284
1406
|
}
|
|
1285
|
-
const latest = await
|
|
1407
|
+
const latest = await lookupLatest(job.tool).catch(() => null)
|
|
1286
1408
|
if (latest?.version && versionStillOlder(probe.version, latest.version)) {
|
|
1287
1409
|
failBecauseStillOlder(job, probe, latest.version)
|
|
1288
1410
|
return true
|
|
1289
1411
|
}
|
|
1290
|
-
const readiness = await
|
|
1412
|
+
const readiness = await readinessFor(job.tool.id, { preferences: job.preferences, plan: job.plan })
|
|
1413
|
+
job.executionReadiness = readiness
|
|
1291
1414
|
if (job.cancelRequested) { markCancelled(job); return true }
|
|
1292
|
-
if (!readiness.ready) {
|
|
1415
|
+
if (!readiness.ready && readiness.supported !== false) {
|
|
1293
1416
|
if (IS_WINDOWS && job.tool.id === 'minimax-code' && !job.configured) {
|
|
1294
1417
|
await runOfficialMiniMaxInstaller(job)
|
|
1295
1418
|
return true
|
|
@@ -1300,8 +1423,11 @@ async function finishVerifiedInstall(job, probe) {
|
|
|
1300
1423
|
return true
|
|
1301
1424
|
}
|
|
1302
1425
|
job.status = 'succeeded'
|
|
1426
|
+
job.error = null
|
|
1303
1427
|
job.finishedAt = new Date().toISOString()
|
|
1304
|
-
pushLine(job,
|
|
1428
|
+
pushLine(job, readiness.supported === false
|
|
1429
|
+
? `— 已安装 ${job.tool.label} ${probe.version ?? ''},版本探测已通过;受限执行未启用:${readiness.reason} —`
|
|
1430
|
+
: `— 已安装 ${job.tool.label} ${probe.version ?? ''},官方执行入口已核验(新版本未经插件测试)—`)
|
|
1305
1431
|
return true
|
|
1306
1432
|
}
|
|
1307
1433
|
|
|
@@ -1378,11 +1504,13 @@ async function runInstallJob(installPlan, job) {
|
|
|
1378
1504
|
// exactly the case the user pressed 修复 for.
|
|
1379
1505
|
if (job.repair) {
|
|
1380
1506
|
pushLine(job, `— ${job.tool.label} ${before.version} 已是最新版本,按修复流程重新安装 —`)
|
|
1381
|
-
} else if (readiness.ready || !CAPABILITY_REPAIRABLE.has(job.tool.id)) {
|
|
1507
|
+
} else if (readiness.ready || readiness.supported === false || !CAPABILITY_REPAIRABLE.has(job.tool.id)) {
|
|
1382
1508
|
job.status = 'succeeded'
|
|
1383
1509
|
job.finishedAt = new Date().toISOString()
|
|
1384
1510
|
job.postInstallProbe = before
|
|
1511
|
+
job.executionReadiness = readiness
|
|
1385
1512
|
pushLine(job, `— ${job.tool.label} ${before.version} 已是最新版本,无需重复安装 —`)
|
|
1513
|
+
if (readiness.supported === false) pushLine(job, `— 受限执行未启用:${readiness.reason} —`)
|
|
1386
1514
|
return
|
|
1387
1515
|
} else {
|
|
1388
1516
|
pushLine(job, `— 已是最新版本 ${before.version},但执行入口未就绪:${readiness.reason};重新安装修复 —`)
|
|
@@ -1464,30 +1592,43 @@ async function runInstallJob(installPlan, job) {
|
|
|
1464
1592
|
await finishVerifiedInstall(job, probe)
|
|
1465
1593
|
}
|
|
1466
1594
|
|
|
1467
|
-
async function runZCodeInstallJob(job
|
|
1595
|
+
export async function runZCodeInstallJob(job, {
|
|
1596
|
+
probe = probeToolWith, lookup = (...args) => latestVersions.lookup(...args),
|
|
1597
|
+
openInstaller = openZCodeInstaller, capture = runCapture,
|
|
1598
|
+
} = {}) {
|
|
1468
1599
|
if (job.status !== 'running') return
|
|
1469
1600
|
job.phase = 'preflight'
|
|
1470
1601
|
probeCache.delete(job.tool.id)
|
|
1471
|
-
const before = await
|
|
1602
|
+
const before = await probe(job.tool, defaultRunner, { preferences: job.preferences, cacheMs: 0 })
|
|
1472
1603
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
1473
|
-
const latest = await
|
|
1604
|
+
const latest = await lookup(job.tool, { fresh: true }).catch(() => null)
|
|
1474
1605
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
1475
|
-
|
|
1606
|
+
const desktopOrder = before.installed && latest?.version ? compareReleaseVersions(before.version, latest.version) : null
|
|
1607
|
+
if (before.installed && desktopOrder === null) {
|
|
1608
|
+
job.status = 'failed'
|
|
1609
|
+
job.phase = 'done'
|
|
1610
|
+
job.finishedAt = new Date().toISOString()
|
|
1611
|
+
job.error = '无法核实 ZCode 最新桌面版本;保留当前安装,网络恢复后重试检查更新。'
|
|
1612
|
+
return
|
|
1613
|
+
}
|
|
1614
|
+
if (desktopOrder === 1 || (job.operation !== 'repair' && desktopOrder === 0)) {
|
|
1476
1615
|
job.status = 'succeeded'
|
|
1477
1616
|
job.phase = 'done'
|
|
1478
1617
|
job.finishedAt = new Date().toISOString()
|
|
1479
1618
|
job.postInstallProbe = before
|
|
1480
|
-
pushLine(job,
|
|
1619
|
+
pushLine(job, desktopOrder === 1
|
|
1620
|
+
? `— ZCode ${before.version} 高于下载页的 ${latest.version},不会打开旧版安装器降级;TUI 使用当前桌面内置入口 —`
|
|
1621
|
+
: `— 已找到 ZCode ${before.version}(最新 ${latest.version}),无需重复打开安装器;TUI 使用当前桌面内置入口 —`)
|
|
1481
1622
|
return
|
|
1482
1623
|
}
|
|
1483
|
-
const prefix = await
|
|
1624
|
+
const prefix = await capture('npm', ['config', 'get', 'prefix'], {
|
|
1484
1625
|
timeoutMs: 5_000, useShell: IS_WINDOWS, signal: job.controller.signal,
|
|
1485
1626
|
})
|
|
1486
1627
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
1487
1628
|
job.phase = 'downloading-and-verifying'
|
|
1488
1629
|
let lastProgress = 0
|
|
1489
1630
|
try {
|
|
1490
|
-
const opened = await
|
|
1631
|
+
const opened = await openInstaller({
|
|
1491
1632
|
latest,
|
|
1492
1633
|
npmPrefix: prefix.ok ? prefix.stdout.trim() : undefined,
|
|
1493
1634
|
signal: job.controller.signal,
|
|
@@ -1504,7 +1645,11 @@ async function runZCodeInstallJob(job) {
|
|
|
1504
1645
|
job.finishedAt = new Date().toISOString()
|
|
1505
1646
|
probeCache.delete(job.tool.id)
|
|
1506
1647
|
pushLine(job, `已验证发布者签名并打开 ZCode ${opened.version} 官方安装器:${opened.installerPath}`)
|
|
1507
|
-
|
|
1648
|
+
const desktopDirectory = before.installDir || (/[\\/]ZCode\.exe$/i.test(job.preferences.zcodeInstallDir ?? '')
|
|
1649
|
+
? dirname(job.preferences.zcodeInstallDir) : job.preferences.zcodeInstallDir)
|
|
1650
|
+
pushLine(job, desktopDirectory
|
|
1651
|
+
? `请在原厂安装界面选择原桌面安装目录 ${desktopDirectory};完成后重新体检,内置 TUI 随桌面版更新。`
|
|
1652
|
+
: '请在原厂安装界面选择独立的 ZCode 桌面目录;完成后保存该目录并重新体检,内置 TUI 随桌面版更新。')
|
|
1508
1653
|
} catch (error) {
|
|
1509
1654
|
if (job.cancelRequested || job.controller.signal.aborted) { markCancelled(job); return }
|
|
1510
1655
|
job.status = 'failed'
|
|
@@ -1575,5 +1720,5 @@ export function resetForTests() {
|
|
|
1575
1720
|
installJobs.clear()
|
|
1576
1721
|
installChain = Promise.resolve()
|
|
1577
1722
|
prefixPromise = null
|
|
1578
|
-
preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
|
|
1723
|
+
preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true, zcodeInstallDir: '' }
|
|
1579
1724
|
}
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
15
|
import { StringDecoder } from 'node:string_decoder'
|
|
16
16
|
import { realpath, stat } from 'node:fs/promises'
|
|
17
|
-
import { isAbsolute, win32 } from 'node:path'
|
|
17
|
+
import { dirname, isAbsolute, join, relative, sep, win32 } from 'node:path'
|
|
18
18
|
import { toolForProvider } from './official-tool-registry.mjs'
|
|
19
|
+
import { findGlobalPackageEntries, findNodeExecutable, officialToolEntrySelection } from './official-tool-executor.mjs'
|
|
20
|
+
import { installedModuleRoots } from './vendor-mimo-grok-adapter.mjs'
|
|
21
|
+
import { CODE_FILE, npmAttestor } from './npm-attestation.mjs'
|
|
19
22
|
import { normalizeExecutionPreference } from './model-profiles.mjs'
|
|
20
23
|
import { BILLING_MODE_LABEL, billingPlan, detectQuotaExhaustion, vendorKey } from './subscription-billing.mjs'
|
|
21
24
|
|
|
@@ -666,6 +669,45 @@ function officialSuccess({ route, adapter, preference, answer, exitCode, version
|
|
|
666
669
|
}
|
|
667
670
|
}
|
|
668
671
|
|
|
672
|
+
/** Gemini's Windows npm shim cannot be spawned without a shell; use its selected package bin. */
|
|
673
|
+
export async function resolvePortableToolLaunch(adapter, workspace, {
|
|
674
|
+
selection = adapter?.id === 'gemini' ? officialToolEntrySelection('gemini') : null,
|
|
675
|
+
findEntries = findGlobalPackageEntries, findNode = findNodeExecutable, attestor = npmAttestor,
|
|
676
|
+
} = {}) {
|
|
677
|
+
if (adapter?.id !== 'gemini') return { file: adapter.executable, args: [] }
|
|
678
|
+
const entries = await findEntries('@google/gemini-cli', 'gemini', 'dist/index.js', workspace, { selection })
|
|
679
|
+
const found = entries[0]
|
|
680
|
+
if (!found) return { reason: '未在所选安装目录中找到 @google/gemini-cli 的 gemini 入口;已回退模型目录 API。' }
|
|
681
|
+
const inside = (root, entry) => {
|
|
682
|
+
const part = relative(root, entry)
|
|
683
|
+
return part !== '' && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
|
|
684
|
+
}
|
|
685
|
+
const roots = Array.isArray(selection?.plan?.packageModuleRoots) ? selection.plan.packageModuleRoots
|
|
686
|
+
: await installedModuleRoots(selection?.directories ?? [], { manager: selection?.plan?.manager })
|
|
687
|
+
const prefix = selection?.plan?.installDir ? await realpath(selection.plan.installDir).catch(() => null) : null
|
|
688
|
+
if (selection?.plan?.installDir && !prefix) return { reason: 'Gemini 所选安装根无法确认,已拒绝启动并回退模型目录 API。' }
|
|
689
|
+
let contained = false
|
|
690
|
+
for (const declared of roots) {
|
|
691
|
+
const boundary = await realpath(declared).catch(() => null)
|
|
692
|
+
const parent = await realpath(dirname(declared)).catch(() => null)
|
|
693
|
+
if (!boundary || !parent || !inside(parent, boundary) || (prefix && !inside(prefix, boundary))) continue
|
|
694
|
+
if (inside(boundary, found.packageRoot) && inside(found.packageRoot, found.entry)
|
|
695
|
+
&& !inside(workspace, found.packageRoot) && found.packageRoot !== workspace) { contained = true; break }
|
|
696
|
+
}
|
|
697
|
+
if (!contained) return { reason: 'Gemini 包或入口离开了所选安装目录的包存储树,已拒绝启动并回退模型目录 API。' }
|
|
698
|
+
// The manifest controls bin/import resolution, so attest it as well as all
|
|
699
|
+
// package code. Checking name/version or a signed Node alone is insufficient.
|
|
700
|
+
const include = new RegExp(`${CODE_FILE.source}|(?:^|/)package\\.json$`, 'i')
|
|
701
|
+
const checked = await attestor.matchesPackage(found.packageRoot, '@google/gemini-cli', found.version, include)
|
|
702
|
+
if (!checked.ok) return { reason: `Gemini 安装文件未通过官方 npm 摘要核验:${checked.reason};已回退模型目录 API。` }
|
|
703
|
+
const member = relative(found.packageRoot, found.entry).split(sep).join('/')
|
|
704
|
+
const entryChecked = await attestor.matches(found.entry, '@google/gemini-cli', found.version, member)
|
|
705
|
+
if (!entryChecked.ok) return { reason: `Gemini 入口未通过官方 npm 摘要核验:${entryChecked.reason};已回退模型目录 API。` }
|
|
706
|
+
const node = await findNode(workspace, [20, 0, 0])
|
|
707
|
+
if (!node) return { reason: '未找到工作区外的 Node.js 20+ 可执行文件;已回退模型目录 API。' }
|
|
708
|
+
return { file: node, args: [found.entry], version: found.version, source: 'official-npm-registry-attested' }
|
|
709
|
+
}
|
|
710
|
+
|
|
669
711
|
/**
|
|
670
712
|
* Run one assigned model task. `auto` and `official` try the vendor CLI first.
|
|
671
713
|
* `api` skips the CLI. A missing tool or a failed process uses `apiFallback`.
|
|
@@ -673,6 +715,7 @@ function officialSuccess({ route, adapter, preference, answer, exitCode, version
|
|
|
673
715
|
export async function executeAssignedTask({
|
|
674
716
|
route, task, workspace, timeoutMs, signal, credentials = null, spawnImpl = spawn,
|
|
675
717
|
apiFallback, runVerified = null, onChunk = null, skipOfficial = null, stopGraceMs, credentialMode = 'default',
|
|
718
|
+
resolvePortableLaunch = resolvePortableToolLaunch,
|
|
676
719
|
} = {}) {
|
|
677
720
|
const prompt = checkedTask(task)
|
|
678
721
|
const preference = executionPreference(route)
|
|
@@ -734,7 +777,13 @@ export async function executeAssignedTask({
|
|
|
734
777
|
})
|
|
735
778
|
}
|
|
736
779
|
const { env, secret, credentialSource } = childEnvironment(adapter, credentials, credentialMode)
|
|
737
|
-
|
|
780
|
+
let launch
|
|
781
|
+
try { launch = await resolvePortableLaunch(adapter, cwd) } catch (error) {
|
|
782
|
+
launch = { reason: `${adapter.label} 的安装入口无法读取:${String(error?.message ?? error).slice(0, 200)};已回退模型目录 API。` }
|
|
783
|
+
}
|
|
784
|
+
if (!launch?.file) return useApi(apiFallback, { route, task: prompt, signal, adapter, preference,
|
|
785
|
+
reason: launch?.reason ?? `${adapter.label} 未安装或无法启动,已回退模型目录 API。` })
|
|
786
|
+
const probe = await captureProcess(spawnImpl, launch.file, [...launch.args, '--version'], {
|
|
738
787
|
cwd, env, stdin: '', timeoutMs: Math.min(PROBE_TIMEOUT_MS, timeout), signal, secret,
|
|
739
788
|
})
|
|
740
789
|
if (probe.spawnError || probe.cancelled || probe.timedOut || probe.exitCode !== 0) {
|
|
@@ -748,7 +797,7 @@ export async function executeAssignedTask({
|
|
|
748
797
|
return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, reason })
|
|
749
798
|
}
|
|
750
799
|
const args = adapter.buildArgs(modelId)
|
|
751
|
-
const run = await captureProcess(spawnImpl,
|
|
800
|
+
const run = await captureProcess(spawnImpl, launch.file, [...launch.args, ...args], {
|
|
752
801
|
cwd, env, stdin: prompt, timeoutMs: timeout, signal, onChunk, secret,
|
|
753
802
|
...(Number.isSafeInteger(stopGraceMs) && stopGraceMs >= 10 && stopGraceMs <= STOP_GRACE_MS ? { stopGraceMs } : {}),
|
|
754
803
|
})
|
|
@@ -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,
|
|
@@ -249,12 +271,17 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
|
|
|
249
271
|
const binDir = target
|
|
250
272
|
? (method.installDirSuffix ? joinPath(target, method.installDirSuffix) : target)
|
|
251
273
|
: vendorScriptDir(tool, { home, env, method })
|
|
252
|
-
const notices = target
|
|
274
|
+
const notices = target && installDir && method.installDirSubdir
|
|
275
|
+
? [`官方脚本在工具专属子目录 ${target} 维护资源,保护统一安装根中的其他工具。`]
|
|
276
|
+
: target ? [] : installDir
|
|
253
277
|
? [`${method.label} 无法使用自定义安装目录(官方脚本写死了安装路径),将安装到 ${vendorScriptDir(tool, { home, env, method })}。`]
|
|
254
278
|
: []
|
|
255
279
|
if (method.registryEnv === 'MCODE_NPM_REGISTRY' && preferences.registry && !minimaxScriptRegistry(preferences.registry)) {
|
|
256
280
|
notices.push('MiniMax 官方脚本只接受 https://registry.npmjs.org/ 或 https://registry.npmmirror.com/,本次不改写它的 npm 源。')
|
|
257
281
|
}
|
|
282
|
+
if (tool.id === 'codex' && method.id === 'script-powershell') {
|
|
283
|
+
notices.push('Codex Windows 脚本将命令目录联接到用户 Codex 资源目录;统一根控制命令位置,运行资源仍由厂商保存在用户目录。需要所有文件位于统一根时请选 npm/pnpm。')
|
|
284
|
+
}
|
|
258
285
|
return {
|
|
259
286
|
toolId: tool.id,
|
|
260
287
|
method,
|
|
@@ -332,6 +359,20 @@ export function resolveInstallLocation(tool, method, preferences = {}, { home =
|
|
|
332
359
|
return { kind: 'package-manager', dir: packageManagerBinDir(preferences?.installDir || '', { platform }), installDir: preferences?.installDir || '', binaries: [] }
|
|
333
360
|
}
|
|
334
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Read-only compatibility location for scripts installed directly into a
|
|
364
|
+
* configured shared root before dedicated script directories were introduced.
|
|
365
|
+
* Never use this result as an install/update target: those installers can
|
|
366
|
+
* replace a whole runtime tree belonging to another tool.
|
|
367
|
+
*/
|
|
368
|
+
export function resolveLegacyScriptLocation(tool, method, preferences = {}, options = {}) {
|
|
369
|
+
if (!preferences.installDir || !method?.legacySharedInstallDir || !methodHonoursInstallDir(method)) return null
|
|
370
|
+
return {
|
|
371
|
+
...resolveInstallLocation(tool, { ...method, installDirSubdir: undefined }, preferences, options),
|
|
372
|
+
legacySharedInstallDir: true,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
335
376
|
function scriptEnv(method, { installDir, scriptUrl, registry }) {
|
|
336
377
|
const env = {}
|
|
337
378
|
if (installDir && method.installDirEnv) env[method.installDirEnv] = installDir
|
|
@@ -46,6 +46,14 @@ function powershellPath(env) {
|
|
|
46
46
|
return join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** PowerShell 7's module path can prevent Windows PowerShell 5.1 loading Security. */
|
|
50
|
+
export function zcodeWindowsPowerShellEnvironment(env = process.env) {
|
|
51
|
+
return {
|
|
52
|
+
...env,
|
|
53
|
+
PSModulePath: join(env.SystemRoot || env.windir || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'Modules'),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
async function powershellJson(script, env, extraEnv = {}) {
|
|
50
58
|
if (process.platform !== 'win32') return null
|
|
51
59
|
const command = Buffer.from(
|
|
@@ -62,7 +70,7 @@ async function powershellJson(script, env, extraEnv = {}) {
|
|
|
62
70
|
encoding: 'utf8',
|
|
63
71
|
timeout: 10_000,
|
|
64
72
|
maxBuffer: 256_000,
|
|
65
|
-
env: { ...env, ...extraEnv },
|
|
73
|
+
env: { ...zcodeWindowsPowerShellEnvironment(env), ...extraEnv },
|
|
66
74
|
})
|
|
67
75
|
const output = stdout.trim().replace(/^\uFEFF/, '')
|
|
68
76
|
return output ? JSON.parse(output) : null
|
|
@@ -94,13 +102,21 @@ function rootFromCommand(value) {
|
|
|
94
102
|
return executable && win32.isAbsolute(executable) ? dirname(executable) : null
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
/** Explicit selection is authoritative, even when the selected bundle is missing. */
|
|
106
|
+
export function zcodeDiscoveryCandidates({ installDir = '', env = process.env, records = [] } = {}) {
|
|
98
107
|
const candidates = []
|
|
99
108
|
const add = (path, source, registryVersion = null) => {
|
|
100
109
|
if (typeof path !== 'string' || !path.trim()) return
|
|
101
110
|
candidates.push({ path: path.trim().replace(/^"|"$/g, ''), source, registryVersion })
|
|
102
111
|
}
|
|
103
|
-
|
|
112
|
+
if (installDir != null && String(installDir).trim()) {
|
|
113
|
+
add(installDir, 'operator-config')
|
|
114
|
+
return candidates
|
|
115
|
+
}
|
|
116
|
+
if (env[ZCODE_INSTALL_ENV_VAR] != null && String(env[ZCODE_INSTALL_ENV_VAR]).trim()) {
|
|
117
|
+
add(env[ZCODE_INSTALL_ENV_VAR], 'operator-env')
|
|
118
|
+
return candidates
|
|
119
|
+
}
|
|
104
120
|
for (const record of records) {
|
|
105
121
|
const version = String(record.displayVersion || '') || null
|
|
106
122
|
add(record.installLocation, 'uninstall-registry', version)
|
|
@@ -204,7 +220,7 @@ async function checkedBundle(candidate, env, trustPath) {
|
|
|
204
220
|
const input = candidate.path
|
|
205
221
|
if (!localWindowsPath(input)) return null
|
|
206
222
|
try {
|
|
207
|
-
const initial =
|
|
223
|
+
const initial = /[\\/]zcode\.exe$/i.test(input) ? dirname(input) : input
|
|
208
224
|
const root = await realpath(initial)
|
|
209
225
|
if (!localWindowsPath(root) || !(await stat(root)).isDirectory()) return null
|
|
210
226
|
const executable = await realpath(join(root, 'ZCode.exe'))
|
|
@@ -239,18 +255,23 @@ async function checkedBundle(candidate, env, trustPath) {
|
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
257
|
|
|
242
|
-
/**
|
|
243
|
-
export async function discoverZCodeBundle({ env = process.env, trustPath = zcodeTrustPath(env), onRejected } = {}) {
|
|
258
|
+
/** A configured desktop location never falls back to a different installation. */
|
|
259
|
+
export async function discoverZCodeBundle({ installDir = '', env = process.env, trustPath = zcodeTrustPath(env), onRejected } = {}) {
|
|
244
260
|
if (process.platform !== 'win32') return null
|
|
245
|
-
const
|
|
261
|
+
const explicit = Boolean(String(installDir || env[ZCODE_INSTALL_ENV_VAR] || '').trim())
|
|
262
|
+
const records = explicit ? [] : await uninstallRecords(env)
|
|
246
263
|
const seen = new Set()
|
|
247
|
-
for (const candidate of
|
|
264
|
+
for (const candidate of zcodeDiscoveryCandidates({ installDir, env, records })) {
|
|
248
265
|
const key = win32.normalize(candidate.path).toLowerCase()
|
|
249
266
|
if (seen.has(key)) continue
|
|
250
267
|
seen.add(key)
|
|
251
268
|
const bundle = await checkedBundle(candidate, env, trustPath)
|
|
252
269
|
if (bundle?.rejected) { onRejected?.(bundle); continue }
|
|
253
270
|
if (bundle) return bundle
|
|
271
|
+
if (explicit) onRejected?.({
|
|
272
|
+
root: candidate.path,
|
|
273
|
+
rejected: `指定的 ZCode 安装位置 ${candidate.path} 不存在,或未通过官方签名、桌面内置 CLI 和目录边界核验;请检查独立安装位置。`,
|
|
274
|
+
})
|
|
254
275
|
}
|
|
255
276
|
return null
|
|
256
277
|
}
|
|
@@ -19,7 +19,7 @@ import { dirname, isAbsolute, join, relative, sep } from 'node:path'
|
|
|
19
19
|
import { Readable, Transform } from 'node:stream'
|
|
20
20
|
import { pipeline } from 'node:stream/promises'
|
|
21
21
|
import { promisify } from 'node:util'
|
|
22
|
-
import { ZCODE_SIGNER } from './zcode-bundle.mjs'
|
|
22
|
+
import { ZCODE_SIGNER, zcodeWindowsPowerShellEnvironment } from './zcode-bundle.mjs'
|
|
23
23
|
import { getOfficialTool } from './official-tool-registry.mjs'
|
|
24
24
|
import { latestVersions } from './latest-versions.mjs'
|
|
25
25
|
|
|
@@ -118,7 +118,7 @@ async function verifyInstallerSignature(path, env) {
|
|
|
118
118
|
encoding: 'utf8',
|
|
119
119
|
timeout: 120_000,
|
|
120
120
|
maxBuffer: 16_384,
|
|
121
|
-
env: { ...env, MODEL_ROUTER_ZCODE_INSTALLER_PATH: path },
|
|
121
|
+
env: { ...zcodeWindowsPowerShellEnvironment(env), MODEL_ROUTER_ZCODE_INSTALLER_PATH: path },
|
|
122
122
|
})
|
|
123
123
|
const signature = JSON.parse(stdout.trim().replace(/^\uFEFF/, ''))
|
|
124
124
|
return signature.status === 'Valid'
|