@ljwei-stak/dsh-model-router 0.17.0 → 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,22 +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 = '' } = {}) {
145
- if (!tool?.defaultScriptInstallDir) return ''
146
- return preferences?.installDir || vendorScriptDir(tool, { home })
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
155
  /** The location the vendor script uses on its own, ignoring any configured directory. */
150
- export function vendorScriptDir(tool, { home = '' } = {}) {
151
- return joinPath(home, ...String(tool?.defaultScriptInstallDir ?? '').split('/').filter(Boolean))
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)) : ''
152
160
  }
153
161
 
154
- /** argv for a package-manager method, honouring the configured registry and prefix. */
155
- export function packageManagerArgs(method, preferences, { installDir = '' } = {}) {
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')) : ''
165
+ }
166
+
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' } = {}) {
156
182
  const registry = preferences?.registry || DEFAULT_NPM_REGISTRY
157
183
  const args = method.installArgs.filter(arg => !arg.startsWith('--registry='))
158
184
  args.push(`--registry=${registry}`)
159
- if (installDir) args.push('--prefix', installDir)
185
+ args.push(...packageManagerDirectoryArgs(method, installDir, { platform }))
160
186
  return [...args]
161
187
  }
162
188
 
@@ -167,6 +193,13 @@ function baseUrlOf(scriptUrl) {
167
193
  } catch { return '' }
168
194
  }
169
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
+
170
203
  /**
171
204
  * The method to use: the requested one, else the user's saved choice, else the
172
205
  * registry default — and if none of those run on this platform, the first
@@ -176,7 +209,12 @@ function baseUrlOf(scriptUrl) {
176
209
  function pickMethod(tool, methodId, preferences, platform) {
177
210
  const requested = methodId ?? preferences?.methods?.[tool.id]
178
211
  if (!requested) {
179
- return installMethodsFor(tool).find(method => !installMethodBlockReason(method, platform)) ?? defaultInstallMethod(tool)
212
+ const available = installMethodsFor(tool).filter(method => !installMethodBlockReason(method, platform))
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)
180
218
  }
181
219
  // A named method is never silently swapped. If the tool does not declare it,
182
220
  // or it does not run here, the caller gets the reason instead of a surprise.
@@ -195,7 +233,7 @@ function unsupportedMethodError(tool) {
195
233
  * One concrete plan for one tool and one method. A thrown Error means the user
196
234
  * asked for something this tool cannot do, with a reason they can act on.
197
235
  */
198
- export function resolveInstallPlan(tool, methodId, preferences = {}, { platform = 'linux', home = '' } = {}) {
236
+ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform = 'linux', home = '', env = {} } = {}) {
199
237
  if (!tool || tool.unsupported) throw new Error(tool?.unsupportedReason ?? '此工具不支持一键安装。')
200
238
  const method = pickMethod(tool, methodId, preferences, platform)
201
239
  if (!method) throw new Error(unsupportedMethodError(tool))
@@ -207,7 +245,16 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
207
245
  const installDir = preferences?.installDir || ''
208
246
  if (method.kind === 'script') {
209
247
  const scriptUrl = preferences?.scriptUrls?.[tool.id] || method.scriptUrl
210
- const target = methodHonoursInstallDir(method) ? scriptInstallDir(tool, preferences, { home }) : ''
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)) {
256
+ notices.push('MiniMax 官方脚本只接受 https://registry.npmjs.org/ 或 https://registry.npmmirror.com/,本次不改写它的 npm 源。')
257
+ }
211
258
  return {
212
259
  toolId: tool.id,
213
260
  method,
@@ -216,13 +263,13 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
216
263
  label: method.label,
217
264
  shell: method.shell,
218
265
  scriptUrl,
266
+ downloadUrls: preferences?.scriptUrls?.[tool.id] ? [scriptUrl] : [...new Set([scriptUrl, ...(method.scriptFallbackUrls ?? [])])],
219
267
  verify: method.verify,
220
268
  installArg: method.installArgs,
221
269
  installDir: target,
222
- env: scriptEnv(method, { installDir: target, scriptUrl }),
223
- notices: target ? [] : installDir
224
- ? [`${method.label} 无法使用自定义安装目录(官方脚本写死了安装路径),将安装到 ${vendorScriptDir(tool, { home })}。`]
225
- : [],
270
+ binDir,
271
+ env: scriptEnv(method, { installDir: target, scriptUrl, registry: preferences.registry }),
272
+ notices,
226
273
  }
227
274
  }
