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

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.
@@ -77,6 +77,8 @@ function scriptMethod(id2, label, {
77
77
  scriptBinaryNames,
78
78
  registryEnv,
79
79
  installDirSubdir,
80
+ legacySharedInstallDir = false,
81
+ sharedDefaultBin = false,
80
82
  uninstallUnsupportedReason,
81
83
  scriptFallbackUrls
82
84
  }) {
@@ -97,6 +99,8 @@ function scriptMethod(id2, label, {
97
99
  installDirEnv,
98
100
  installDirSuffix,
99
101
  ...installDirSubdir ? { installDirSubdir } : {},
102
+ ...legacySharedInstallDir ? { legacySharedInstallDir: true } : {},
103
+ ...sharedDefaultBin ? { sharedDefaultBin: true } : {},
100
104
  ...uninstallUnsupportedReason ? { uninstallUnsupportedReason } : {},
101
105
  ...defaultScriptInstallDir ? { defaultScriptInstallDir } : {},
102
106
  ...defaultScriptInstallDirEnv ? { defaultScriptInstallDirEnv } : {},
@@ -131,6 +135,8 @@ var OFFICIAL_TOOLS = Object.freeze([
131
135
  supportsInstallDir: true,
132
136
  installDirEnv: "KIMI_INSTALL_DIR",
133
137
  installDirSuffix: "bin",
138
+ installDirSubdir: "kimi-code",
139
+ legacySharedInstallDir: true,
134
140
  env: ["KIMI_INSTALL_DIR"],
135
141
  scriptBinaryNames: ["kimi"],
136
142
  verify: Object.freeze({ mustInclude: ["kimi-code", "code.kimi.com/kimi-code"] })
@@ -142,6 +148,8 @@ var OFFICIAL_TOOLS = Object.freeze([
142
148
  supportsInstallDir: true,
143
149
  installDirEnv: "KIMI_INSTALL_DIR",
144
150
  installDirSuffix: "bin",
151
+ installDirSubdir: "kimi-code",
152
+ legacySharedInstallDir: true,
145
153
  env: ["KIMI_INSTALL_DIR"],
146
154
  scriptBinaryNames: ["kimi.exe"],
147
155
  verify: Object.freeze({ mustInclude: ["kimi-code", "code.kimi.com/kimi-code"] })
@@ -174,6 +182,7 @@ var OFFICIAL_TOOLS = Object.freeze([
174
182
  scriptFallbackUrls: ["https://downloads.claude.ai/claude-code-releases/bootstrap.sh"],
175
183
  platforms: ["linux", "darwin"],
176
184
  scriptBinaryNames: ["claude"],
185
+ sharedDefaultBin: true,
177
186
  verify: Object.freeze({ mustInclude: ["downloads.claude.ai/claude-code-releases", "manifest.json"] })
178
187
  }),
179
188
  scriptMethod("script-powershell", "irm | iex", {
@@ -182,6 +191,7 @@ var OFFICIAL_TOOLS = Object.freeze([
182
191
  scriptFallbackUrls: ["https://downloads.claude.ai/claude-code-releases/bootstrap.ps1"],
183
192
  platforms: ["win32"],
184
193
  scriptBinaryNames: ["claude.exe"],
194
+ sharedDefaultBin: true,
185
195
  verify: Object.freeze({ mustInclude: ["downloads.claude.ai/claude-code-releases", "manifest.json"] })
186
196
  })
187
197
  ])
@@ -210,6 +220,9 @@ var OFFICIAL_TOOLS = Object.freeze([
210
220
  supportsInstallDir: true,
211
221
  installDirEnv: "CODEX_INSTALL_DIR",
212
222
  env: ["CODEX_INSTALL_DIR"],
223
+ installDirSubdir: "codex/bin",
224
+ legacySharedInstallDir: true,
225
+ sharedDefaultBin: true,
213
226
  scriptBinaryNames: ["codex"],
214
227
  verify: Object.freeze({ mustInclude: ["releases.openai.com/codex", "CODEX_INSTALL_DIR"] })
215
228
  }),
@@ -259,6 +272,8 @@ var OFFICIAL_TOOLS = Object.freeze([
259
272
  supportsInstallDir: true,
260
273
  installDirEnv: "MCODE_INSTALL_DIR",
261
274
  installDirSuffix: "bin",
275
+ installDirSubdir: "minimax-code",
276
+ legacySharedInstallDir: true,
262
277
  registryEnv: "MCODE_NPM_REGISTRY",
263
278
  env: ["MCODE_INSTALL_DIR", "MCODE_NPM_REGISTRY"],
264
279
  verify: Object.freeze({ mustInclude: ["MiniMax Code CLI", "@minimax-ai/code"] })
@@ -269,6 +284,8 @@ var OFFICIAL_TOOLS = Object.freeze([
269
284
  platforms: ["win32"],
270
285
  supportsInstallDir: true,
271
286
  installDirEnv: "MCODE_INSTALL_DIR",
287
+ installDirSubdir: "minimax-code",
288
+ legacySharedInstallDir: true,
272
289
  registryEnv: "MCODE_NPM_REGISTRY",
273
290
  env: ["MCODE_INSTALL_DIR", "MCODE_NPM_REGISTRY"],
274
291
  defaultScriptInstallDir: ".minimax-code",
@@ -307,6 +324,8 @@ var OFFICIAL_TOOLS = Object.freeze([
307
324
  supportsInstallDir: true,
308
325
  installDirEnv: "MIMOCODE_INSTALL_DIR",
309
326
  env: ["MIMOCODE_INSTALL_DIR"],
327
+ installDirSubdir: "mimo-code",
328
+ legacySharedInstallDir: true,
310
329
  scriptBinaryNames: ["mimo.exe"],
311
330
  verify: Object.freeze({ mustInclude: ["mimo.xiaomi.com", ".mimocode"] })
312
331
  })
@@ -336,6 +355,8 @@ var OFFICIAL_TOOLS = Object.freeze([
336
355
  supportsInstallDir: true,
337
356
  installDirEnv: "GROK_BIN_DIR",
338
357
  env: ["GROK_BIN_DIR"],
358
+ installDirSubdir: "grok-build",
359
+ legacySharedInstallDir: true,
339
360
  scriptBinaryNames: ["grok", "agent"],
340
361
  verify: Object.freeze({ mustInclude: ["Grok CLI installer", "x.ai/cli"] })
341
362
  }),
@@ -346,6 +367,8 @@ var OFFICIAL_TOOLS = Object.freeze([
346
367
  supportsInstallDir: true,
347
368
  installDirEnv: "GROK_BIN_DIR",
348
369
  env: ["GROK_BIN_DIR"],
370
+ installDirSubdir: "grok-build",
371
+ legacySharedInstallDir: true,
349
372
  scriptBinaryNames: ["grok.exe", "agent.exe", "grove.exe", "grove-fsmonitor.exe", "grove-credential.exe"],
350
373
  verify: Object.freeze({ mustInclude: ["Grok CLI installer", "x.ai/cli"] })
351
374
  })
@@ -428,6 +451,10 @@ var OFFICIAL_TOOLS = Object.freeze([
428
451
  installArg: ["--install-dir"],
429
452
  env: ["STEP_INSTALL_DIR", "STEP_RELEASE_BASE_URL"],
430
453
  supportsInstallDir: true,
454
+ // The installer replaces its runtime directories, including the entire
455
+ // node_modules tree on Windows. It must own a dedicated directory.
456
+ installDirSubdir: "stepcode",
457
+ legacySharedInstallDir: true,
431
458
  verify: Object.freeze({
432
459
  mustInclude: ["static-openapi.stepfun.com", "stepcode installer"],
433
460
  mustNotInclude: ["anomalyco/opencode"]
@@ -440,6 +467,8 @@ var OFFICIAL_TOOLS = Object.freeze([
440
467
  installArg: ["-InstallDir"],
441
468
  env: ["STEP_INSTALL_DIR", "STEP_RELEASE_BASE_URL"],
442
469
  supportsInstallDir: true,
470
+ installDirSubdir: "stepcode",
471
+ legacySharedInstallDir: true,
443
472
  verify: Object.freeze({
444
473
  mustInclude: ["static-openapi.stepfun.com", "stepcode"],
445
474
  mustNotInclude: ["anomalyco/opencode"]
@@ -12704,7 +12733,7 @@ var xterm_default = `/**
12704
12733
  `;
12705
12734
 
12706
12735
  // .dsh-plugin/client/host-version.mjs
12707
- var ROUTER_CLIENT_VERSION = true ? "0.18.1" : "";
12736
+ var ROUTER_CLIENT_VERSION = true ? "0.18.2" : "";
12708
12737
  var STALE_HOST_MESSAGE = "\u63D2\u4EF6\u540E\u53F0\u7248\u672C\u8F83\u65E7\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u542F Harness\uFF08\u5305\u62EC\u6258\u76D8\u56FE\u6807\uFF09\u540E\u518D\u4F7F\u7528\u3002";
12709
12738
  function isMissingRemoteMethod(message) {
12710
12739
  const value = String(message ?? "");
@@ -13537,10 +13566,10 @@ function probeLabel(probe) {
13537
13566
  var INSTALL_SETTING_FIELDS = [
13538
13567
  {
13539
13568
  key: "toolInstallDir",
13540
- label: "\u7EDF\u4E00\u5B89\u88C5\u76EE\u5F55",
13569
+ label: "\u7EDF\u4E00\u5B89\u88C5\u6839\u76EE\u5F55",
13541
13570
  fallback: "",
13542
13571
  placeholder: "\u7559\u7A7A = \u5404\u5382\u5546\u9ED8\u8BA4\u76EE\u5F55",
13543
- hint: "\u586B\u5199\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u4F8B\u5982 D:/AI/tools\u3002\u5404\u65B9\u5F0F\u7684\u5B9E\u9645\u76EE\u5F55\u4F1A\u663E\u793A\u5728\u5DE5\u5177\u547D\u4EE4\u65C1\uFF1B\u4E0D\u652F\u6301\u7EDF\u4E00\u76EE\u5F55\u7684\u65B9\u5F0F\u4F1A\u8BF4\u660E\u539F\u56E0\u3002"
13572
+ hint: "\u4F8B\u5982 D:/AI/tools\u3002npm / pnpm \u4F7F\u7528\u8BE5\u6839\uFF1B\u5382\u5546\u811A\u672C\u4F7F\u7528\u5DE5\u5177\u4E13\u5C5E\u5B50\u76EE\u5F55\uFF0C\u5B9E\u9645\u5730\u5740\u89C1\u5DE5\u5177\u884C\u3002\u56FA\u5B9A\u5382\u5546\u5730\u5740\u7684\u65B9\u5F0F\u4F1A\u8BF4\u660E\u539F\u56E0\u3002"
13544
13573
  },
13545
13574
  {
13546
13575
  key: "toolNpmRegistry",
@@ -14876,8 +14905,8 @@ function RouterSettingsCard(props) {
14876
14905
  import_dsh_client_ui_primitives.SettingsValueField,
14877
14906
  {
14878
14907
  id: "model-router-tool-install-dir",
14879
- label: "\u7EDF\u4E00\u5B89\u88C5\u76EE\u5F55",
14880
- hint: "\u586B\u5199\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u7559\u7A7A\u4F7F\u7528\u5382\u5546\u9ED8\u8BA4\u4F4D\u7F6E\u3002npm / pnpm \u4F7F\u7528\u8BE5\u76EE\u5F55\u5B89\u88C5\uFF1B\u5382\u5546\u811A\u672C\u4E0D\u652F\u6301\u81EA\u5B9A\u4E49\u76EE\u5F55\u65F6\uFF0C\u5B98\u65B9\u5DE5\u5177\u9875\u4F1A\u663E\u793A\u5B9E\u9645\u76EE\u5F55\u4E0E\u539F\u56E0\u3002\u66F4\u65B0\u4F7F\u7528\u540C\u4E00\u5B89\u88C5\u65B9\u5F0F\u3001\u76EE\u5F55\u4E0E\u4E0B\u8F7D\u6E90\u3002",
14908
+ label: "\u7EDF\u4E00\u5B89\u88C5\u6839\u76EE\u5F55",
14909
+ hint: "\u586B\u5199\u7EDD\u5BF9\u8DEF\u5F84\uFF0C\u7559\u7A7A\u4F7F\u7528\u5382\u5546\u9ED8\u8BA4\u4F4D\u7F6E\u3002npm / pnpm \u5171\u7528\u8BE5\u6839\uFF1B\u53EF\u914D\u7F6E\u5382\u5546\u811A\u672C\u4F7F\u7528\u5DE5\u5177\u4E13\u5C5E\u5B50\u76EE\u5F55\uFF0C\u5B9E\u9645\u4F4D\u7F6E\u89C1\u5B98\u65B9\u5DE5\u5177\u9875\u3002\u66F4\u65B0\u590D\u7528\u6240\u9009\u65B9\u5F0F\u3001\u76EE\u5F55\u4E0E\u4E0B\u8F7D\u6E90\u3002",
14881
14910
  disabled,
14882
14911
  ...state.toolInstallDir,
14883
14912
  overriddenLabel: FIELD_COPY.overridden,
@@ -345,7 +345,7 @@ function nodeVersion(entry, pattern = /^v(\d+\.\d+\.\d+)/) {
345
345
  })
346
346
  }
347
347
 
348
- async function findNodeExecutable(workspace, minimum = [22, 19, 0], acceptedVersion = () => true) {
348
+ export async function findNodeExecutable(workspace, minimum = [22, 19, 0], acceptedVersion = () => true) {
349
349
  await ensureNpmPrefixOnPath()
350
350
  const candidates = [process.execPath, ...pathDirectories().map(directory => join(directory, IS_WINDOWS ? 'node.exe' : 'node'))]
351
351
  for (const candidate of candidates) {
@@ -676,17 +676,17 @@ export function harnessSandboxBlocker(toolId, platform = process.platform) {
676
676
  /** Check the actual trusted launch entry without starting an account call. */
677
677
  export async function officialToolReadiness(toolId, workspace = process.cwd(), selectionOptions = {}) {
678
678
  const tool = getOfficialTool(toolId)
679
- if (!tool) return { id: String(toolId), ready: false, reason: '工具不在固定官方注册表中。' }
679
+ if (!tool) return { id: String(toolId), supported: false, ready: false, reason: '工具不在固定官方注册表中。' }
680
680
  const capability = CAPABILITIES[tool.id]
681
- if (!capability?.supported) return { id: tool.id, ready: false, reason: capability?.reason ?? '当前平台不支持托管执行。' }
681
+ if (!capability?.supported) return { id: tool.id, supported: false, ready: false, reason: capability?.reason ?? '当前平台不支持托管执行。' }
682
682
  try {
683
683
  const directory = await checkedWorkspace(workspace)
684
684
  const spec = await launchSpec(tool.id, directory, capability.modes[0], null, undefined, selectionOptions)
685
685
  return spec.unsupported
686
- ? { id: tool.id, ready: false, reason: spec.unsupported }
687
- : { id: tool.id, ready: true, source: spec.source, modes: capability.modes }
686
+ ? { id: tool.id, supported: true, ready: false, reason: spec.unsupported }
687
+ : { id: tool.id, supported: true, ready: true, source: spec.source, modes: capability.modes }
688
688
  } catch (error) {
689
- return { id: tool.id, ready: false, reason: String(error?.message ?? error).slice(0, 300) }
689
+ return { id: tool.id, supported: true, ready: false, reason: String(error?.message ?? error).slice(0, 300) }
690
690
  }
691
691
  }
692
692
 
@@ -45,7 +45,7 @@ function scriptMethod(id, label, {
45
45
  shell, scriptUrl, verify, installArg = [], env = [], platforms,
46
46
  supportsInstallDir = false, installDirEnv = '', installDirSuffix = '',
47
47
  defaultScriptInstallDir, defaultScriptInstallDirEnv, scriptBinaryNames,
48
- registryEnv, installDirSubdir, uninstallUnsupportedReason, scriptFallbackUrls,
48
+ registryEnv, installDirSubdir, legacySharedInstallDir = false, sharedDefaultBin = false, uninstallUnsupportedReason, scriptFallbackUrls,
49
49
  }) {
50
50
  return Object.freeze({
51
51
  id,
@@ -64,6 +64,8 @@ function scriptMethod(id, label, {
64
64
  installDirEnv,
65
65
  installDirSuffix,
66
66
  ...(installDirSubdir ? { installDirSubdir } : {}),
67
+ ...(legacySharedInstallDir ? { legacySharedInstallDir: true } : {}),
68
+ ...(sharedDefaultBin ? { sharedDefaultBin: true } : {}),
67
69
  ...(uninstallUnsupportedReason ? { uninstallUnsupportedReason } : {}),
68
70
  ...(defaultScriptInstallDir ? { defaultScriptInstallDir } : {}),
69
71
  ...(defaultScriptInstallDirEnv ? { defaultScriptInstallDirEnv } : {}),
@@ -96,6 +98,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
96
98
  shell: 'bash', scriptUrl: 'https://code.kimi.com/kimi-code/install.sh',
97
99
  platforms: ['linux', 'darwin'], supportsInstallDir: true,
98
100
  installDirEnv: 'KIMI_INSTALL_DIR', installDirSuffix: 'bin',
101
+ installDirSubdir: 'kimi-code', legacySharedInstallDir: true,
99
102
  env: ['KIMI_INSTALL_DIR'],
100
103
  scriptBinaryNames: ['kimi'],
101
104
  verify: Object.freeze({ mustInclude: ['kimi-code', 'code.kimi.com/kimi-code'] }),
@@ -104,6 +107,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
104
107
  shell: 'powershell', scriptUrl: 'https://code.kimi.com/kimi-code/install.ps1',
105
108
  platforms: ['win32'], supportsInstallDir: true,
106
109
  installDirEnv: 'KIMI_INSTALL_DIR', installDirSuffix: 'bin',
110
+ installDirSubdir: 'kimi-code', legacySharedInstallDir: true,
107
111
  env: ['KIMI_INSTALL_DIR'],
108
112
  scriptBinaryNames: ['kimi.exe'],
109
113
  verify: Object.freeze({ mustInclude: ['kimi-code', 'code.kimi.com/kimi-code'] }),
@@ -135,6 +139,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
135
139
  scriptFallbackUrls: ['https://downloads.claude.ai/claude-code-releases/bootstrap.sh'],
136
140
  platforms: ['linux', 'darwin'],
137
141
  scriptBinaryNames: ['claude'],
142
+ sharedDefaultBin: true,
138
143
  verify: Object.freeze({ mustInclude: ['downloads.claude.ai/claude-code-releases', 'manifest.json'] }),
139
144
  }),
140
145
  scriptMethod('script-powershell', 'irm | iex', {
@@ -142,6 +147,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
142
147
  scriptFallbackUrls: ['https://downloads.claude.ai/claude-code-releases/bootstrap.ps1'],
143
148
  platforms: ['win32'],
144
149
  scriptBinaryNames: ['claude.exe'],
150
+ sharedDefaultBin: true,
145
151
  verify: Object.freeze({ mustInclude: ['downloads.claude.ai/claude-code-releases', 'manifest.json'] }),
146
152
  }),
147
153
  ]),
@@ -167,6 +173,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
167
173
  shell: 'sh', scriptUrl: 'https://chatgpt.com/codex/install.sh',
168
174
  platforms: ['linux', 'darwin'], supportsInstallDir: true,
169
175
  installDirEnv: 'CODEX_INSTALL_DIR', env: ['CODEX_INSTALL_DIR'],
176
+ installDirSubdir: 'codex/bin', legacySharedInstallDir: true,
177
+ sharedDefaultBin: true,
170
178
  scriptBinaryNames: ['codex'],
171
179
  verify: Object.freeze({ mustInclude: ['releases.openai.com/codex', 'CODEX_INSTALL_DIR'] }),
172
180
  }),
@@ -210,6 +218,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
210
218
  shell: 'bash', scriptUrl: 'https://filecdn.minimax.chat/public/install.sh',
211
219
  platforms: ['linux', 'darwin'], supportsInstallDir: true,
212
220
  installDirEnv: 'MCODE_INSTALL_DIR', installDirSuffix: 'bin',
221
+ installDirSubdir: 'minimax-code', legacySharedInstallDir: true,
213
222
  registryEnv: 'MCODE_NPM_REGISTRY', env: ['MCODE_INSTALL_DIR', 'MCODE_NPM_REGISTRY'],
214
223
  verify: Object.freeze({ mustInclude: ['MiniMax Code CLI', '@minimax-ai/code'] }),
215
224
  }),
@@ -217,6 +226,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
217
226
  shell: 'powershell', scriptUrl: 'https://filecdn.minimax.chat/public/install.ps1',
218
227
  platforms: ['win32'], supportsInstallDir: true,
219
228
  installDirEnv: 'MCODE_INSTALL_DIR',
229
+ installDirSubdir: 'minimax-code', legacySharedInstallDir: true,
220
230
  registryEnv: 'MCODE_NPM_REGISTRY', env: ['MCODE_INSTALL_DIR', 'MCODE_NPM_REGISTRY'],
221
231
  defaultScriptInstallDir: '.minimax-code',
222
232
  scriptBinaryNames: ['mcode.cmd', 'mcode.ps1', 'mcode-tools.cmd', 'mcode-tools.ps1'],
@@ -250,6 +260,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
250
260
  shell: 'powershell', scriptUrl: 'https://mimo.xiaomi.com/install.ps1',
251
261
  platforms: ['win32'], supportsInstallDir: true,
252
262
  installDirEnv: 'MIMOCODE_INSTALL_DIR', env: ['MIMOCODE_INSTALL_DIR'],
263
+ installDirSubdir: 'mimo-code', legacySharedInstallDir: true,
253
264
  scriptBinaryNames: ['mimo.exe'],
254
265
  verify: Object.freeze({ mustInclude: ['mimo.xiaomi.com', '.mimocode'] }),
255
266
  }),
@@ -276,6 +287,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
276
287
  shell: 'bash', scriptUrl: 'https://x.ai/cli/install.sh',
277
288
  platforms: ['linux', 'darwin'], supportsInstallDir: true,
278
289
  installDirEnv: 'GROK_BIN_DIR', env: ['GROK_BIN_DIR'],
290
+ installDirSubdir: 'grok-build', legacySharedInstallDir: true,
279
291
  scriptBinaryNames: ['grok', 'agent'],
280
292
  verify: Object.freeze({ mustInclude: ['Grok CLI installer', 'x.ai/cli'] }),
281
293
  }),
@@ -283,6 +295,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
283
295
  shell: 'powershell', scriptUrl: 'https://x.ai/cli/install.ps1',
284
296
  platforms: ['win32'], supportsInstallDir: true,
285
297
  installDirEnv: 'GROK_BIN_DIR', env: ['GROK_BIN_DIR'],
298
+ installDirSubdir: 'grok-build', legacySharedInstallDir: true,
286
299
  scriptBinaryNames: ['grok.exe', 'agent.exe', 'grove.exe', 'grove-fsmonitor.exe', 'grove-credential.exe'],
287
300
  verify: Object.freeze({ mustInclude: ['Grok CLI installer', 'x.ai/cli'] }),
288
301
  }),
@@ -365,6 +378,9 @@ export const OFFICIAL_TOOLS = Object.freeze([
365
378
  installArg: ['--install-dir'],
366
379
  env: ['STEP_INSTALL_DIR', 'STEP_RELEASE_BASE_URL'],
367
380
  supportsInstallDir: true,
381
+ // The installer replaces its runtime directories, including the entire
382
+ // node_modules tree on Windows. It must own a dedicated directory.
383
+ installDirSubdir: 'stepcode', legacySharedInstallDir: true,
368
384
  verify: Object.freeze({
369
385
  mustInclude: ['static-openapi.stepfun.com', 'stepcode installer'],
370
386
  mustNotInclude: ['anomalyco/opencode'],
@@ -377,6 +393,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
377
393
  installArg: ['-InstallDir'],
378
394
  env: ['STEP_INSTALL_DIR', 'STEP_RELEASE_BASE_URL'],
379
395
  supportsInstallDir: true,
396
+ installDirSubdir: 'stepcode', legacySharedInstallDir: true,
380
397
  verify: Object.freeze({
381
398
  mustInclude: ['static-openapi.stepfun.com', 'stepcode'],
382
399
  mustNotInclude: ['anomalyco/opencode'],
@@ -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'
@@ -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 || existingRoot || join(home, '.minimax-code')
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, installDir,
321
+ const executionPreferences = { ...selectedPreferences,
319
322
  methods: { ...selectedPreferences.methods, 'minimax-code': 'script-powershell' } }
320
- return { plan: resolveInstallPlan(getOfficialTool('minimax-code'), 'script-powershell', executionPreferences, options), executionPreferences }
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,
@@ -827,6 +927,22 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
827
927
  bannerLine: banner.split('\n').find(Boolean)?.slice(0, 200) ?? '' }
828
928
  }
829
929
  }
930
+ const legacy = resolveLegacyScriptLocation(tool, selectedPlan.method, selectedPreferences, currentInstallOptions())
931
+ if (legacy) {
932
+ for (const executable of tool.probeExecutables) {
933
+ const names = IS_WINDOWS ? [`${executable}.exe`, `${executable}.cmd`, executable] : [executable]
934
+ for (const name of names) {
935
+ const target = join(legacy.dir, name)
936
+ if (!(await fileExists(target))) continue
937
+ // Discovery is read-only. The old root can never satisfy a new job's
938
+ // target probe or become a script update/uninstall destination.
939
+ return { id: tool.id, installed: false, version: null, status: 'legacy-install',
940
+ installDir: selectedPlan.installDir, binDir: selectedPlan.binDir,
941
+ legacyExecutablePath: target,
942
+ detail: `发现旧共享目录中的 ${tool.label}:${target}。请安装到独立目录 ${selectedPlan.installDir};旧文件会保留,不能用旧目录覆盖其他工具。` }
943
+ }
944
+ }
945
+ }
830
946
  return { id: tool.id, installed: false, version: null, status: 'not-installed',
831
947
  installDir: selectedPlan.installDir, binDir: selectedPlan.binDir,
832
948
  detail: `未在所选安装目录 ${selectedPlan.binDir} 中找到官方命令。` }
@@ -1006,6 +1122,7 @@ function jobView(job) {
1006
1122
  exitCode: job.exitCode,
1007
1123
  timedOut: job.timedOut,
1008
1124
  error: job.error,
1125
+ executionReadiness: job.executionReadiness ?? null,
1009
1126
  outputTail: [...job.outputTail],
1010
1127
  outputTruncated: job.outputTruncated,
1011
1128
  postInstallProbe: job.postInstallProbe,
@@ -1272,8 +1389,10 @@ export function installEnvironment(plan, base = process.env) {
1272
1389
  return env
1273
1390
  }
1274
1391
 
1275
- /** Mark the job succeeded only when the fresh probe is installed, ready, and not still older. */
1276
- async function finishVerifiedInstall(job, probe) {
1392
+ /** Installation and version must pass; verify execution only where an adapter supports it. */
1393
+ export async function finishVerifiedInstall(job, probe, {
1394
+ lookupLatest = tool => latestVersions.lookup(tool), readinessFor = trustedExecutionReadiness,
1395
+ } = {}) {
1277
1396
  job.postInstallProbe = probe
1278
1397
  if (job.cancelRequested) { markCancelled(job); return true }
1279
1398
  if (!probe.installed) {
@@ -1282,14 +1401,15 @@ async function finishVerifiedInstall(job, probe) {
1282
1401
  job.error = `安装命令已完成,但重探测失败:${probe.detail}`
1283
1402
  return true
1284
1403
  }
1285
- const latest = await latestVersions.lookup(job.tool).catch(() => null)
1404
+ const latest = await lookupLatest(job.tool).catch(() => null)
1286
1405
  if (latest?.version && versionStillOlder(probe.version, latest.version)) {
1287
1406
  failBecauseStillOlder(job, probe, latest.version)
1288
1407
  return true
1289
1408
  }
1290
- const readiness = await trustedExecutionReadiness(job.tool.id, { preferences: job.preferences, plan: job.plan })
1409
+ const readiness = await readinessFor(job.tool.id, { preferences: job.preferences, plan: job.plan })
1410
+ job.executionReadiness = readiness
1291
1411
  if (job.cancelRequested) { markCancelled(job); return true }
1292
- if (!readiness.ready) {
1412
+ if (!readiness.ready && readiness.supported !== false) {
1293
1413
  if (IS_WINDOWS && job.tool.id === 'minimax-code' && !job.configured) {
1294
1414
  await runOfficialMiniMaxInstaller(job)
1295
1415
  return true
@@ -1300,8 +1420,11 @@ async function finishVerifiedInstall(job, probe) {
1300
1420
  return true
1301
1421
  }
1302
1422
  job.status = 'succeeded'
1423
+ job.error = null
1303
1424
  job.finishedAt = new Date().toISOString()
1304
- pushLine(job, `— 已安装 ${job.tool.label} ${probe.version ?? ''},官方执行入口已核验(新版本未经插件测试)—`)
1425
+ pushLine(job, readiness.supported === false
1426
+ ? `— 已安装 ${job.tool.label} ${probe.version ?? ''},版本探测已通过;受限执行未启用:${readiness.reason} —`
1427
+ : `— 已安装 ${job.tool.label} ${probe.version ?? ''},官方执行入口已核验(新版本未经插件测试)—`)
1305
1428
  return true
1306
1429
  }
1307
1430
 
@@ -1378,11 +1501,13 @@ async function runInstallJob(installPlan, job) {
1378
1501
  // exactly the case the user pressed 修复 for.
1379
1502
  if (job.repair) {
1380
1503
  pushLine(job, `— ${job.tool.label} ${before.version} 已是最新版本,按修复流程重新安装 —`)
1381
- } else if (readiness.ready || !CAPABILITY_REPAIRABLE.has(job.tool.id)) {
1504
+ } else if (readiness.ready || readiness.supported === false || !CAPABILITY_REPAIRABLE.has(job.tool.id)) {
1382
1505
  job.status = 'succeeded'
1383
1506
  job.finishedAt = new Date().toISOString()
1384
1507
  job.postInstallProbe = before
1508
+ job.executionReadiness = readiness
1385
1509
  pushLine(job, `— ${job.tool.label} ${before.version} 已是最新版本,无需重复安装 —`)
1510
+ if (readiness.supported === false) pushLine(job, `— 受限执行未启用:${readiness.reason} —`)
1386
1511
  return
1387
1512
  } else {
1388
1513
  pushLine(job, `— 已是最新版本 ${before.version},但执行入口未就绪:${readiness.reason};重新安装修复 —`)
@@ -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
- const probe = await captureProcess(spawnImpl, adapter.executable, ['--version'], {
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, adapter.executable, args, {
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
  })
@@ -249,12 +249,17 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
249
249
  const binDir = target
250
250
  ? (method.installDirSuffix ? joinPath(target, method.installDirSuffix) : target)
251
251
  : vendorScriptDir(tool, { home, env, method })
252
- const notices = target ? [] : installDir
252
+ const notices = target && installDir && method.installDirSubdir
253
+ ? [`官方脚本在工具专属子目录 ${target} 维护资源,保护统一安装根中的其他工具。`]
254
+ : target ? [] : installDir
253
255
  ? [`${method.label} 无法使用自定义安装目录(官方脚本写死了安装路径),将安装到 ${vendorScriptDir(tool, { home, env, method })}。`]
254
256
  : []
255
257
  if (method.registryEnv === 'MCODE_NPM_REGISTRY' && preferences.registry && !minimaxScriptRegistry(preferences.registry)) {
256
258
  notices.push('MiniMax 官方脚本只接受 https://registry.npmjs.org/ 或 https://registry.npmmirror.com/,本次不改写它的 npm 源。')
257
259
  }
260
+ if (tool.id === 'codex' && method.id === 'script-powershell') {
261
+ notices.push('Codex Windows 脚本将命令目录联接到用户 Codex 资源目录;统一根控制命令位置,运行资源仍由厂商保存在用户目录。需要所有文件位于统一根时请选 npm/pnpm。')
262
+ }
258
263
  return {
259
264
  toolId: tool.id,
260
265
  method,
@@ -332,6 +337,20 @@ export function resolveInstallLocation(tool, method, preferences = {}, { home =
332
337
  return { kind: 'package-manager', dir: packageManagerBinDir(preferences?.installDir || '', { platform }), installDir: preferences?.installDir || '', binaries: [] }
333
338
  }
334
339
 
340
+ /**
341
+ * Read-only compatibility location for scripts installed directly into a
342
+ * configured shared root before dedicated script directories were introduced.
343
+ * Never use this result as an install/update target: those installers can
344
+ * replace a whole runtime tree belonging to another tool.
345
+ */
346
+ export function resolveLegacyScriptLocation(tool, method, preferences = {}, options = {}) {
347
+ if (!preferences.installDir || !method?.legacySharedInstallDir || !methodHonoursInstallDir(method)) return null
348
+ return {
349
+ ...resolveInstallLocation(tool, { ...method, installDirSubdir: undefined }, preferences, options),
350
+ legacySharedInstallDir: true,
351
+ }
352
+ }
353
+
335
354
  function scriptEnv(method, { installDir, scriptUrl, registry }) {
336
355
  const env = {}
337
356
  if (installDir && method.installDirEnv) env[method.installDirEnv] = installDir
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.2 — 2026-10-12
4
+
5
+ Stable patch under the npm latest tag. Upgrade and fully restart Harness before using Step install, repair or update.
6
+
7
+ - Treat the configured install directory as a shared base. Configurable vendor scripts use dedicated tool subdirectories; npm/pnpm retain their existing shared prefix. Step's official installer replaces its entire runtime directories, so passing the shared root could delete every other npm package there.
8
+ - Check script targets before download or execution: refuse shared roots, other tool/project manifests and commands, package stores, and junctions into another tool's tree. Codex's junction within its own release tree and intentional shared default launcher directories remain supported. Legacy script roots are discovered read-only and cannot satisfy or redirect new install jobs.
9
+ - Separate successful version/install verification from unsupported restricted execution capabilities. Gemini, OpenCode and Step can be installed successfully while retaining explicit execution limitations; supported tools still require their original trusted-entry checks.
10
+ - Resolve Windows Gemini through its selected npm/pnpm package bin and a verified Node executable. Check package-tree containment and official registry file digests before launch, and preserve API fallback when the selected copy is missing or fails verification.
11
+ - Add regressions for destructive vendor runtime replacement, shared-prefix sentinels, junctions, legacy discovery, install capability status and Windows Gemini entry selection. Release notes record validation and the original-version recovery performed on the reported machine.
12
+
3
13
  ## 0.18.1 — 2026-10-12
4
14
 
5
15
  Stable patch under the npm latest tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading.
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness Desktop
2
2
 
3
- Version: **0.18.1**. 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.2**. 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.2:** isolates configurable vendor scripts beneath the shared install root, preventing Step from replacing other tools’ npm packages. Gemini/OpenCode installation success is separate from restricted execution support; Windows Gemini uses its verified package entry through Node. Upgrade and restart Harness before updating Step.
8
+
7
9
  **0.18.1:** fixes install-method selection stuck on saving. Saved volatile settings now update Host install preferences immediately; command previews refresh separately from tool inspection.
8
10
 
9
11
  **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.
@@ -18,7 +20,7 @@ A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe
18
20
 
19
21
  **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.
20
22
 
21
- Current version: **0.18.1** (`@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.
23
+ Current version: **0.18.2** (`@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.
22
24
 
23
25
  **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.
24
26
 
@@ -50,16 +52,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
50
52
  Open **Plugins → Add plugin** and enter:
51
53
 
52
54
  ```text
53
- @ljwei-stak/dsh-model-router@0.18.1
55
+ @ljwei-stak/dsh-model-router@0.18.2
54
56
  ```
55
57
 
56
- 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.1** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
58
+ 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.2** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
57
59
 
58
60
  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.
59
61
 
60
62
  ### Upgrade from the old package name
61
63
 
62
- 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.1`. 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.
64
+ 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.2`. 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.
63
65
 
64
66
  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).
65
67
 
package/README.zh.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness 模型路由插件
2
2
 
3
- 版本:**0.18.1**。选模求解器概览见[路由研究说明](docs/ROUTING_RESEARCH.zh.md);公式、条件证明与实验见[数学推导与实验记录](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md)。[LiveBench 最新证据与验证协议](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md)已核查 2026-06-25 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
3
+ 版本:**0.18.2**。选模求解器概览见[路由研究说明](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.2**:厂商脚本使用统一根目录下的工具专属子目录,修复 Step 安装替换其他 npm 工具包的问题。Gemini/OpenCode 安装成功与受限执行能力分别显示;Windows Gemini 使用核验后的包入口通过 Node 启动。先升级并重启 Harness,再更新 Step。
8
+
7
9
  **0.18.1**:修复安装方式切换卡在“保存方式中…”的问题。已保存设置立即同步到宿主安装配置,命令预览单独刷新;工具体检延迟时仍能继续切换方式。
8
10
 
9
11
  **0.18.0**:补齐厂商提供的 curl/irm 安装方式,安装与更新复用已保存的目录、方式和下载源,并核验目标目录里的版本。修复 MiniMax 官方 PowerShell 脚本以 `<# ... #>` 帮助注释开头时被误判为 HTML 的问题。统一安装设置改为明确保存后生效。
@@ -18,7 +20,7 @@
18
20
 
19
21
  **0.16.0 新增**:[动态数据与持续反馈](docs/ROUTING_ADAPTIVE_LEARNING.zh.md):可选公开价格/LiveBench快照刷新;按任务类型、时间衰减和低样本收缩调整个人偏好,质量门槛独立保留。设置在“预算与安全”,公开源默认关闭;价格源需提供按官方文档核对的固定USD单价JSON,不是通用官网抓价器。本地DSH_HOME共享、最近200运行窗口,暂无真实收益证明。
20
22
 
21
- 当前版本为 **0.18.1**,包名为 `@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) 已拆成两个独立插件。
23
+ 当前版本为 **0.18.2**,包名为 `@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) 已拆成两个独立插件。
22
24
 
23
25
  **0.15.0 更新**:工作台分为四页,路由建议紧接任务规划展示;执行输入变化时丢弃旧预览;裁剪历史或重试后仍保留费用累计;源码开发新增独立模拟 UI 预览。
24
26
 
@@ -50,16 +52,16 @@
50
52
  打开 **插件 → 添加插件**,填写:
51
53
 
52
54
  ```text
53
- @ljwei-stak/dsh-model-router@0.18.1
55
+ @ljwei-stak/dsh-model-router@0.18.2
54
56
  ```
55
57
 
56
- 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.18.1**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
58
+ 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.18.2**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
57
59
 
58
60
  也可在插件管理器填写本地 `.tgz` 文件的绝对路径,或解压后的内层 `package` 目录;其中应包含 `package.json` 与 `.dsh-plugin`。[历史发布页](https://github.com/Alice-Marx/dsh-model-router/releases)提供归档;已克隆源码的开发方式见下文。
59
61
 
60
62
  ### 从旧包名升级
61
63
 
62
- `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.18.1`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
64
+ `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.18.2`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
63
65
 
64
66
  从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](docs/archive/MIGRATION.md)。
65
67
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  模型路由从 **0.12.0** 起独立发布;GAL 是新的 **0.1.0** 插件。两者均声明支持官方 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**,可分别安装和更新。
4
4
 
5
- 当前路由正式版为 **0.18.1**。本版修复安装方式切换卡在“保存方式中…”及宿主继续使用旧安装设置的问题。方式保存和命令预览不再等待全量工具体检。保留 0.18.0 的官方安装方法、MiniMax 脚本校验和统一目录改进。统一安装设置需点击保存,再选择工具的安装方式。公开数据默认关闭;安装时选择下方精确版本。
5
+ 当前路由正式版为 **0.18.2**。本版修复 Step 安装覆盖共享 npm 包目录的问题:统一目录是安装根,厂商脚本使用工具专属子目录。Gemini 安装状态与受限执行能力分开,Windows 无界面启动使用核验后的包入口。先升级并完全退出 Harness 后重启,再使用 Step 更新/修复。保留 0.18.0 的官方安装方法、MiniMax 脚本校验和统一目录改进。统一安装设置需点击保存,再选择工具的安装方式。公开数据默认关闭;安装时选择下方精确版本。
6
6
 
7
7
  [路由 README](../README.zh.md) · [迁移说明](archive/MIGRATION.md) · [独立 GAL README](https://github.com/Alice-Marx/deepseek-harness-galgame#readme)
8
8
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  | 需求 | 添加插件输入框 | 安装后的入口 |
12
12
  | --- | --- | --- |
13
- | 分析复杂度、按质量和费用选模型、调用官方工具 | `@ljwei-stak/dsh-model-router@0.18.1` | 模型路由 |
13
+ | 分析复杂度、按质量和费用选模型、调用官方工具 | `@ljwei-stak/dsh-model-router@0.18.2` | 模型路由 |
14
14
  | 剧情、立绘、音乐、存档、自由对话 | `@ljwei-stak/dsh-galgame@0.1.0` | Gal 模块 |
15
15
  | 两者都要 | 分别安装上面两个包 | 模型路由 + Gal 模块 |
16
16
 
@@ -24,7 +24,7 @@
24
24
  2. 在“包名、GitHub 仓库地址或本地目录路径”输入框填一个完整包名,例如:
25
25
 
26
26
  ```text
27
- @ljwei-stak/dsh-model-router@0.18.1
27
+ @ljwei-stak/dsh-model-router@0.18.2
28
28
  ```
29
29
 
30
30
  只想安装 GAL 时填写:
@@ -40,7 +40,7 @@
40
40
  ```
41
41
 
42
42
  4. 核对预览的包名、版本和宿主兼容信息,安装并启用。完全退出 Harness(包括托盘进程)再启动,确保 Host 加载新代码。
43
- 5. 检查插件详情与侧边栏:路由应为 **0.18.1 / 模型路由**;GAL 应为 **0.1.0 / Gal 模块**。仅装路由时没有 GAL 入口是预期行为。
43
+ 5. 检查插件详情与侧边栏:路由应为 **0.18.2 / 模型路由**;GAL 应为 **0.1.0 / Gal 模块**。仅装路由时没有 GAL 入口是预期行为。
44
44
 
45
45
  普通用户无需 `npm install -g`,该命令不会把插件注册到当前 Harness profile。
46
46
 
@@ -48,13 +48,13 @@
48
48
 
49
49
  进入对应 Release 下载 `.tgz`:
50
50
 
51
- - [模型路由 Releases](https://github.com/Alice-Marx/dsh-model-router/releases)(也可运行 `npm pack @ljwei-stak/dsh-model-router@0.18.1` 获取同一安装包)
51
+ - [模型路由 Releases](https://github.com/Alice-Marx/dsh-model-router/releases)(也可运行 `npm pack @ljwei-stak/dsh-model-router@0.18.2` 获取同一安装包)
52
52
  - [独立 GAL v0.1.0](https://github.com/Alice-Marx/deepseek-harness-galgame/releases/tag/v0.1.0)
53
53
 
54
54
  在“添加插件”填写下载文件的**绝对路径**,例如:
55
55
 
56
56
  ```text
57
- D:\Plugins\ljwei-stak-dsh-model-router-0.18.1.tgz
57
+ D:\Plugins\ljwei-stak-dsh-model-router-0.18.2.tgz
58
58
  ```
59
59
 
60
60
  ```text
@@ -64,7 +64,7 @@ D:\Plugins\ljwei-stak-dsh-galgame-0.1.0.tgz
64
64
  如附有 `.sha256` 文件,用 PowerShell 计算并比较摘要:
65
65
 
66
66
  ```powershell
67
- Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-model-router-0.18.1.tgz'
67
+ Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-model-router-0.18.2.tgz'
68
68
  Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-galgame-0.1.0.tgz'
69
69
  ```
70
70
 
@@ -74,7 +74,7 @@ Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-galgame-0
74
74
 
75
75
  1. 在旧合并插件的 GAL 播放器导出需要保留的剧情进度,并备份整个 Harness profile。每份 JSON 仅保存**当前剧情状态**,不含所有手动槽、设置、已读记录或本地音乐;重要槽位可依次读档再导出。
76
76
  2. 在**同一个 profile**安装独立 `@ljwei-stak/dsh-galgame@0.1.0`。
77
- 3. 0.13.0 起包名改为 `@ljwei-stak/dsh-model-router`(旧包名最后版本为 0.13.0,内容相同,之后不再更新):在插件管理器卸载原 `@ljwei-stak/model-router-galgame`(保留 profile 与应用数据),再添加 `@ljwei-stak/dsh-model-router@0.18.1`。不要两个同时安装(条目 id 与工具名相同);条目 id 未变,路由设置与执行记录会保留。完成两项更新后再玩剧情,避免同时操作旧合并入口和新入口。
77
+ 3. 0.13.0 起包名改为 `@ljwei-stak/dsh-model-router`(旧包名最后版本为 0.13.0,内容相同,之后不再更新):在插件管理器卸载原 `@ljwei-stak/model-router-galgame`(保留 profile 与应用数据),再添加 `@ljwei-stak/dsh-model-router@0.18.2`。不要两个同时安装(条目 id 与工具名相同);条目 id 未变,路由设置与执行记录会保留。完成两项更新后再玩剧情,避免同时操作旧合并入口和新入口。
78
78
  4. 现在路由与 GAL 各有一个入口。独立 GAL 沿用旧存档 localStorage 键,核对当前进度和三槽;更换 profile 或进度未恢复时,先切换到匹配剧目,再导入 JSON 备份。
79
79
  5. 后续分别更新需要的插件;只用路由可以不启用或不安装 GAL。保留 profile 与应用数据,卸载插件时不要清理故事数据。
80
80
 
@@ -82,7 +82,7 @@ GAL 核心只包含**《回声之城·正篇》**与**《旧城迁移篇:未
82
82
 
83
83
  ## 5. 模型路由第一次使用
84
84
 
85
- 工作台自 0.15.0 起分成**任务与执行、模型配置、官方工具、预算与安全**四页;0.18.1 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
85
+ 工作台自 0.15.0 起分成**任务与执行、模型配置、官方工具、预算与安全**四页;0.18.2 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
86
86
 
87
87
  1. 在 Harness 官方**模型**页配置供应商、模型与凭据。插件读取准确的 `provider/model` 目录,模型档案不保存 API Key。
88
88
  2. 在**模型配置**刷新目录,逐条填写自报质量评分、核对过的输入/输出单价(USD / 百万 token)和擅长方向;缺价留空。可选 `cliModel` 必须是该厂商 CLI 已验证接受的名字。可选 LiveBench 名称须准确对应版本与推理档位;它是用户确认的映射,不自动证明实际执行档位。
@@ -114,7 +114,7 @@ GAL 核心只包含**《回声之城·正篇》**与**《旧城迁移篇:未
114
114
  | 检查 | 预期 |
115
115
  | --- | --- |
116
116
  | 宿主版本 | 0.2.0-rc.1 或 0.2.0-rc.2;包括 `rc` 后缀。 |
117
- | 路由独立安装 | 0.18.1 详情、模型路由入口、目录和模型档案可用;不出现旧合并 GAL 入口。 |
117
+ | 路由独立安装 | 0.18.2 详情、模型路由入口、目录和模型档案可用;不出现旧合并 GAL 入口。 |
118
118
  | GAL 独立安装 | 0.1.0 详情、Gal 模块入口、标题与两部核心剧情可用;不要求安装路由。 |
119
119
  | 共同安装 | 两个入口各一份,路由工具和 GAL 播放器均能使用。 |
120
120
  | 存档迁移 | 同 profile 可读旧键;跨 profile 用匹配剧目 JSON 导入;旧篇目的数据不覆盖。 |
@@ -125,11 +125,11 @@ GAL 核心只包含**《回声之城·正篇》**与**《旧城迁移篇:未
125
125
  | 动态公开数据 | 默认关闭;自行启用后核对快照版本、校验时间及失败/过期状态;人工单价仍优先。 |
126
126
  | 可编辑执行 | 在干净测试仓库核对审批、隔离工作区、补丁整合、厂商实际模型与计费。 |
127
127
 
128
- 历史 0.15.0 的自动测试验证为 220 项通过、3 项 Windows 平台跳过;组件 UI 预览检查使用模拟数据。**0.18.1 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
128
+ 历史 0.15.0 的自动测试验证为 220 项通过、3 项 Windows 平台跳过;组件 UI 预览检查使用模拟数据。**0.18.2 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
129
129
 
130
130
  ## 8. 开发者从完整源码构建
131
131
 
132
- 路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.18.1 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
132
+ 路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.18.2 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
133
133
 
134
134
  ```powershell
135
135
  pnpm install --frozen-lockfile --strict-peer-dependencies
@@ -1,6 +1,6 @@
1
1
  # 模型路由工作台:配置、规划与实际执行
2
2
 
3
- 适用版本:正式版 `@ljwei-stak/dsh-model-router@0.18.1`;声明兼容 DeepSeek Harness Desktop `0.2.0-rc.1 / 0.2.0-rc.2`。安装与开发见[中文 README](../README.zh.md),旧包迁移见[迁移说明](archive/MIGRATION.md)。
3
+ 适用版本:正式版 `@ljwei-stak/dsh-model-router@0.18.2`;声明兼容 DeepSeek Harness Desktop `0.2.0-rc.1 / 0.2.0-rc.2`。安装与开发见[中文 README](../README.zh.md),旧包迁移见[迁移说明](archive/MIGRATION.md)。
4
4
 
5
5
  0.15.0 新增四页布局、规划后直接展示路由建议、执行输入变化时丢弃旧预览,以及历史裁剪和重试后的费用累计保留。源码中的独立 UI 预览使用模拟数据,用于组件检查,不代表真实账号调用或桌面端到端验收。
6
6
 
@@ -113,11 +113,13 @@
113
113
 
114
114
  | 设置 | 作用 | 限制 |
115
115
  | --- | --- | --- |
116
- | 统一安装目录 | 所有一键安装的目标目录 | 必须是绝对路径,不能是磁盘或共享根目录;留空=各厂商默认位置 |
116
+ | 统一安装根目录 | npm/pnpm 使用该根;可配置厂商脚本使用各工具子目录 | 必须是绝对路径,不能是磁盘或共享根目录;留空=各厂商默认位置 |
117
117
  | npm 源地址 | npm / pnpm 方式的下载源 | 必须 https,可填国内镜像 |
118
118
  | 安装脚本源覆盖 | 按工具 ID 指定厂商安装脚本的镜像 | JSON,键是官方工具 ID,值是 https 链接 |
119
119
  | 允许 curl / irm 安装脚本 | 是否允许执行厂商安装脚本 | 关闭后只剩 npm / pnpm |
120
120
 
121
+ 0.18.2 起,以 `D:/AI/tools` 为根时,Step 脚本安装到 `D:/AI/tools/stepcode`,MiniMax 脚本安装到 `D:/AI/tools/minimax-code`,其他可配置脚本同样使用专属子目录。厂商脚本可能替换整个运行资源目录,不能直接交给共享 npm 根。旧共享目录中的文件仅供识别并保留,不用于新脚本更新;可重新安装到独立目录。升级并重启 Host 后再操作 Step。
122
+
121
123
  几个容易踩的点:
122
124
 
123
125
  - **不同工具对"自定义目录"的支持不一样。** npm 使用 `--prefix`;pnpm 使用自身的全局包目录、命令目录和存储目录参数。阶跃的安装脚本接受 `--install-dir`(Windows 上是 `-InstallDir`),也生效。OpenCode 的官方脚本把路径写死成 `~/.opencode/bin`,**无法**接受自定义目录——如果设置里填了目录,它的行内会明确写出将装到哪里。
@@ -132,20 +134,20 @@ MiniMax 官方 Windows 脚本的 `<# ... #>` 是 PowerShell 帮助注释。0.18.
132
134
 
133
135
  | 工具 | npm / pnpm | curl(Linux / macOS) | irm(Windows) | 统一目录支持 |
134
136
  | --- | --- | --- | --- | --- |
135
- | Kimi Code | ✓ | bash | PowerShell | 支持;脚本命令在目录的 bin 子目录 |
137
+ | Kimi Code | ✓ | bash | PowerShell | 脚本使用根目录/kimi-code/bin |
136
138
  | Claude Code | ✓ | bash | PowerShell | 包管理器支持;脚本固定在 ~/.local/bin |
137
139
  | Codex | ✓ | sh | PowerShell | 支持;Windows 脚本使用统一目录/codex/bin,避免把共享目录变成目录联接 |
138
- | MiniMax Code | ✓ | bash | PowerShell | 支持 |
140
+ | MiniMax Code | ✓ | bash | PowerShell | 脚本使用根目录/minimax-code |
139
141
  | MiMo Code | ✓ | bash | PowerShell | 包管理器与 Windows 脚本支持;Unix 脚本固定 ~/.mimocode/bin |
140
- | Grok Build | ✓ | bash | PowerShell | 支持 |
142
+ | Grok Build | ✓ | bash | PowerShell | 脚本使用根目录/grok-build |
141
143
  | Gemini CLI | ✓ | — | — | 支持 |
142
144
  | OpenCode | ✓ | bash | — | 包管理器支持;脚本固定 ~/.opencode/bin |
143
- | Step Code | — | bash | PowerShell | 支持 |
145
+ | Step Code | — | bash | PowerShell | 脚本使用根目录/stepcode |
144
146
  | ZCode | — | — | — | 官方 Windows 桌面安装窗口中选择 |
145
147
 
146
148
  “—”表示当前厂商未提供相应渠道。每个工具的安装方式下拉框保留所有声明方式,并说明本机不适用的原因。MiniMax 官方脚本只接受官方 npm 源或 npmmirror;其他 npm 镜像请选 npm/pnpm 方式。
147
149
 
148
- 脚本安装成功只表示命令和版本已确认。Kimi、MiMo、Grok 的独立原生脚本安装目前没有插件需要的 npm 文件摘要证明,自动任务会显示执行入口未就绪并回退到 API;npm/pnpm 安装保留可核验的执行路径。Windows Codex 脚本使用目录联接,插件不提供通用文件删除卸载;请按官方方式移除原生安装。
150
+ 安装成功只表示所选目录的命令入口和版本已确认,不代表工具支持受限执行。Gemini 使用核验后的 npm/pnpm 包入口无界面执行;OpenCode、Step 当前可安装和在终端中使用,尚不支持受限任务。缺少这类能力不再报安装失败。Kimi、MiMo、Grok 的独立原生脚本安装目前没有插件需要的 npm 文件摘要证明,自动任务会显示执行入口未就绪并回退到 API;npm/pnpm 安装保留可核验的执行路径。Windows Codex 脚本使用目录联接,插件不提供通用文件删除卸载;请按官方方式移除原生安装。
149
151
 
150
152
  ### 安装方式的会话与 Agent 等价物
151
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ljwei-stak/dsh-model-router",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "type": "module",
5
5
  "description": "Model Router for official DeepSeek Harness Desktop: cost-aware task routing, dependent work packages and official vendor CLI discovery, installation and execution. GAL is available separately as @ljwei-stak/dsh-galgame.",
6
6
  "repository": {
@@ -103,7 +103,7 @@
103
103
  "experiment:routing-summary": "node scripts/summarize-routing-search.mjs",
104
104
  "experiment:routing-adaptive": "node scripts/adaptive-routing-study.mjs",
105
105
  "audit:livebench": "node scripts/livebench-aggregate-audit.mjs",
106
- "test": "node --test tests/adaptive-*.test.mjs tests/dynamic-data.test.mjs tests/router.test.mjs tests/routing-optimizer.test.mjs tests/routing-evaluation.test.mjs tests/routing-paired-statistics.test.mjs tests/routing-selftest-tools.test.mjs tests/livebench-aggregate-audit.test.mjs tests/livebench-prior-study.test.mjs tests/routing-search-experiment.test.mjs tests/routing-regression-study.test.mjs tests/routing-search-summary.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/tool-install-sdk-ui.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/install-settings-host.test.mjs tests/task-executors.test.mjs tests/tool-install-preferences.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs tests/cli-terminal.test.mjs tests/cli-terminal-gateway.test.mjs tests/latest-versions.test.mjs tests/vendor-installer-validation.test.mjs tests/install-entry-selection.test.mjs",
106
+ "test": "node --test tests/adaptive-*.test.mjs tests/dynamic-data.test.mjs tests/router.test.mjs tests/routing-optimizer.test.mjs tests/routing-evaluation.test.mjs tests/routing-paired-statistics.test.mjs tests/routing-selftest-tools.test.mjs tests/livebench-aggregate-audit.test.mjs tests/livebench-prior-study.test.mjs tests/routing-search-experiment.test.mjs tests/routing-regression-study.test.mjs tests/routing-search-summary.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/tool-install-sdk-ui.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/install-settings-host.test.mjs tests/task-executors.test.mjs tests/tool-install-preferences.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs tests/cli-terminal.test.mjs tests/cli-terminal-gateway.test.mjs tests/latest-versions.test.mjs tests/vendor-installer-validation.test.mjs tests/install-entry-selection.test.mjs tests/script-directory-safety.test.mjs",
107
107
  "prepack": "node scripts/prepack.mjs",
108
108
  "pack:local": "npm pack --pack-destination dist"
109
109
  },