@ljwei-stak/dsh-model-router 0.16.2 → 0.16.4

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.
@@ -1898,12 +1898,11 @@ function toolInstallAction({ tool, probe, readiness, job, probeStatus, latestVer
1898
1898
  const running = job?.status === "running";
1899
1899
  const installed = probe?.installed === true;
1900
1900
  const order = installed && latestVersion ? compareReleaseVersions(probe.version, latestVersion) : null;
1901
- const ready = readiness?.ready === true;
1902
1901
  const desktop = tool.manager === "signed-windows-installer";
1903
1902
  const noRunner = desktop || tool.headlessAdapter === true;
1904
- const repair = installed && !noRunner && readiness?.ready === false;
1903
+ const repair = installed && !noRunner && readiness?.ready === false && order !== 0;
1905
1904
  const label = running ? "\u5B89\u88C5\u4E2D\u2026" : !installed ? job?.status === "failed" ? "\u91CD\u8BD5\u5B89\u88C5" : desktop ? "\u4E0B\u8F7D\u6700\u65B0\u5B89\u88C5\u5668" : "\u4E00\u952E\u5B89\u88C5\u6700\u65B0\u7248" : order === -1 ? `\u66F4\u65B0\u5230\u6700\u65B0\u7248 ${latestVersion}` : order === 1 ? "\u5DF2\u9AD8\u4E8E\u6700\u65B0\u6B63\u5F0F\u7248" : repair ? "\u4FEE\u590D\u5B98\u65B9\u6267\u884C\u5165\u53E3" : order === 0 ? "\u5DF2\u662F\u6700\u65B0\u7248\u672C" : "\u5B89\u88C5\u6700\u65B0\u7248";
1906
- const current = order === 0 && (ready || noRunner);
1905
+ const current = order === 0;
1907
1906
  const interactive = probeStatus === "ready" || probeStatus === "refreshing";
1908
1907
  return {
1909
1908
  label,
@@ -12155,7 +12154,7 @@ var xterm_default = `/**
12155
12154
  `;
12156
12155
 
12157
12156
  // .dsh-plugin/client/host-version.mjs
12158
- var ROUTER_CLIENT_VERSION = true ? "0.16.2" : "";
12157
+ var ROUTER_CLIENT_VERSION = true ? "0.16.4" : "";
12159
12158
  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";
12160
12159
  function isMissingRemoteMethod(message) {
12161
12160
  const value = String(message ?? "");
@@ -81,9 +81,14 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
81
81
  /** Re-probe the local fixed registry; the caller cannot supply a command. */
82
82
  async list() {
83
83
  const tools = await probeAllTools({ fresh: true })
84
- const executionReadiness = await Promise.all(tools.map(tool => tool.installed
85
- ? officialToolReadiness(tool.id)
86
- : Promise.resolve({ id: tool.id, ready: false, reason: 'CLI 尚未安装或版本检测失败。' })))
84
+ const executionReadiness = await Promise.all(tools.map(async tool => {
85
+ if (!tool.installed) return { id: tool.id, ready: false, reason: 'CLI 尚未安装或版本检测失败。' }
86
+ try {
87
+ return await officialToolReadiness(tool.id)
88
+ } catch (error) {
89
+ return { id: tool.id, ready: false, reason: `执行入口检测失败:${String(error?.message ?? error).slice(0, 200)}` }
90
+ }
91
+ }))
87
92
  return { tools, executionCapabilities: officialToolExecutionCapabilities(), executionReadiness, hostVersion: HOST_PLUGIN_VERSION }
88
93
  }
89
94
 
@@ -171,6 +171,42 @@ function versionAtLeast(value, minimum) {
171
171
  return true
172
172
  }
173
173
 
174
+ /** Relative bin path inside a package. Reject `..` and empty segments. */
175
+ export function normalizeRelativeBin(value) {
176
+ const text = String(value ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '')
177
+ if (!text || text.split('/').some(part => part === '' || part === '..')) return ''
178
+ return text
179
+ }
180
+
181
+ /**
182
+ * Official entry files to try, declared bin first. `./dist/main.mjs` matches
183
+ * the pinned `dist/main.mjs`. A package whose bin moved still resolves when
184
+ * the file stays inside that package.
185
+ */
186
+ export function packageBinCandidates(binField, binName, expectedBin) {
187
+ const declared = normalizeRelativeBin(binField && typeof binField === 'object' ? binField[binName] : binField)
188
+ const expected = normalizeRelativeBin(expectedBin)
189
+ return [...new Set([declared, expected].filter(Boolean))]
190
+ }
191
+
192
+ async function readPackageEntry(packageDirectory, packageName, binName, expectedBin, workspace, seen) {
193
+ try {
194
+ const manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8'))
195
+ if (manifest.name !== packageName) return null
196
+ const packageRoot = await realpath(packageDirectory)
197
+ if (packageRoot === workspace || inside(workspace, packageRoot) || seen.has(packageRoot)) return null
198
+ for (const relativeBin of packageBinCandidates(manifest.bin, binName, expectedBin)) {
199
+ try {
200
+ const entry = await realpath(join(packageRoot, relativeBin))
201
+ if (!inside(packageRoot, entry) || !(await stat(entry)).isFile()) continue
202
+ seen.add(packageRoot)
203
+ return { entry, packageRoot, version: manifest.version, source: 'verified-npm-package' }
204
+ } catch { /* try the package's other bin candidate */ }
205
+ }
206
+ } catch { /* this directory is not the requested package */ }
207
+ return null
208
+ }
209
+
174
210
  /** Resolve the package's real `bin` file, never its Windows .cmd shim. */
175
211
  async function findGlobalPackageEntries(packageName, binName, expectedBin, workspace) {
176
212
  await ensureNpmPrefixOnPath()
@@ -181,19 +217,13 @@ async function findGlobalPackageEntries(packageName, binName, expectedBin, works
181
217
  const moduleParents = [join(directory, 'node_modules')]
182
218
  if (basename(directory).toLowerCase() === '.bin') moduleParents.push(resolve(directory, '..'))
183
219
  if (!IS_WINDOWS) moduleParents.push(resolve(directory, '..', 'lib', 'node_modules'))
184
- for (const modules of moduleParents) {
185
- const packageDirectory = join(modules, ...packageParts)
186
- try {
187
- const manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8'))
188
- if (manifest.name !== packageName || manifest.bin?.[binName] !== expectedBin) continue
189
- const packageRoot = await realpath(packageDirectory)
190
- if (packageRoot === workspace || inside(workspace, packageRoot)) continue
191
- if (seen.has(packageRoot)) continue
192
- const entry = await realpath(join(packageRoot, expectedBin))
193
- if (!inside(packageRoot, entry) || !(await stat(entry)).isFile()) continue
194
- seen.add(packageRoot)
195
- found.push({ entry, packageRoot, version: manifest.version, source: 'verified-npm-package' })
196
- } catch { /* this PATH entry is not the requested package */ }
220
+ const candidates = moduleParents.map(modules => join(modules, ...packageParts))
221
+ // Native installs put the exe in `<package>/bin` and that directory on PATH
222
+ // (for example D:\Program Files\Claude\node_modules\@anthropic-ai\claude-code\bin).
223
+ candidates.push(directory, resolve(directory, '..'))
224
+ for (const packageDirectory of candidates) {
225
+ const entry = await readPackageEntry(packageDirectory, packageName, binName, expectedBin, workspace, seen)
226
+ if (entry) found.push(entry)
197
227
  }
198
228
  }
199
229
  return found
@@ -39,18 +39,44 @@ const CLAUDE_UPDATE_TIMEOUT_MS = 180_000
39
39
  export const CLAUDE_UPDATE_ARGS = Object.freeze(['update'])
40
40
 
41
41
  /**
42
- * Quote a fixed registry command for `shell: true`.
43
- * Node concatenates shell arguments without escaping; Windows cmd would otherwise
44
- * split on characters inside package specs such as `@scope/pkg@latest`.
42
+ * Spawn shape for a fixed registry command.
43
+ *
44
+ * Windows needs `shell: true` so `npm.cmd` / `where.exe` resolve. Node itself
45
+ * wraps that as `cmd.exe /d /s /c`. Pre-quoting every argument (0.16.2) made
46
+ * cmd strip the wrong quotes and launch `node` against a missing script:
47
+ * `MODULE_NOT_FOUND` at `executeUserEntryPoint` with an empty requireStack.
48
+ * That failed `npm config get prefix`, `--version`, and `npm install -g` for
49
+ * every official tool. Keep the executable and argv separate.
45
50
  */
46
- export function shellCommandLine(executable, args, platform = process.platform) {
47
- const quote = value => {
48
- const text = String(value)
49
- if (text.includes('\0') || /[\r\n]/.test(text)) throw new Error('命令参数含有非法字符。')
50
- if (platform === 'win32') return `"${text.replace(/"/g, '""')}"`
51
- return `'${text.replace(/'/g, `'"'"'`)}'`
51
+ export function shellSpawnSpec(executable, args = [], { useShell = false } = {}) {
52
+ return { file: String(executable), args: [...(args ?? [])], shell: Boolean(useShell) }
53
+ }
54
+
55
+ /** Decode a CLI banner. Native Windows exes often write UTF-16 to a pipe. */
56
+ export function decodeProcessOutput(value) {
57
+ const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value ?? '')
58
+ if (bytes.length >= 2 && bytes[0] === 0xFF && bytes[1] === 0xFE) {
59
+ return bytes.toString('utf16le').replace(/^\uFEFF/, '')
60
+ }
61
+ if (bytes.length >= 2 && bytes[0] === 0xFE && bytes[1] === 0xFF) {
62
+ const swapped = Buffer.from(bytes)
63
+ swapped.swap16()
64
+ return swapped.toString('utf16le').replace(/^\uFEFF/, '')
52
65
  }
53
- return [executable, ...(args ?? [])].map(quote).join(' ')
66
+ const sample = bytes.subarray(0, Math.min(bytes.length, 64))
67
+ let zeros = 0
68
+ for (let index = 1; index < sample.length; index += 2) if (sample[index] === 0) zeros += 1
69
+ const pairs = Math.floor(sample.length / 2)
70
+ if (pairs >= 4 && zeros >= pairs * 0.6) return bytes.toString('utf16le').replace(/\u0000/g, '')
71
+ return bytes.toString('utf8').replace(/^\uFEFF/, '')
72
+ }
73
+
74
+ /** Absolute paths printed by `where` / `which`, one per line. */
75
+ export function locatorPaths(stdout) {
76
+ return String(stdout ?? '').split(/\r?\n/).map(line => line.trim().replace(/^"|"$/g, '')).filter(line => {
77
+ if (!line || /^INFO:/i.test(line)) return false
78
+ return isAbsolute(line)
79
+ })
54
80
  }
55
81
 
56
82
  /** Visible failure when the installer exited cleanly but the probed copy is still older. */
@@ -85,22 +111,28 @@ function runCapture(executable, args, { timeoutMs, onOutput, useShell = false, s
85
111
  resolve({ ok: false, code: null, stdout: '', stderr: '', timedOut: false, cancelled: true })
86
112
  return
87
113
  }
88
- let stdout = ''
89
- let stderr = ''
114
+ const stdoutChunks = []
115
+ const stderrChunks = []
116
+ const decoded = chunks => decodeProcessOutput(Buffer.concat(chunks))
117
+ const pushChunk = (chunks, data) => {
118
+ const buf = Buffer.isBuffer(data) ? data : Buffer.from(data)
119
+ chunks.push(buf)
120
+ let total = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
121
+ while (total > 64_000 && chunks.length > 1) total -= chunks.shift().length
122
+ return decodeProcessOutput(buf)
123
+ }
90
124
  let settled = false
91
125
  let stopReason = null
92
126
  let child
93
127
  try {
94
- // One pre-quoted command string so Node's shell wrapper cannot split
95
- // `@scope/pkg@latest`. PowerShell keeps useShell false and an args array.
96
- const command = useShell ? shellCommandLine(executable, args) : executable
97
- child = spawn(command, useShell ? [] : args, {
128
+ const spec = shellSpawnSpec(executable, args, { useShell })
129
+ child = spawn(spec.file, spec.args, {
98
130
  stdio: ['ignore', 'pipe', 'pipe'],
99
131
  windowsHide: true,
100
132
  // Windows npm CLIs install as .cmd shims; only cmd.exe can execute
101
133
  // them. Safe here because every executable and argument comes from
102
134
  // the fixed registry — caller input never reaches this boundary.
103
- shell: useShell,
135
+ shell: spec.shell,
104
136
  ...(env ? { env } : {}),
105
137
  })
106
138
  } catch (error) {
@@ -131,7 +163,7 @@ function runCapture(executable, args, { timeoutMs, onOutput, useShell = false, s
131
163
  } else {
132
164
  try { child.kill('SIGTERM') } catch { /* already gone */ }
133
165
  }
134
- graceTimer = setTimeout(() => finish({ ok: false, code: null, stdout, stderr,
166
+ graceTimer = setTimeout(() => finish({ ok: false, code: null, stdout: decoded(stdoutChunks), stderr: decoded(stderrChunks),
135
167
  timedOut: reason === 'timeout', cancelled: reason === 'cancel' }), 5_000)
136
168
  graceTimer.unref?.()
137
169
  }
@@ -141,20 +173,25 @@ function runCapture(executable, args, { timeoutMs, onOutput, useShell = false, s
141
173
  if (signal?.aborted) onAbort()
142
174
  child.stdout?.on('data', data => {
143
175
  if (settled || stopReason) return
144
- stdout = appendBounded(stdout, String(data))
145
- onOutput?.(String(data))
176
+ // Always keep the bytes. Optional chaining must not guard pushChunk:
177
+ // version and PATH probes omit onOutput, and a skipped push leaves
178
+ // stdout empty so every installed CLI looks not-installed.
179
+ const text = pushChunk(stdoutChunks, data)
180
+ onOutput?.(text)
146
181
  })
147
182
  child.stderr?.on('data', data => {
148
183
  if (settled || stopReason) return
149
- stderr = appendBounded(stderr, String(data))
150
- onOutput?.(String(data))
184
+ const text = pushChunk(stderrChunks, data)
185
+ onOutput?.(text)
151
186
  })
152
187
  child.on('error', error => {
188
+ const stdout = decoded(stdoutChunks)
189
+ const stderr = decoded(stderrChunks)
153
190
  finish({ ok: false, code: null, stdout, stderr: `${stderr}${error.message}`.trim(),
154
191
  timedOut: stopReason === 'timeout', cancelled: stopReason === 'cancel' })
155
192
  })
156
193
  child.on('close', (code, signal) => {
157
- finish({ ok: !stopReason && code === 0, code, stdout, stderr,
194
+ finish({ ok: !stopReason && code === 0, code, stdout: decoded(stdoutChunks), stderr: decoded(stderrChunks),
158
195
  timedOut: stopReason === 'timeout', cancelled: stopReason === 'cancel', signal })
159
196
  })
160
197
  })
@@ -277,11 +314,6 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
277
314
  /** Tools whose ready-check failure can be repaired by reinstalling the same version. */
278
315
  const CAPABILITY_REPAIRABLE = new Set(['claude-code', 'codex', 'kimi-code', 'minimax-code', 'mimo-code', 'grok-build'])
279
316
 
280
- function appendBounded(current, addition) {
281
- const merged = current + addition
282
- return merged.length > 64_000 ? merged.slice(merged.length - 32_000) : merged
283
- }
284
-
285
317
  /** Extract the first version-looking token from a CLI banner. */
286
318
  export function versionFromBanner(banner) {
287
319
  const match = String(banner ?? '').match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/)
@@ -336,16 +368,28 @@ async function probeUncached(tool, runner) {
336
368
  if (!located.ok || !located.stdout.trim()) {
337
369
  continue
338
370
  }
339
- const attempt = await runner(executable, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS })
371
+ const attempt = await runner(executable, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS, useShell: IS_WINDOWS })
340
372
  if (attempt.timedOut) {
341
373
  return { id: tool.id, installed: false, version: null, status: 'probe-timeout', detail: `${executable} --version 超时(${PROBE_TIMEOUT_MS / 1000}s)` }
342
374
  }
343
- if (attempt.ok) {
344
- const banner = `${attempt.stdout}\n${attempt.stderr}`.trim()
375
+ const attempts = [attempt]
376
+ if (!versionFromBanner(probeBanner(attempt))) {
377
+ for (const target of locatorPaths(located.stdout)) {
378
+ if (target === executable) continue
379
+ const direct = await runner(target, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS, useShell: false })
380
+ attempts.push(direct)
381
+ if (direct.timedOut) break
382
+ if (direct.ok || versionFromBanner(probeBanner(direct))) break
383
+ }
384
+ }
385
+ const chosen = attempts.find(item => item && (item.ok || versionFromBanner(probeBanner(item)))) ?? attempt
386
+ const banner = probeBanner(chosen)
387
+ const version = versionFromBanner(banner)
388
+ if (chosen.ok || version) {
345
389
  return {
346
390
  id: tool.id,
347
391
  installed: true,
348
- version: versionFromBanner(banner),
392
+ version,
349
393
  status: 'installed',
350
394
  detail: tool.probeNote ?? '',
351
395
  bannerLine: banner.split('\n').find(Boolean)?.slice(0, 200) ?? '',
@@ -356,7 +400,7 @@ async function probeUncached(tool, runner) {
356
400
  installed: false,
357
401
  version: null,
358
402
  status: 'probe-failed',
359
- detail: `${executable} --version 退出码 ${attempt.code ?? '信号终止'};程序可能损坏或版本过旧。${attempt.stderr.split('\n').find(Boolean)?.slice(0, 120) ?? ''}`,
403
+ detail: `${executable} --version 退出码 ${chosen.code ?? '信号终止'};程序可能损坏或版本过旧。${String(chosen.stderr ?? '').split('\n').find(Boolean)?.slice(0, 120) ?? ''}`,
360
404
  }
361
405
  }
362
406
  return { id: tool.id, installed: false, version: null, status: 'not-installed', detail: '未在 PATH 中找到官方命令。' }
@@ -364,7 +408,7 @@ async function probeUncached(tool, runner) {
364
408
 
365
409
  /** Default runner: real spawn with the platform's shell for .cmd shims. */
366
410
  export const defaultRunner = (executable, args, options = {}) =>
367
- runCapture(executable, args, { ...options, useShell: IS_WINDOWS })
411
+ runCapture(executable, args, { useShell: IS_WINDOWS, ...options })
368
412
 
369
413
  let prefixPromise = null
370
414
 
@@ -413,12 +457,31 @@ export async function installedToolIds() {
413
457
  * Probe every registry tool in parallel (the slowest single probe bounds the
414
458
  * wall time; the cache keeps repeat calls instant).
415
459
  */
460
+ /** One tool's probe failure must not reject detection of the rest of the page. */
461
+ export async function probeToolSafely(tool, runner, options) {
462
+ try {
463
+ return await probeToolWith(tool, runner, options)
464
+ } catch (error) {
465
+ return {
466
+ id: tool.id,
467
+ installed: false,
468
+ version: null,
469
+ status: 'probe-failed',
470
+ detail: `检测失败:${String(error?.message ?? error).slice(0, 200)}`,
471
+ }
472
+ }
473
+ }
474
+
416
475
  export async function probeAllTools({ fresh = false } = {}) {
417
476
  await ensureNpmPrefixOnPath()
418
- return Promise.all(OFFICIAL_TOOLS.map(tool => probeToolWith(tool, defaultRunner,
477
+ return Promise.all(OFFICIAL_TOOLS.map(tool => probeToolSafely(tool, defaultRunner,
419
478
  fresh ? { cacheMs: 0 } : {})))
420
479
  }
421
480
 
481
+ function probeBanner(attempt) {
482
+ return `${attempt?.stdout ?? ''}\n${attempt?.stderr ?? ''}`.trim()
483
+ }
484
+
422
485
  export function probeSnapshot() {
423
486
  return [...probeCache.values()].map(entry => entry.value)
424
487
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.4 — 2026-10-09
4
+
5
+ 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.16.3 artifacts remain unchanged.
6
+
7
+ - PATH probes and `npm config get prefix` omit the install-log callback. 0.16.3 only stored stdout inside `onOutput?.(...)`, so the optional call skipped capture and `where` / `--version` returned success with empty output. Every PATH CLI was reported not-installed. MiniMax and ZCode still appeared because they do not use that probe.
8
+ - Capture now always records stdout and stderr, then forwards text to `onOutput` when a caller supplied one. Install logs are unchanged.
9
+ - The 0.16.3 spawn shape (no pre-quoted Windows command line) and the 0.16.2 update-button job ownership remain.
10
+ - Linux validation: 631 tests passed, 4 conditional skips, `npm run check:client` passed. This environment did not re-run the Windows PATH probe.
11
+
12
+ ## 0.16.3 — 2026-10-09
13
+
14
+ Stable patch under the npm `latest` tag. Fully quit Harness, including a WebUI server process or the desktop tray, and restart after upgrading so the Host reloads. Existing 0.16.2 artifacts remain unchanged.
15
+
16
+ - Windows `shell: true` spawns again pass the executable and arguments separately. 0.16.2 pre-quoted that command line; cmd then started Node on a missing script (`MODULE_NOT_FOUND`, empty `requireStack`, Node 24) for every official tool's probe, `npm config get prefix`, and `npm install -g`.
17
+ - A `--version` banner that contains a version counts as installed, including UTF-16 output from a native Windows executable. If the name on PATH fails, the absolute path from `where`/`which` is tried without a shell. One probe or readiness failure stays on that card.
18
+ - Package entry lookup accepts `./dist/main.mjs` and a `bin/` directory that is itself on PATH. A CLI that is already the latest release stays on “已是最新版本” instead of “修复官方执行入口”.
19
+ - The 0.16.2 update-button behavior remains: the click keeps its own job, refresh does not disable a visible update, and an install that leaves the probed version older fails with a row error.
20
+ - Release validation on Linux: 630 tests passed, 4 conditional skips, `npm run check:client` passed. No Harness WebUI, Harness Desktop, or Windows `cmd.exe` run was available in this environment.
21
+
3
22
  ## 0.16.2 — 2026-10-09
4
23
 
5
24
  Stable patch under the npm `latest` tag. Fully quit Harness, including the tray process, and restart after upgrading. Existing 0.16.1 artifacts remain unchanged.
@@ -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.16.2**。本版修复官方工具更新/安装按钮:点击会真正启动更新,失败时在该行显示原因,而不是没有任何反应。0.16.1 的反馈缓存、刷新与预算保护仍然保留。公开数据默认关闭;主观偏好调整不改变公共能力评分或质量门槛。源码开发可使用独立模拟 UI 预览;安装时选择下方精确版本。
5
+ 当前路由正式版为 **0.16.4**。本版修复 PATH 探测丢输出:没有安装日志回调时,`where` 和 `--version` 的 stdout 仍会保留,已安装的 CLI 不再被报成未安装。0.16.3 的命令行参数修复、0.16.2 的更新按钮修复和 0.16.1 的反馈缓存仍然保留。公开数据默认关闭;主观偏好调整不改变公共能力评分或质量门槛。源码开发可使用独立模拟 UI 预览;安装时选择下方精确版本。
6
6
 
7
7
  [路由 README](README.zh.md) · [迁移说明](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.16.2` | 模型路由 |
13
+ | 分析复杂度、按质量和费用选模型、调用官方工具 | `@ljwei-stak/dsh-model-router@0.16.4` | 模型路由 |
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.16.2
27
+ @ljwei-stak/dsh-model-router@0.16.4
28
28
  ```
29
29
 
30
30
  只想安装 GAL 时填写:
@@ -40,7 +40,7 @@
40
40
  ```
41
41
 
42
42
  4. 核对预览的包名、版本和宿主兼容信息,安装并启用。完全退出 Harness(包括托盘进程)再启动,确保 Host 加载新代码。
43
- 5. 检查插件详情与侧边栏:路由应为 **0.16.2 / 模型路由**;GAL 应为 **0.1.0 / Gal 模块**。仅装路由时没有 GAL 入口是预期行为。
43
+ 5. 检查插件详情与侧边栏:路由应为 **0.16.4 / 模型路由**;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.16.2` 获取同一安装包)
51
+ - [模型路由 Releases](https://github.com/Alice-Marx/dsh-model-router/releases)(也可运行 `npm pack @ljwei-stak/dsh-model-router@0.16.4` 获取同一安装包)
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.16.2.tgz
57
+ D:\Plugins\ljwei-stak-dsh-model-router-0.16.4.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.16.2.tgz'
67
+ Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-model-router-0.16.4.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.16.2`。不要两个同时安装(条目 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.16.4`。不要两个同时安装(条目 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.16.2 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
85
+ 工作台自 0.15.0 起分成**任务与执行、模型配置、官方工具、预算与安全**四页;0.16.4 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
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.16.2 详情、模型路由入口、目录和模型档案可用;不出现旧合并 GAL 入口。 |
117
+ | 路由独立安装 | 0.16.4 详情、模型路由入口、目录和模型档案可用;不出现旧合并 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.16.2 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
128
+ 历史 0.15.0 的自动测试验证为 220 项通过、3 项 Windows 平台跳过;组件 UI 预览检查使用模拟数据。**0.16.4 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
129
129
 
130
130
  ## 8. 开发者从完整源码构建
131
131
 
132
- 路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.16.2 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
132
+ 路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.16.4 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
133
133
 
134
134
  ```powershell
135
135
  pnpm install --frozen-lockfile --strict-peer-dependencies
package/README.md CHANGED
@@ -1,16 +1,20 @@
1
1
  # Model Router · DeepSeek Harness Desktop
2
2
 
3
- Version: **0.16.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.
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.
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.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
+
9
+ **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 “修复官方执行入口”.
10
+
7
11
  **0.16.2:** the official-tools update/install button keeps the job started by that click, stays usable while a previous probe is refreshing, and shows a row error when the click cannot start or the probed version does not advance. Installed Claude Code runs `claude update` before the registry npm install. Installed Windows MiniMax runs the official installer.
8
12
 
9
13
  **0.16.1:** [maturity improvements and validation](docs/ROUTING_MATURITY.zh.md) adds stable feedback cache identity, per-model/task feedback explanations, source-refresh and settings-race protection, and budget gates for unreadable or incomplete cost history. Mechanism tests do not establish real quality–cost gains.
10
14
 
11
15
  **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.
12
16
 
13
- Current version: **0.16.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.
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.
14
18
 
15
19
  **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.
16
20
 
@@ -42,16 +46,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
42
46
  Open **Plugins → Add plugin** and enter:
43
47
 
44
48
  ```text
45
- @ljwei-stak/dsh-model-router@0.16.2
49
+ @ljwei-stak/dsh-model-router@0.16.4
46
50
  ```
47
51
 
48
- 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.2** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
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.
49
53
 
50
54
  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.
51
55
 
52
56
  ### Upgrade from the old package name
53
57
 
54
- 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.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.
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.
55
59
 
56
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).
57
61
 
package/README.zh.md CHANGED
@@ -1,16 +1,20 @@
1
1
  # Model Router · DeepSeek Harness 模型路由插件
2
2
 
3
- 版本:**0.16.2**。选模求解器概览见[路由研究说明](docs/ROUTING_RESEARCH.zh.md);公式、条件证明与实验见[数学推导与实验记录](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md)。[LiveBench 最新证据与验证协议](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md)已核查 2026-06-25 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
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 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
4
4
 
5
5
  为 **DeepSeek Harness Desktop** 提供任务规划、成本感知的模型分配和官方工具执行工作台。输入任务,比较已配置模型,审阅计划,再明确选择是否执行。
6
6
 
7
+ **0.16.4**:PATH 探测在没有安装日志回调时也会保留命令输出。0.16.3 把 stdout 写进了 `onOutput?.(...)`,`where` 和 `--version` 因此变成空字符串,PATH 上的 CLI 全被报成未安装。MiniMax 与 ZCode 不走这条探测,所以仍能显示。
8
+
9
+ **0.16.3**:恢复全部官方工具卡片的检测和安装。Windows 上的探测与 `npm` 再次按独立参数启动,避免 cmd 把 Node 指到不存在的脚本。只要输出版本号就视为已安装;单张卡片的检测错误留在该行;已经是最新版的 CLI 不再被收成「修复官方执行入口」。
10
+
7
11
  **0.16.2**:官方工具的更新/安装按钮会保留本次点击启动的任务;已有探测结果时重新体检不再把按钮禁用成无响应;点击无法开始、或安装后探测版本仍低于最新版时,在该行显示错误。已安装的 Claude Code 先执行 `claude update`,再按注册表走 npm;已安装的 Windows MiniMax 走官方安装脚本。
8
12
 
9
13
  **0.16.1 成熟度改进**:稳定反馈缓存、分模型/任务解释、刷新与设置竞态保护,以及费用历史不可读取或不完整时的预算门禁。[推导与验证](docs/ROUTING_MATURITY.zh.md)随安装包提供,机制测试不等于真实质量—成本收益。
10
14
 
11
15
  **0.16.0 新增**:[动态数据与持续反馈](docs/ROUTING_ADAPTIVE_LEARNING.zh.md):可选公开价格/LiveBench快照刷新;按任务类型、时间衰减和低样本收缩调整个人偏好,质量门槛独立保留。设置在“预算与安全”,公开源默认关闭;价格源需提供按官方文档核对的固定USD单价JSON,不是通用官网抓价器。本地DSH_HOME共享、最近200运行窗口,暂无真实收益证明。
12
16
 
13
- 当前版本为 **0.16.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) 已拆成两个独立插件。
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) 已拆成两个独立插件。
14
18
 
15
19
  **0.15.0 更新**:工作台分为四页,路由建议紧接任务规划展示;执行输入变化时丢弃旧预览;裁剪历史或重试后仍保留费用累计;源码开发新增独立模拟 UI 预览。
16
20
 
@@ -42,16 +46,16 @@
42
46
  打开 **插件 → 添加插件**,填写:
43
47
 
44
48
  ```text
45
- @ljwei-stak/dsh-model-router@0.16.2
49
+ @ljwei-stak/dsh-model-router@0.16.4
46
50
  ```
47
51
 
48
- 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.2**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
52
+ 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.4**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
49
53
 
50
54
  也可在插件管理器填写本地 `.tgz` 文件的绝对路径,或解压后的内层 `package` 目录;其中应包含 `package.json` 与 `.dsh-plugin`。[历史发布页](https://github.com/Alice-Marx/dsh-model-router/releases)提供归档;已克隆源码的开发方式见下文。
51
55
 
52
56
  ### 从旧包名升级
53
57
 
54
- `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.2`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
58
+ `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.4`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
55
59
 
56
60
  从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](MIGRATION.md)。
57
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ljwei-stak/dsh-model-router",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
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": {