228
275
  return {
@@ -232,8 +279,9 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
232
279
  kind: 'package-manager',
233
280
  label: method.label,
234
281
  manager: method.manager,
235
- args: packageManagerArgs(method, preferences, { installDir }),
282
+ args: packageManagerArgs(method, preferences, { installDir, platform }),
236
283
  installDir,
284
+ binDir: packageManagerBinDir(installDir, { platform }),
237
285
  env: {},
238
286
  notices: [],
239
287
  }
@@ -248,6 +296,7 @@ export function resolveUninstallPlan(tool, methodId, preferences = {}, options =
248
296
  : `${tool?.label ?? '此工具'} 没有可用的卸载方式。`)
249
297
  const blocked = installMethodBlockReason(method, platform)
250
298
  if (blocked) throw new Error(`${tool.label}:${blocked}`)
299
+ if (method.uninstallUnsupportedReason) throw new Error(method.uninstallUnsupportedReason)
251
300
  if (method.kind === 'script') {
252
301
  return { toolId: tool.id, method, methodId: method.id, kind: 'script', label: method.label, ...resolveInstallLocation(tool, method, preferences, options) }
253
302
  }
@@ -261,28 +310,35 @@ export function resolveUninstallPlan(tool, methodId, preferences = {}, options =
261
310
  args: [
262
311
  ...method.uninstallArgs,
263
312
  ...(preferences?.registry ? [`--registry=${preferences.registry}`] : []),
264
- ...(preferences?.installDir ? ['--prefix', preferences.installDir] : []),
313
+ ...packageManagerDirectoryArgs(method, preferences?.installDir || '', { platform }),
265
314
  ],
266
315
  installDir: preferences?.installDir || '',
316
+ binDir: packageManagerBinDir(preferences?.installDir || '', { platform }),
267
317
  }
268
318
  }
269
319
 
270
320
  /** Where the tool's binary lives, so a later uninstall can find it. */
271
- export function resolveInstallLocation(tool, method, preferences = {}, { home = '' } = {}) {
321
+ export function resolveInstallLocation(tool, method, preferences = {}, { home = '', platform = 'linux', env = {} } = {}) {
272
322
  if (!method) return null
273
323
  if (method.kind === 'script') {
274
324
  // A method that cannot honour the configured directory always lands in the
275
325
  // vendor's own location, whatever the user typed.
276
326
  const dir = methodHonoursInstallDir(method)
277
- ? scriptInstallDir(tool, preferences, { home })
278
- : vendorScriptDir(tool, { home })
279
- return { kind: 'script', dir, binaries: [...(tool.scriptBinaryNames ?? [])] }
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 ?? [])] }
280
331
  }
281
- return { kind: 'package-manager', dir: preferences?.installDir || '', binaries: [] }
332
+ return { kind: 'package-manager', dir: packageManagerBinDir(preferences?.installDir || '', { platform }), installDir: preferences?.installDir || '', binaries: [] }
282
333
  }
283
334
 
