@ljwei-stak/dsh-model-router 0.17.1 → 0.18.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.
@@ -113,9 +113,9 @@ function keepKnownToolIds(raw, known, subject) {
113
113
  * Throws with a user-facing message on the first bad value, so the settings
114
114
  * form and the installer fail the same way.
115
115
  */
116
- export function parseInstallPreferences(config = {}, { tools = [] } = {}) {
116
+ export function parseInstallPreferences(config = {}, { tools = [], home = '', env = {} } = {}) {
117
117
  const known = new Set(tools.map(tool => tool.id))
118
- const installDir = expandInstallDir(valueOf(config, 'toolInstallDir', ''))
118
+ const installDir = expandInstallDir(valueOf(config, 'toolInstallDir', ''), { home, env })
119
119
  const registry = normalizeSourceUrl(valueOf(config, 'toolNpmRegistry', DEFAULT_NPM_REGISTRY), 'npm 源') || DEFAULT_NPM_REGISTRY
120
120
  const methodRaw = keepKnownToolIds(
121
121
  parseJsonObject(valueOf(config, 'toolInstallMethodsJson', '{}'), '安装方式覆盖'), known, '安装方式覆盖')
@@ -126,9 +126,11 @@ export function parseInstallPreferences(config = {}, { tools = [] } = {}) {
126
126
  for (const [id, methodId] of Object.entries(methodRaw)) methods[id] = methodId.trim()
127
127
  for (const [id, url] of Object.entries(scriptRaw)) scriptUrls[id] = normalizeSourceUrl(url, `${id} 安装脚本源`)
128
128
  for (const [id, methodId] of Object.entries(methods)) {
129
- if (!installMethodFor({ id, installMethods: tools.find(tool => tool.id === id)?.installMethods }, methodId)) {
129
+ const selected = installMethodFor({ id, installMethods: tools.find(tool => tool.id === id)?.installMethods }, methodId)
130
+ if (!selected) {
130
131
  throw new Error(`${id} 不支持安装方式 ${methodId}。`)
131
132
  }
133
+ methods[id] = selected.id
132
134
  }
133
135
  return {
134
136
  installDir,
@@ -141,26 +143,46 @@ export function parseInstallPreferences(config = {}, { tools = [] } = {}) {
141
143
  }
142
144
 
143
145
  /** Where a script install puts the binary: the user's directory, else the vendor default. */
144
- export function scriptInstallDir(tool, preferences, { home = '', method = null } = {}) {
145
- if (!vendorScriptRelative(tool, method)) return ''
146
- return preferences?.installDir || vendorScriptDir(tool, { home, method })
146
+ export function scriptInstallDir(tool, preferences, { home = '', env = {}, method } = {}) {
147
+ const binaryDir = vendorScriptDir(tool, { home, env, method })
148
+ if (!binaryDir) return ''
149
+ if (preferences?.installDir) return method?.installDirSubdir ? joinPath(preferences.installDir, method.installDirSubdir) : preferences.installDir
150
+ // Environment-only installers take a root and then add their own bin suffix.
151
+ const suffix = String(method?.installDirSuffix ?? '')
152
+ return suffix && new RegExp(`[\\\\/]${suffix}$`).test(binaryDir) ? binaryDir.slice(0, -suffix.length - 1) : binaryDir
147
153
  }
148
154
 
149
- function vendorScriptRelative(tool, method) {
150
- return String(method?.defaultInstallDir || tool?.defaultScriptInstallDir || '')
155
+ /** The location the vendor script uses on its own, ignoring any configured directory. */
156
+ export function vendorScriptDir(tool, { home = '', env = {}, method } = {}) {
157
+ const value = method?.defaultScriptInstallDir ?? tool?.defaultScriptInstallDir
158
+ const root = method?.defaultScriptInstallDirEnv ? env[method.defaultScriptInstallDirEnv] || home : home
159
+ return value ? joinPath(root, ...String(value).split('/').filter(Boolean)) : ''
151
160
  }
152
161
 
153
- /** The location the vendor script uses on its own, ignoring any configured directory. */
154
- export function vendorScriptDir(tool, { home = '', method = null } = {}) {
155
- return joinPath(home, ...vendorScriptRelative(tool, method).split('/').filter(Boolean))
162
+ /** The directory containing the package manager's global command shims. */
163
+ export function packageManagerBinDir(installDir, { platform = 'linux' } = {}) {
164
+ return installDir ? (platform === 'win32' ? installDir : joinPath(installDir, 'bin')) : ''
156
165
  }
157
166
 
158
- /** argv for a package-manager method, honouring the configured registry and prefix. */
159
- export function packageManagerArgs(method, preferences, { installDir = '' } = {}) {
167
+ function packageManagerDirectoryArgs(method, installDir, options) {
168
+ if (!installDir) return []
169
+ if (method.manager !== 'pnpm') return ['--prefix', installDir]
170
+ // pnpm does not implement npm's global --prefix layout. Its package tree and
171
+ // bin directory are separate settings; the config namespace works for add,
172
+ // install, remove and bin commands across pnpm 10.
173
+ return [
174
+ `--config.global-dir=${joinPath(installDir, '.pnpm', 'global')}`,
175
+ `--config.global-bin-dir=${packageManagerBinDir(installDir, options)}`,
176
+ `--config.store-dir=${joinPath(installDir, '.pnpm', 'store')}`,
177
+ ]
178
+ }
179
+
180
+ /** argv for a package-manager method, honouring its actual global layout. */
181
+ export function packageManagerArgs(method, preferences, { installDir = '', platform = 'linux' } = {}) {
160
182
  const registry = preferences?.registry || DEFAULT_NPM_REGISTRY
161
183
  const args = method.installArgs.filter(arg => !arg.startsWith('--registry='))
162
184
  args.push(`--registry=${registry}`)
163
- if (installDir) args.push('--prefix', installDir)
185
+ args.push(...packageManagerDirectoryArgs(method, installDir, { platform }))
164
186
  return [...args]
165
187
  }
166
188
 
@@ -171,6 +193,13 @@ function baseUrlOf(scriptUrl) {
171
193
  } catch { return '' }
172
194
  }
173
195
 
196
+ /** MiniMax's official installer itself accepts exactly these npm registries. */
197
+ export function minimaxScriptRegistry(registry) {
198
+ const raw = String(registry ?? '').trim()
199
+ const normalized = raw && !raw.endsWith('/') ? `${raw}/` : raw
200
+ return normalized === 'https://registry.npmjs.org/' || normalized === 'https://registry.npmmirror.com/' ? normalized : ''
201
+ }
202
+
174
203
  /**
175
204
  * The method to use: the requested one, else the user's saved choice, else the
176
205
  * registry default — and if none of those run on this platform, the first
@@ -181,14 +210,11 @@ function pickMethod(tool, methodId, preferences, platform) {
181
210
  const requested = methodId ?? preferences?.methods?.[tool.id]
182
211
  if (!requested) {
183
212
  const available = installMethodsFor(tool).filter(method => !installMethodBlockReason(method, platform))
184
- // An installed Windows MiniMax lives in the official installer layout.
185
- // npm does not move that copy, so the unpinned one-click update is the
186
- // vendor PowerShell script. Pinning npm or pnpm still runs that manager.
187
- if (platform === 'win32' && tool?.id === 'minimax-code' && preferences?.allowScriptInstall !== false) {
188
- const powershell = available.find(method => method.id === 'script-powershell')
189
- if (powershell) return powershell
190
- }
191
- return available[0] ?? defaultInstallMethod(tool)
213
+ const allowed = method => !installMethodBlockReason(method, platform)
214
+ && !(method.kind === 'script' && preferences?.allowScriptInstall === false)
215
+ const platformDefault = installMethodFor(tool, tool.defaultMethodsByPlatform?.[platform])
216
+ if (platformDefault && allowed(platformDefault)) return platformDefault
217
+ return available.find(allowed) ?? available[0] ?? defaultInstallMethod(tool)
192
218
  }
193
219
  // A named method is never silently swapped. If the tool does not declare it,
194
220
  // or it does not run here, the caller gets the reason instead of a surprise.
@@ -207,7 +233,7 @@ function unsupportedMethodError(tool) {
207
233
  * One concrete plan for one tool and one method. A thrown Error means the user
208
234
  * asked for something this tool cannot do, with a reason they can act on.
209
235
  */
210
- export function resolveInstallPlan(tool, methodId, preferences = {}, { platform = 'linux', home = '' } = {}) {
236
+ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform = 'linux', home = '', env = {} } = {}) {
211
237
  if (!tool || tool.unsupported) throw new Error(tool?.unsupportedReason ?? '此工具不支持一键安装。')
212
238
  const method = pickMethod(tool, methodId, preferences, platform)
213
239
  if (!method) throw new Error(unsupportedMethodError(tool))
@@ -219,12 +245,14 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
219
245
  const installDir = preferences?.installDir || ''
220
246
  if (method.kind === 'script') {
221
247
  const scriptUrl = preferences?.scriptUrls?.[tool.id] || method.scriptUrl
222
- const target = methodHonoursInstallDir(method) ? scriptInstallDir(tool, preferences, { home, method }) : ''
223
- const notices = []
224
- if (!target && installDir) {
225
- notices.push(`${method.label} 无法使用自定义安装目录(官方脚本写死了安装路径),将安装到 ${vendorScriptDir(tool, { home, method })}。`)
226
- }
227
- if (method.env?.includes('MCODE_NPM_REGISTRY') && preferences?.registry && !minimaxScriptRegistry(preferences.registry)) {
248
+ const target = methodHonoursInstallDir(method) ? scriptInstallDir(tool, preferences, { home, env, method }) : ''
249
+ const binDir = target
250
+ ? (method.installDirSuffix ? joinPath(target, method.installDirSuffix) : target)
251
+ : vendorScriptDir(tool, { home, env, method })
252
+ const notices = target ? [] : installDir
253
+ ? [`${method.label} 无法使用自定义安装目录(官方脚本写死了安装路径),将安装到 ${vendorScriptDir(tool, { home, env, method })}。`]
254
+ : []
255
+ if (method.registryEnv === 'MCODE_NPM_REGISTRY' && preferences.registry && !minimaxScriptRegistry(preferences.registry)) {
228
256
  notices.push('MiniMax 官方脚本只接受 https://registry.npmjs.org/ 或 https://registry.npmmirror.com/,本次不改写它的 npm 源。')
229
257
  }
230
258
  return {
@@ -235,12 +263,12 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
235
263
  label: method.label,
236
264
  shell: method.shell,
237
265
  scriptUrl,
266
+ downloadUrls: preferences?.scriptUrls?.[tool.id] ? [scriptUrl] : [...new Set([scriptUrl, ...(method.scriptFallbackUrls ?? [])])],
238
267
  verify: method.verify,
239
268
  installArg: method.installArgs,
240
- installDirEnv: method.installDirEnv || (method.env?.includes('STEP_INSTALL_DIR') ? 'STEP_INSTALL_DIR' : ''),
241
269
  installDir: target,
242
- relatives: scriptRelatives(tool, method),
243
- env: scriptEnv(method, { installDir: target, scriptUrl, registry: preferences?.registry }),
270
+ binDir,
271
+ env: scriptEnv(method, { installDir: target, scriptUrl, registry: preferences.registry }),
244
272
  notices,
245
273
  }
246
274
  }
@@ -251,8 +279,9 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
251
279
  kind: 'package-manager',
252
280
  label: method.label,
253
281
  manager: method.manager,
254
- args: packageManagerArgs(method, preferences, { installDir }),
282
+ args: packageManagerArgs(method, preferences, { installDir, platform }),
255
283
  installDir,
284
+ binDir: packageManagerBinDir(installDir, { platform }),
256
285
  env: {},
257
286
  notices: [],
258
287
  }
@@ -267,6 +296,7 @@ export function resolveUninstallPlan(tool, methodId, preferences = {}, options =
267
296
  : `${tool?.label ?? '此工具'} 没有可用的卸载方式。`)
268
297
  const blocked = installMethodBlockReason(method, platform)
269
298
  if (blocked) throw new Error(`${tool.label}:${blocked}`)
299
+ if (method.uninstallUnsupportedReason) throw new Error(method.uninstallUnsupportedReason)
270
300
  if (method.kind === 'script') {
271
301
  return { toolId: tool.id, method, methodId: method.id, kind: 'script', label: method.label, ...resolveInstallLocation(tool, method, preferences, options) }
272
302
  }
@@ -280,67 +310,41 @@ export function resolveUninstallPlan(tool, methodId, preferences = {}, options =
280
310
  args: [
281
311
  ...method.uninstallArgs,
282
312
  ...(preferences?.registry ? [`--registry=${preferences.registry}`] : []),
283
- ...(preferences?.installDir ? ['--prefix', preferences.installDir] : []),
313
+ ...packageManagerDirectoryArgs(method, preferences?.installDir || '', { platform }),
284
314
  ],
285
315
  installDir: preferences?.installDir || '',
316
+ binDir: packageManagerBinDir(preferences?.installDir || '', { platform }),
286
317
  }
287
318
  }
288
319
 
289
- /** Paths under the install directory that an uninstall may delete. No `..`. */
290
- export function scriptRelatives(tool, method) {
291
- const listed = method?.binaryRelatives?.length
292
- ? method.binaryRelatives
293
- : tool?.scriptBinaryRelatives?.length
294
- ? tool.scriptBinaryRelatives
295
- : tool?.scriptBinaryNames
296
- return [...(listed ?? [])]
297
- }
298
-
299
320
  /** Where the tool's binary lives, so a later uninstall can find it. */
300
- export function resolveInstallLocation(tool, method, preferences = {}, { home = '' } = {}) {
321
+ export function resolveInstallLocation(tool, method, preferences = {}, { home = '', platform = 'linux', env = {} } = {}) {
301
322
  if (!method) return null
302
323
  if (method.kind === 'script') {
303
324
  // A method that cannot honour the configured directory always lands in the
304
325
  // vendor's own location, whatever the user typed.
305
326
  const dir = methodHonoursInstallDir(method)
306
- ? scriptInstallDir(tool, preferences, { home, method })
307
- : vendorScriptDir(tool, { home, method })
308
- return {
309
- kind: 'script',
310
- dir,
311
- binaries: [...(tool.scriptBinaryNames ?? [])],
312
- relatives: scriptRelatives(tool, method),
313
- }
327
+ ? scriptInstallDir(tool, preferences, { home, env, method })
328
+ : vendorScriptDir(tool, { home, env, method })
329
+ const binDir = methodHonoursInstallDir(method) && method.installDirSuffix ? joinPath(dir, method.installDirSuffix) : dir
330
+ return { kind: 'script', dir: binDir, installDir: dir, binaries: [...(method.scriptBinaryNames ?? tool.scriptBinaryNames ?? [])] }
314
331
  }
315
- return { kind: 'package-manager', dir: preferences?.installDir || '', binaries: [], relatives: [] }
316
- }
317
-
318
- /** MiniMax's installer rejects every registry except these two. */
319
- const MINIMAX_SCRIPT_REGISTRIES = new Set([
320
- 'https://registry.npmjs.org/',
321
- 'https://registry.npmmirror.com/',
322
- ])
323
-
324
- export function minimaxScriptRegistry(registry) {
325
- const value = String(registry ?? '').trim()
326
- if (!value) return ''
327
- const normalized = value.endsWith('/') ? value : `${value}/`
328
- return MINIMAX_SCRIPT_REGISTRIES.has(normalized) ? normalized : ''
332
+ return { kind: 'package-manager', dir: packageManagerBinDir(preferences?.installDir || '', { platform }), installDir: preferences?.installDir || '', binaries: [] }
329
333
  }
330
334
 
331
335
  function scriptEnv(method, { installDir, scriptUrl, registry }) {
332
336
  const env = {}
333
- const dirEnv = method.installDirEnv || (method.env?.includes('STEP_INSTALL_DIR') ? 'STEP_INSTALL_DIR' : '')
334
- if (installDir && dirEnv) env[dirEnv] = installDir
337
+ if (installDir && method.installDirEnv) env[method.installDirEnv] = installDir
338
+ if (registry && method.registryEnv) {
339
+ const allowed = method.registryEnv === 'MCODE_NPM_REGISTRY' ? minimaxScriptRegistry(registry) : registry
340
+ if (allowed) env[method.registryEnv] = allowed
341
+ }
342
+ if (installDir && method.env?.includes('STEP_INSTALL_DIR')) env.STEP_INSTALL_DIR = installDir
335
343
  // The step installers read their release base from this variable, so a mirror
336
344
  // URL also moves the binaries and the checksum manifest, not just the script.
337
345
  if (method.env?.includes('STEP_RELEASE_BASE_URL') && scriptUrl) {
338
346
  const base = baseUrlOf(scriptUrl)
339
347
  if (base) env.STEP_RELEASE_BASE_URL = base
340
348
  }
341
- if (method.env?.includes('MCODE_NPM_REGISTRY')) {
342
- const allowed = minimaxScriptRegistry(registry)
343
- if (allowed) env.MCODE_NPM_REGISTRY = allowed
344
- }
345
349
  return env
346
350
  }
@@ -17,11 +17,11 @@ import { createReadStream } from 'node:fs'
17
17
  import { createHash } from 'node:crypto'
18
18
  import { npmAttestor } from './npm-attestation.mjs'
19
19
  import { isReleaseVersion } from './latest-versions.mjs'
20
- import { readFile, realpath, stat } from 'node:fs/promises'
20
+ import { readFile, readdir, realpath, stat } from 'node:fs/promises'
21
21
  import { createBrotliDecompress } from 'node:zlib'
22
22
  import { homedir } from 'node:os'
23
23
  import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from 'node:path'
24
- import { ensureNpmPrefixOnPath } from './official-tools-runtime.mjs'
24
+ import { ensureNpmPrefixOnPath, preferredToolDirectories } from './official-tools-runtime.mjs'
25
25
  import { usageFromEvents } from './task-executors.mjs'
26
26
 
27
27
  const TOOL_IDS = Object.freeze(['mimo-code', 'grok-build'])
@@ -50,18 +50,37 @@ function inside(parent, child) {
50
50
 
51
51
  function packagePath(modules, name) { return join(modules, ...name.split('/')) }
52
52
 
53
- function moduleRoots() {
54
- const path = process.env.PATH ?? process.env.Path ?? ''
53
+ /** npm prefixes and pnpm's versioned global tree share fixed package names. */
54
+ export async function installedModuleRoots(directories, { manager = null } = {}) {
55
55
  const roots = new Set()
56
- for (const raw of path.split(delimiter)) {
56
+ for (const raw of directories) {
57
57
  const directory = raw.trim().replace(/^"|"$/g, '')
58
58
  if (!directory) continue
59
- roots.add(join(directory, 'node_modules'))
60
- if (basename(directory).toLowerCase() === '.bin') roots.add(resolve(directory, '..'))
59
+ if (manager !== 'pnpm') {
60
+ roots.add(join(directory, 'node_modules'))
61
+ if (basename(directory).toLowerCase() === '.bin') roots.add(resolve(directory, '..'))
62
+ if (process.platform !== 'win32') roots.add(resolve(directory, '..', 'lib', 'node_modules'))
63
+ }
64
+ if (manager !== 'npm') {
65
+ if (/[\\/]global[\\/]\d+[\\/]node_modules[\\/]\.bin$/i.test(directory)) roots.add(resolve(directory, '..'))
66
+ const prefix = process.platform !== 'win32' && basename(directory).toLowerCase() === 'bin' ? resolve(directory, '..') : directory
67
+ for (const globalRoot of [join(prefix, '.pnpm', 'global'), join(prefix, 'global')]) {
68
+ try {
69
+ for (const entry of await readdir(globalRoot, { withFileTypes: true })) {
70
+ if (entry.isDirectory() && /^\d+$/.test(entry.name)) roots.add(join(globalRoot, entry.name, 'node_modules'))
71
+ }
72
+ } catch { /* this command directory has no pnpm global tree */ }
73
+ }
74
+ }
61
75
  }
62
76
  return [...roots]
63
77
  }
64
78
 
79
+ function entryDirectories(toolId) {
80
+ const preferred = preferredToolDirectories(toolId)
81
+ return preferred.length ? preferred : (process.env.PATH ?? process.env.Path ?? '').split(delimiter)
82
+ }
83
+
65
84
  async function manifestAt(directory, name, version = null) {
66
85
  try {
67
86
  const root = await realpath(directory)
@@ -91,24 +110,29 @@ async function realFileWithin(root, candidate, workspace) {
91
110
  }
92
111
 
93
112
  async function optionalPackage(wrapper, platform, modules, workspace) {
113
+ const boundary = await realpath(modules).catch(() => null)
114
+ if (!boundary || !inside(boundary, wrapper.root) || wrapper.root === workspace || inside(workspace, wrapper.root)) return null
94
115
  // The wrapper pins its platform package to its own version.
95
116
  const version = wrapper.manifest.optionalDependencies?.[platform.package]
96
117
  if (version !== wrapper.manifest.version) return null
97
118
  const candidates = [
98
119
  packagePath(join(wrapper.root, 'node_modules'), platform.package),
120
+ // pnpm links an optional dependency beside the real wrapper in the
121
+ // virtual store, rather than inside the wrapper's own node_modules.
122
+ packagePath(resolve(wrapper.root, '..', '..'), platform.package),
99
123
  packagePath(modules, platform.package),
100
124
  ]
101
125
  for (const candidate of candidates) {
102
126
  const optional = await manifestAt(candidate, platform.package, version)
103
- if (!optional || optional.root === workspace || inside(workspace, optional.root)) continue
127
+ if (!optional || !inside(boundary, optional.root) || optional.root === workspace || inside(workspace, optional.root)) continue
104
128
  return optional
105
129
  }
106
130
  return null
107
131
  }
108
132
 
109
- async function findMiMoExecutable(workspace, platform, attestor, problems) {
133
+ async function findMiMoExecutable(workspace, platform, attestor, problems, modulesRoots) {
110
134
  const wrapperInfo = WRAPPERS['mimo-code']
111
- for (const modules of moduleRoots()) {
135
+ for (const modules of modulesRoots) {
112
136
  const wrapper = await manifestAt(packagePath(modules, wrapperInfo.name), wrapperInfo.name)
113
137
  if (!wrapper || String(wrapper.manifest.bin?.[wrapperInfo.command]).replace(/^\.\//, '') !== wrapperInfo.bin) continue
114
138
  const optional = await optionalPackage(wrapper, platform, modules, workspace)
@@ -129,9 +153,9 @@ async function grokHome() {
129
153
  catch { return join(homedir(), '.grok') }
130
154
  }
131
155
 
132
- async function findGrokExecutable(workspace, platform, attestor, problems) {
156
+ async function findGrokExecutable(workspace, platform, attestor, problems, modulesRoots) {
133
157
  const wrapperInfo = WRAPPERS['grok-build']
134
- for (const modules of moduleRoots()) {
158
+ for (const modules of modulesRoots) {
135
159
  const wrapper = await manifestAt(packagePath(modules, wrapperInfo.name), wrapperInfo.name)
136
160
  if (!wrapper || String(wrapper.manifest.bin?.[wrapperInfo.command]).replace(/^\.\//, '') !== wrapperInfo.bin) continue
137
161
  const optional = await optionalPackage(wrapper, platform, modules, workspace)
@@ -167,7 +191,7 @@ async function findGrokExecutable(workspace, platform, attestor, problems) {
167
191
  * process runner, or an unsupported reason. The Host retains its own task,
168
192
  * workspace, model, approval, timeout, and cancellation validation.
169
193
  */
170
- export async function resolveMiMoGrokLaunch({ toolId, workspace, mode, modelId, task, attestor = npmAttestor }) {
194
+ export async function resolveMiMoGrokLaunch({ toolId, workspace, mode, modelId, task, attestor = npmAttestor, directories, moduleRoots, manager = null }) {
171
195
  if (!TOOL_IDS.includes(toolId)) return { unsupported: '未知的 MiMo/Grok 官方工具 ID。' }
172
196
  if (process.platform !== 'win32') return { unsupported: 'MiMo/Grok 原生入口目前仅完成 Windows 适配。' }
173
197
  if (!isAbsolute(workspace) || !['read-only', 'workspace-write'].includes(mode)) {
@@ -181,12 +205,13 @@ export async function resolveMiMoGrokLaunch({ toolId, workspace, mode, modelId,
181
205
  }
182
206
  const platform = PLATFORMS[toolId][process.arch]
183
207
  if (!platform) return { unsupported: '当前 Windows CPU 架构没有核验的官方原生程序。' }
184
- await ensureNpmPrefixOnPath()
208
+ if (!Array.isArray(moduleRoots) && !directories && preferredToolDirectories(toolId).length === 0) await ensureNpmPrefixOnPath()
185
209
  const directory = await realpath(workspace)
210
+ const modulesRoots = Array.isArray(moduleRoots) ? moduleRoots : await installedModuleRoots(directories ?? entryDirectories(toolId), { manager })
186
211
  const problems = []
187
212
  const found = toolId === 'mimo-code'
188
- ? await findMiMoExecutable(directory, platform, attestor, problems)
189
- : await findGrokExecutable(directory, platform, attestor, problems)
213
+ ? await findMiMoExecutable(directory, platform, attestor, problems, modulesRoots)
214
+ : await findGrokExecutable(directory, platform, attestor, problems, modulesRoots)
190
215
  if (!found) {
191
216
  return { unsupported: problems.length
192
217
  ? `官方原生程序未通过 npm registry 摘要核验:${problems[0]}`
package/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.18.0 — 2026-10-12
4
+
5
+ Stable minor under the npm latest tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading. Existing releases remain unchanged.
6
+
7
+ - Add verified vendor curl/irm channels for Kimi, Claude, Codex, MiniMax, MiMo and Grok. Gemini retains npm/pnpm, OpenCode adds its Unix script, Step uses vendor scripts, and ZCode opens its signed Windows installer. Fixed vendor paths and unsupported platform methods are explained on each row.
8
+ - Fix MiniMax’s official Windows installer rejection: leading PowerShell `<# ... #>` help is accepted; HTML pages and missing vendor markers are still refused. Legacy MiniMax updates preserve the discovered native install root, including attested repair-release pointers.
9
+ - Correct pnpm global package/bin/store directory options and retain MiniMax native dependency build flags. Updates probe the selected directory, never accept an unrelated newer PATH copy, and refuse known downgrades. Queued jobs keep their selected method, source and directory snapshot.
10
+ - Make shared installation settings an explicit Save/Discard form. Pending edits, saves and summary refreshes block install actions; saved unavailable methods retain their explanation. Removal stays available when future script installations are disabled.
11
+ - Resolve restricted execution entries from the selected installation. Claude/Codex native Windows files require publisher verification. Kimi/MiMo/Grok standalone script installations currently lack npm-file attestation and report this execution limit; npm/pnpm execution remains supported. Do not delete the Windows Codex standalone junction through generic script uninstall.
12
+ - Vendor downloads remain size-limited, staged and marker-checked. Windows retries through the system network/proxy transport when Node cannot retrieve an installer; the downloaded response is never executed during transport.
13
+ - Validation results are recorded in the release notes. Verification uses automated regressions, current official script downloads without execution, and the actual-component mock UI. No real vendor CLI installation or paid model request is part of release acceptance.
4
14
 
5
15
  ## 0.17.1 — 2026-10-11
6
16
 
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness Desktop
2
2
 
3
- Version: **0.16.4**. See [routing research](docs/ROUTING_RESEARCH.zh.md), [mathematical derivation and experiments](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md), and the [LiveBench evidence audit and validation protocol](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md). The latest public 2026-06-25 score/cost tables are aggregates; its paired question-quality/cost matrix has not been obtained, and no real routing gain is claimed.
3
+ Version: **0.18.0**. See [routing research](docs/ROUTING_RESEARCH.zh.md), [mathematical derivation and experiments](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md), and the [LiveBench evidence audit and validation protocol](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md). The latest public 2026-06-25 score/cost tables are aggregates; its paired question-quality/cost matrix has not been obtained, and no real routing gain is claimed.
4
4
 
5
5
  A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe a task, compare your configured models, inspect the plan, and explicitly choose when to execute it.
6
6
 
7
+ **0.18.0:** expands official vendor installer choices alongside npm/pnpm, preserves the configured install destination during updates, and verifies the target copy instead of an older PATH copy. MiniMax Windows scripts beginning with PowerShell `<# ... #>` help comments are accepted; HTML error pages remain rejected. Save unified install settings before installing or updating.
8
+
7
9
  **0.16.4:** PATH probes keep command output even when no install log callback is attached. 0.16.3 captured stdout only inside `onOutput?.(...)`, so `where` and `--version` looked empty and every PATH CLI was reported not-installed. MiniMax and ZCode were unaffected because they do not use that probe.
8
10
 
9
11
  **0.16.3:** restores official-tools detection and install for every registry card. Windows probes and `npm` are spawned with separate arguments again, so cmd no longer launches Node against a missing script. A version banner still counts as installed, one card's probe error stays on that row, and a CLI that is already current is not sent through “修复官方执行入口”.
@@ -14,7 +16,7 @@ A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe
14
16
 
15
17
  **0.16.0:** [dynamic data and adaptive feedback](docs/ROUTING_ADAPTIVE_LEARNING.zh.md) adds opt-in public benchmark/curated fixed-USD pricing refresh and task-specific, time-decayed, shrinkage-regularized subjective utility. Objective proxy quality floors remain separate. Public sources default off; preferences are shared within one local DSH_HOME and limited to the latest 200 runs. This is not a universal vendor-price scraper or evidence of real quality–cost gains.
16
18
 
17
- Current version: **0.16.4** (`@ljwei-stak/dsh-model-router`). Its declared host compatibility is **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
19
+ Current version: **0.18.0** (`@ljwei-stak/dsh-model-router`). Its declared host compatibility is **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
18
20
 
19
21
  **0.15.0 changes:** a four-tab workbench with routing advice directly below task planning, protection against stale execution previews, preserved spending totals across history pruning and retries, and a standalone mock UI preview for source development.
20
22
 
@@ -31,7 +33,7 @@ For self-testing, follow the [desktop acceptance and quality–cost experiment s
31
33
  | Routing presets | Economy, balanced and quality-first presets; quality floors, estimated token costs and bounded assignment search. Missing prices stay unknown. |
32
34
  | Read-only execution | Preview and confirm a routed run from the workbench, or invoke it in an official Harness session. Supported headless CLIs and model-directory API calls return recorded results. |
33
35
  | Editable CLI execution | One supported official CLI or a sequential CLI team, using approved isolated Git worktrees and conditional patch integration. |
34
- | Official tools | Detect, install/update, check launch readiness and inspect login status for eight registry tools; installation jobs have logs and cancellation. |
36
+ | Official tools | Detect, install/update, check launch readiness and inspect login status for ten registry tools; installation jobs have logs and cancellation. |
35
37
  | Interactive terminal | PowerShell/system shell and seven CLI targets, including fixed login commands, live output and multiple sessions. |
36
38
  | Costs and subscriptions | Daily/monthly budget checks, economy downgrade or pause, subscription-first routing, quota cooldown and explicit handling of other subscription failures. |
37
39
  | History and quality | Per-package results, dependency views, failure details, rerun/reassignment, ratings and optional stronger-model review. |
@@ -46,16 +48,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
46
48
  Open **Plugins → Add plugin** and enter:
47
49
 
48
50
  ```text
49
- @ljwei-stak/dsh-model-router@0.16.4
51
+ @ljwei-stak/dsh-model-router@0.18.0
50
52
  ```
51
53
 
52
- Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.16.4** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
54
+ Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.18.0** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
53
55
 
54
56
  You can instead install a local `.tgz` archive or its extracted inner `package` directory through the plugin manager. Both must contain `package.json` and `.dsh-plugin`. See [releases](https://github.com/Alice-Marx/dsh-model-router/releases) for archived builds, or build your own checkout below.
55
57
 
56
58
  ### Upgrade from the old package name
57
59
 
58
- The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.16.4`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
60
+ The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.18.0`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
59
61
 
60
62
  For the combined 0.11.x package, back up the profile and export the GAL saves you want to preserve before upgrading. Story playback, artwork, music and saves now belong to the independent `@ljwei-stak/dsh-galgame` package. The detailed save limitations are in [MIGRATION.md](docs/archive/MIGRATION.md).
61
63
 
@@ -151,9 +153,16 @@ Installers use fixed official npm packages at each vendor's `@latest`, the vendo
151
153
 
152
154
  **Unified install settings.** The top of the **官方工具** page (also in the plugin settings form) sets the install address for every tool at once: one install directory (empty means each vendor's default location), an npm source, and per-tool overrides for vendor installer-script mirrors. Directories must be absolute; sources must be https. Each row picks its own install method from `npm`, `pnpm` or the vendor script (`curl | bash` or `irm | iex`); a method that cannot run on this platform says so on the row instead of being silently swapped. The exact command is shown above the buttons before anything runs.
153
155
 
154
- **Script installs never pipe remote content into an interpreter.** The plugin downloads the vendor script, checks it is still that vendor's official installer (vendor markers required, HTML pages and other vendors' scripts refused), and runs the local copy. A mirror may rehost the same script; it may not substitute a different program. Step Code's script accepts an install directory; the OpenCode script hardcodes `~/.opencode/bin`, so a custom directory only applies to it through npm/pnpm's `--prefix`. Script installs can be switched off entirely, leaving only the package managers.
156
+ **Script installs never pipe remote content into an interpreter.** The plugin downloads the vendor script, checks it is still that vendor's official installer (vendor markers required, HTML pages and other vendors' scripts refused), and runs the local copy. A mirror may rehost the same script; it may not substitute a different program. Step Code's script accepts an install directory; the OpenCode script hardcodes `~/.opencode/bin`, so a custom directory only applies to it through the package managers' configured global directories. Script installs can be switched off entirely, leaving only the package managers.
157
+
158
+
159
+ **Update destination.** Click **保存安装设置** after changing the shared directory or sources. Tool actions wait until saving and command refresh finish. npm uses `--prefix`; pnpm uses its own `global-dir`, `global-bin-dir` and store settings. Install jobs retain the chosen configuration even if another settings page changes it while they are queued. Updates check the selected command directory, so a different current copy on PATH cannot make a missing target look installed. Vendor scripts with fixed paths explain their actual destination; ZCode still requires directory selection in its signed installer window.
160
+
161
+ The [workbench guide](./docs/WORKBENCH_USER_GUIDE.zh.md#厂商安装渠道) lists each vendor’s actual channels and native-install execution limits.
162
+
163
+ With no shared directory, ordinary checks discover the selected manager's packages on PATH. An install, update, repair or removal job resolves and locks that manager's default global destination before comparing versions. If that destination cannot be resolved safely, set and save a shared directory first.
155
164
 
156
- **One-click repair and uninstall.** Repair re-runs the same fixed official command, which is what a broken install needs even when it already reports the latest version. Uninstall removes only the program: a package-manager install goes back through the same manager, and a script install deletes just the executable the vendor installer placed plus that install's own PATH entry in the shell profile. **Configuration, credentials and session history are always kept.**
165
+ **One-click repair and uninstall.** Repair re-runs the same fixed official command, which is what a broken install needs even when it already reports the latest version. Uninstall removes only the program: a package-manager install goes back through the same manager, and a script install removes its registered command entries and supported PATH records; vendor version directories may remain. The Windows Codex native directory junction has no generic delete-based uninstall. The row explains this limitation. **Configuration, credentials and session history are always kept.**
157
166
 
158
167
  Installation, trusted launch readiness, account login and model entitlement are separate checks. An unknown login status is not proof of logout. On Windows, supported managed launch entries are checked against publisher signatures or official npm-tarball attestations; ZCode also records trust for its bundled script per signed build. See [the registry](.dsh-plugin/shared/official-tool-registry.mjs) and [executor](.dsh-plugin/shared/official-tool-executor.mjs) for the exact platform rules.
159
168
 
package/README.zh.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness 模型路由插件
2
2
 
3
- 版本:**0.16.4**。选模求解器概览见[路由研究说明](docs/ROUTING_RESEARCH.zh.md);公式、条件证明与实验见[数学推导与实验记录](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md)。[LiveBench 最新证据与验证协议](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md)已核查 2026-06-25 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
3
+ 版本:**0.18.0**。选模求解器概览见[路由研究说明](docs/ROUTING_RESEARCH.zh.md);公式、条件证明与实验见[数学推导与实验记录](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md)。[LiveBench 最新证据与验证协议](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md)已核查 2026-06-25 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
4
4
 
5
5
  为 **DeepSeek Harness Desktop** 提供任务规划、成本感知的模型分配和官方工具执行工作台。输入任务,比较已配置模型,审阅计划,再明确选择是否执行。
6
6
 
7
+ **0.18.0**:补齐厂商提供的 curl/irm 安装方式,安装与更新复用已保存的目录、方式和下载源,并核验目标目录里的版本。修复 MiniMax 官方 PowerShell 脚本以 `<# ... #>` 帮助注释开头时被误判为 HTML 的问题。统一安装设置改为明确保存后生效。
8
+
7
9
  **0.16.4**:PATH 探测在没有安装日志回调时也会保留命令输出。0.16.3 把 stdout 写进了 `onOutput?.(...)`,`where` 和 `--version` 因此变成空字符串,PATH 上的 CLI 全被报成未安装。MiniMax 与 ZCode 不走这条探测,所以仍能显示。
8
10
 
9
11
  **0.16.3**:恢复全部官方工具卡片的检测和安装。Windows 上的探测与 `npm` 再次按独立参数启动,避免 cmd 把 Node 指到不存在的脚本。只要输出版本号就视为已安装;单张卡片的检测错误留在该行;已经是最新版的 CLI 不再被收成「修复官方执行入口」。
@@ -14,7 +16,7 @@
14
16
 
15
17
  **0.16.0 新增**:[动态数据与持续反馈](docs/ROUTING_ADAPTIVE_LEARNING.zh.md):可选公开价格/LiveBench快照刷新;按任务类型、时间衰减和低样本收缩调整个人偏好,质量门槛独立保留。设置在“预算与安全”,公开源默认关闭;价格源需提供按官方文档核对的固定USD单价JSON,不是通用官网抓价器。本地DSH_HOME共享、最近200运行窗口,暂无真实收益证明。
16
18
 
17
- 当前版本为 **0.16.4**,包名为 `@ljwei-stak/dsh-model-router`;声明兼容的宿主为 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**。自 0.12.0 起,路由与 [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) 已拆成两个独立插件。
19
+ 当前版本为 **0.18.0**,包名为 `@ljwei-stak/dsh-model-router`;声明兼容的宿主为 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**。自 0.12.0 起,路由与 [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) 已拆成两个独立插件。
18
20
 
19
21
  **0.15.0 更新**:工作台分为四页,路由建议紧接任务规划展示;执行输入变化时丢弃旧预览;裁剪历史或重试后仍保留费用累计;源码开发新增独立模拟 UI 预览。
20
22
 
@@ -31,7 +33,7 @@
31
33
  | 路由方案 | 省钱优先、均衡、效果优先;按质量门槛、预估 token 成本与有限组合搜索分配模型。未知价格保持未知。 |
32
34
  | 只读执行 | 工作台先预览、再确认执行;也可在官方会话调用工具。支持的无界面 CLI 或模型目录 API 返回结果并留存记录。 |
33
35
  | 可编辑执行 | 通过官方会话启动单个 CLI 或顺序 CLI 团队,经审批在独立 Git 工作树修改,满足条件后整合补丁。 |
34
- | 官方工具 | 八个固定注册表工具的检测、安装/更新、可信入口与登录体检;安装有日志和取消操作。 |
36
+ | 官方工具 | 十个固定注册表工具的检测、安装/更新、可信入口与登录体检;安装有日志和取消操作。 |
35
37
  | 交互终端 | 系统终端/PowerShell 与七个 CLI,含固定登录命令、多会话和实时输出。 |
36
38
  | 成本与订阅 | 每日/月度预算检查、自动尝试省钱方案或暂停、订阅优先、额度冷却,以及其他订阅失败的确认流程。 |
37
39
  | 执行与质量记录 | 逐工作包结果、依赖图、失败详情、重跑/改派、用户评价与可选强模型复核。 |
@@ -46,16 +48,16 @@
46
48
  打开 **插件 → 添加插件**,填写:
47
49
 
48
50
  ```text
49
- @ljwei-stak/dsh-model-router@0.16.4
51
+ @ljwei-stak/dsh-model-router@0.18.0
50
52
  ```
51
53
 
52
- 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.4**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
54
+ 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.18.0**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
53
55
 
54
56
  也可在插件管理器填写本地 `.tgz` 文件的绝对路径,或解压后的内层 `package` 目录;其中应包含 `package.json` 与 `.dsh-plugin`。[历史发布页](https://github.com/Alice-Marx/dsh-model-router/releases)提供归档;已克隆源码的开发方式见下文。
55
57
 
56
58
  ### 从旧包名升级
57
59
 
58
- `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.4`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
60
+ `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.18.0`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
59
61
 
60
62
  从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](docs/archive/MIGRATION.md)。
61
63
 
@@ -151,9 +153,16 @@ CLI 团队是插件自有的顺序依赖执行器。Harness 内建 Agent Teams
151
153
 
152
154
  **统一安装设置。** 在**官方工具**页顶部(或插件设置页)可以一次设定所有工具的安装目录与下载源:统一安装目录(留空=各厂商默认位置)、npm 源地址,以及按工具 ID 覆盖厂商安装脚本的镜像地址。目录必须是绝对路径,下载地址必须是 https。每个工具都可以在**安装方式**下拉框里单独选择 `npm`、`pnpm`、厂商安装脚本(`curl | bash` 或 `irm | iex`);选了某个平台不支持的方式时,行内会写明原因而不是悄悄换成别的。每行上方实时显示将要执行的完整命令。
153
155
 
154
- **脚本方式不会把远程内容直接管道给解释器。** 插件先下载厂商安装脚本、核对它仍然是该厂商的官方脚本(校验厂商标记、拒绝 HTML 页面和其他厂商的脚本),再执行本地副本。镜像可以重新托管同一个脚本,但不能替换成别的程序。阶跃的脚本还支持安装目录参数,OpenCode 的官方脚本写死了 `~/.opencode/bin`,因此自定义安装目录对它只能通过 npm/pnpm 的 `--prefix` 生效。脚本方式可以整体关闭,只保留包管理器方式。
156
+ **脚本方式不会把远程内容直接管道给解释器。** 插件先下载厂商安装脚本、核对它仍然是该厂商的官方脚本(校验厂商标记、拒绝 HTML 页面和其他厂商的脚本),再执行本地副本。镜像可以重新托管同一个脚本,但不能替换成别的程序。阶跃的脚本还支持安装目录参数,OpenCode 的官方脚本写死了 `~/.opencode/bin`,因此自定义安装目录对它只能通过 npm/pnpm 的全局目录参数 生效。脚本方式可以整体关闭,只保留包管理器方式。
157
+
158
+
159
+ **更新位置。** 修改统一目录或下载源后,点击**保存安装设置**。保存和命令刷新完成前,安装与更新暂不可用。npm 使用 `--prefix`;pnpm 使用自身的全局包目录、命令目录和存储目录。排队中的任务保留点击时的配置快照。更新优先核验所选目录中的命令,避免 PATH 上另一份最新版让目标安装被误判为已完成。厂商固定目录的脚本会说明实际位置;ZCode 仍需在官方安装窗口选择目录。
160
+
161
+ 完整渠道与原生安装执行限制见[工作台指南](./docs/WORKBENCH_USER_GUIDE.zh.md#厂商安装渠道)。
162
+
163
+ 未设统一目录时,普通体检会从 PATH 发现所选管理器的安装;安装、更新、修复和卸载任务则先确定并锁定该管理器的默认全局目录,再比较版本。无法可靠确定默认目录时,请先填写并保存统一目录。
155
164
 
156
- **一键修复与一键卸载。** 修复会用同一条固定官方命令重新安装一遍,适合修复损坏或不完整的安装;卸载只删除程序本身——包管理器安装走同一个管理器卸载,脚本安装只删除厂商安装器放下的那一个可执行文件,并顺带清掉它在 shell 配置里写的 PATH 记录。**配置、登录信息和历史记录一律保留**,重新安装后仍在。
165
+ **一键修复与一键卸载。** 修复会用同一条固定官方命令重新安装一遍,适合修复损坏或不完整的安装;卸载只删除程序本身——包管理器安装走同一个管理器卸载,脚本安装移除登记的命令入口,并按厂商支持清理 PATH 记录,可能保留版本目录。Windows Codex 原生目录联接不使用通用删除卸载,界面会给出说明。**配置、登录信息和历史记录一律保留**,重新安装后仍在。
157
166
 
158
167
  **已安装、可信入口就绪、账号已登录、模型有权限是不同状态。** 登录未知不代表已退出。在 Windows,托管入口按发布者签名或官方 npm tarball 校验;ZCode 另对签名构建中的脚本记录首次信任摘要。平台细则见[注册表](.dsh-plugin/shared/official-tool-registry.mjs)和[执行器](.dsh-plugin/shared/official-tool-executor.mjs)。
159
168