284
- function scriptEnv(method, { installDir, scriptUrl }) {
335
+ function scriptEnv(method, { installDir, scriptUrl, registry }) {
285
336
  const env = {}
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
+ }
286
342
  if (installDir && method.env?.includes('STEP_INSTALL_DIR')) env.STEP_INSTALL_DIR = installDir
287
343
  // The step installers read their release base from this variable, so a mirror
288
344
  // URL also moves the binaries and the checksum manifest, not just the script.
@@ -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,5 +1,26 @@
1
1
  # Changelog
2
2
 
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.
14
+
15
+ ## 0.17.1 — 2026-10-11
16
+
17
+ Stable patch under the npm `latest` tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading so the Host reloads. Existing 0.17.0 artifacts remain unchanged.
18
+
19
+ - **MiniMax Windows one-click update accepts the official installer again.** The published `install.ps1` opens with PowerShell comment-based help (`<#`), and the sanity check treated every leading `<` as an HTML page, so the real script was refused before it ran. HTML and XML documents are still refused. UTF-8/UTF-16 BOMs are decoded first. Content-Type is not used: the CDN serves the script as `application/octet-stream`. A download that is empty, HTML, or missing `@minimax-ai/code`, `MCODE_INSTALL_DIR` and `MiniMax Code` still fails with the missing piece named.
20
+ - **Official script installers are selectable.** Claude Code, Codex, MiniMax, Kimi, MiMo and Grok now offer the vendor `curl | bash` / `irm | iex` installer next to npm and pnpm, on the platforms that script supports. Gemini stays on npm/pnpm. ZCode stays the signed desktop installer. One-click install, repair and uninstall run the method shown on the row. MiniMax on Windows defaults to `irm | iex` (that is the copy npm does not update); pinning npm or pnpm runs that manager instead, and still falls back to the official script if the probed version does not advance.
21
+ - **Updates use the configured install directory.** npm/pnpm keep `--prefix`. Scripts that document a directory variable (`MCODE_INSTALL_DIR`, `CODEX_INSTALL_DIR`, `KIMI_INSTALL_DIR`, `GROK_BIN_DIR`, `MIMOCODE_INSTALL_DIR`, Step Code's flag) receive that path. The MiniMax Windows fallback no longer substitutes the npm prefix: a configured directory wins, otherwise an existing official layout is updated in place, otherwise `%USERPROFILE%\.minimax-code`. Scripts that hardcode a path (Claude, OpenCode, MiMo on Unix) say so on the row.
22
+ - Windows validation for this patch: MiniMax `irm | iex` against a configured install directory completed with `mcode.cmd` present and version 0.6.8; the false “not an official Windows install script” refusal did not recur on main tip `1162f276`.
23
+
3
24
  ## 0.17.0 — 2026-10-11
4
25
 
5
26
  Stable minor under the npm `latest` tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading so the Host reloads. Existing 0.16.4 artifacts remain unchanged.
@@ -144,7 +165,7 @@ Prerelease for DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2, published under
144
165
 
145
166
  ### Package renamed
146
167
  - The npm package is now **`@ljwei-stak/dsh-model-router`**. The last version under the old name `@ljwei-stak/model-router-galgame` is 0.13.0, with the same content as `@ljwei-stak/dsh-model-router@0.13.0`; the old name will get no further updates. Upgrade by removing the old plugin and adding the new one; do not install both.
147
- - The profile entry id, panel id, settings namespace and Cordis plugin name stay `model-router-galgame`, and state stays in `~/.dsh/model-router/`, so saved settings and run history carry over. See [MIGRATION.md](MIGRATION.md).
168
+ - The profile entry id, panel id, settings namespace and Cordis plugin name stay `model-router-galgame`, and state stays in `~/.dsh/model-router/`, so saved settings and run history carry over. See [MIGRATION.md](docs/archive/MIGRATION.md).
148
169
 
149
170
  ### Official CLI execution (PR #1)
150
171
  - Assigned work packages run through the official vendor CLIs (Claude Code, Codex, Gemini CLI, Kimi Code, MiniMax Code, MiMo Code, Grok Build) with headless adapters; failures fall back to the Harness model catalog API.
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,11 +16,11 @@ 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
 
21
- [简体中文](README.zh.md) · [Workbench guide (Chinese)](docs/WORKBENCH_USER_GUIDE.zh.md) · [Installation guide (Chinese)](INSTALLATION_GUIDE.zh.md) · [Migration](MIGRATION.md) · [Changelog](CHANGELOG.md)
23
+ [简体中文](README.zh.md) · [Workbench guide (Chinese)](docs/WORKBENCH_USER_GUIDE.zh.md) · [Installation guide (Chinese)](docs/INSTALLATION_GUIDE.zh.md) · [Migration](docs/archive/MIGRATION.md) · [Changelog](CHANGELOG.md)
22
24
 
23
25
  For self-testing, follow the [desktop acceptance and quality–cost experiment steps (Chinese)](docs/ROUTING_SELFTEST_STEPS.zh.md). Start with an 18-question, three-model pilot and an explicit spending limit. Offline helpers do not collect answers or call models; first-stage selection replay is distinct from end-to-end team execution.
24
26
 
@@ -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,18 +48,18 @@ 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
- 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](MIGRATION.md).
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
 
62
64
  ## Use the workbench
63
65
 
@@ -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,11 +16,11 @@
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
 
21
- [English](README.md) · [工作台完整指南](docs/WORKBENCH_USER_GUIDE.zh.md) · [安装与验证](INSTALLATION_GUIDE.zh.md) · [迁移说明](MIGRATION.md) · [更新记录](CHANGELOG.md)
23
+ [English](README.md) · [工作台完整指南](docs/WORKBENCH_USER_GUIDE.zh.md) · [安装与验证](docs/INSTALLATION_GUIDE.zh.md) · [迁移说明](docs/archive/MIGRATION.md) · [更新记录](CHANGELOG.md)
22
24
 
23
25
  需要自行实测时,按[安装验收与质量—成本实验步骤](docs/ROUTING_SELFTEST_STEPS.zh.md)先完成桌面检查和 18 题×3 模型小样,再决定正式样本与费用上限。方案区分首包选模投影和实际团队执行;配套离线入口不采集、不调用模型,真实收益仍待验证。
24
26
 
@@ -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,18 +48,18 @@
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
- 从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](MIGRATION.md)。
62
+ 从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](docs/archive/MIGRATION.md)。
61
63
 
62
64
  ## 工作台的四个页面
63
65
 
@@ -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