@ljwei-stak/dsh-model-router 0.16.2 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +3 -4
- package/.dsh-plugin/official-tools-remote-service.mjs +8 -3
- package/.dsh-plugin/shared/official-tool-executor.mjs +43 -13
- package/.dsh-plugin/shared/official-tools-runtime.mjs +93 -35
- package/CHANGELOG.md +10 -0
- package/INSTALLATION_GUIDE.zh.md +12 -12
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/package.json +1 -1
package/.dsh-plugin/client.js
CHANGED
|
@@ -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
|
|
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.
|
|
12157
|
+
var ROUTER_CLIENT_VERSION = true ? "0.16.3" : "";
|
|
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 =>
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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:
|
|
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,20 @@ 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
|
-
|
|
145
|
-
onOutput?.(String(data))
|
|
176
|
+
onOutput?.(pushChunk(stdoutChunks, data))
|
|
146
177
|
})
|
|
147
178
|
child.stderr?.on('data', data => {
|
|
148
179
|
if (settled || stopReason) return
|
|
149
|
-
|
|
150
|
-
onOutput?.(String(data))
|
|
180
|
+
onOutput?.(pushChunk(stderrChunks, data))
|
|
151
181
|
})
|
|
152
182
|
child.on('error', error => {
|
|
183
|
+
const stdout = decoded(stdoutChunks)
|
|
184
|
+
const stderr = decoded(stderrChunks)
|
|
153
185
|
finish({ ok: false, code: null, stdout, stderr: `${stderr}${error.message}`.trim(),
|
|
154
186
|
timedOut: stopReason === 'timeout', cancelled: stopReason === 'cancel' })
|
|
155
187
|
})
|
|
156
188
|
child.on('close', (code, signal) => {
|
|
157
|
-
finish({ ok: !stopReason && code === 0, code, stdout, stderr,
|
|
189
|
+
finish({ ok: !stopReason && code === 0, code, stdout: decoded(stdoutChunks), stderr: decoded(stderrChunks),
|
|
158
190
|
timedOut: stopReason === 'timeout', cancelled: stopReason === 'cancel', signal })
|
|
159
191
|
})
|
|
160
192
|
})
|
|
@@ -277,11 +309,6 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
|
277
309
|
/** Tools whose ready-check failure can be repaired by reinstalling the same version. */
|
|
278
310
|
const CAPABILITY_REPAIRABLE = new Set(['claude-code', 'codex', 'kimi-code', 'minimax-code', 'mimo-code', 'grok-build'])
|
|
279
311
|
|
|
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
312
|
/** Extract the first version-looking token from a CLI banner. */
|
|
286
313
|
export function versionFromBanner(banner) {
|
|
287
314
|
const match = String(banner ?? '').match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?/)
|
|
@@ -336,16 +363,28 @@ async function probeUncached(tool, runner) {
|
|
|
336
363
|
if (!located.ok || !located.stdout.trim()) {
|
|
337
364
|
continue
|
|
338
365
|
}
|
|
339
|
-
const attempt = await runner(executable, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS })
|
|
366
|
+
const attempt = await runner(executable, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS, useShell: IS_WINDOWS })
|
|
340
367
|
if (attempt.timedOut) {
|
|
341
368
|
return { id: tool.id, installed: false, version: null, status: 'probe-timeout', detail: `${executable} --version 超时(${PROBE_TIMEOUT_MS / 1000}s)` }
|
|
342
369
|
}
|
|
343
|
-
|
|
344
|
-
|
|
370
|
+
const attempts = [attempt]
|
|
371
|
+
if (!versionFromBanner(probeBanner(attempt))) {
|
|
372
|
+
for (const target of locatorPaths(located.stdout)) {
|
|
373
|
+
if (target === executable) continue
|
|
374
|
+
const direct = await runner(target, ['--version'], { timeoutMs: PROBE_TIMEOUT_MS, useShell: false })
|
|
375
|
+
attempts.push(direct)
|
|
376
|
+
if (direct.timedOut) break
|
|
377
|
+
if (direct.ok || versionFromBanner(probeBanner(direct))) break
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const chosen = attempts.find(item => item && (item.ok || versionFromBanner(probeBanner(item)))) ?? attempt
|
|
381
|
+
const banner = probeBanner(chosen)
|
|
382
|
+
const version = versionFromBanner(banner)
|
|
383
|
+
if (chosen.ok || version) {
|
|
345
384
|
return {
|
|
346
385
|
id: tool.id,
|
|
347
386
|
installed: true,
|
|
348
|
-
version
|
|
387
|
+
version,
|
|
349
388
|
status: 'installed',
|
|
350
389
|
detail: tool.probeNote ?? '',
|
|
351
390
|
bannerLine: banner.split('\n').find(Boolean)?.slice(0, 200) ?? '',
|
|
@@ -356,7 +395,7 @@ async function probeUncached(tool, runner) {
|
|
|
356
395
|
installed: false,
|
|
357
396
|
version: null,
|
|
358
397
|
status: 'probe-failed',
|
|
359
|
-
detail: `${executable} --version 退出码 ${
|
|
398
|
+
detail: `${executable} --version 退出码 ${chosen.code ?? '信号终止'};程序可能损坏或版本过旧。${String(chosen.stderr ?? '').split('\n').find(Boolean)?.slice(0, 120) ?? ''}`,
|
|
360
399
|
}
|
|
361
400
|
}
|
|
362
401
|
return { id: tool.id, installed: false, version: null, status: 'not-installed', detail: '未在 PATH 中找到官方命令。' }
|
|
@@ -364,7 +403,7 @@ async function probeUncached(tool, runner) {
|
|
|
364
403
|
|
|
365
404
|
/** Default runner: real spawn with the platform's shell for .cmd shims. */
|
|
366
405
|
export const defaultRunner = (executable, args, options = {}) =>
|
|
367
|
-
runCapture(executable, args, {
|
|
406
|
+
runCapture(executable, args, { useShell: IS_WINDOWS, ...options })
|
|
368
407
|
|
|
369
408
|
let prefixPromise = null
|
|
370
409
|
|
|
@@ -413,12 +452,31 @@ export async function installedToolIds() {
|
|
|
413
452
|
* Probe every registry tool in parallel (the slowest single probe bounds the
|
|
414
453
|
* wall time; the cache keeps repeat calls instant).
|
|
415
454
|
*/
|
|
455
|
+
/** One tool's probe failure must not reject detection of the rest of the page. */
|
|
456
|
+
export async function probeToolSafely(tool, runner, options) {
|
|
457
|
+
try {
|
|
458
|
+
return await probeToolWith(tool, runner, options)
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
id: tool.id,
|
|
462
|
+
installed: false,
|
|
463
|
+
version: null,
|
|
464
|
+
status: 'probe-failed',
|
|
465
|
+
detail: `检测失败:${String(error?.message ?? error).slice(0, 200)}`,
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
416
470
|
export async function probeAllTools({ fresh = false } = {}) {
|
|
417
471
|
await ensureNpmPrefixOnPath()
|
|
418
|
-
return Promise.all(OFFICIAL_TOOLS.map(tool =>
|
|
472
|
+
return Promise.all(OFFICIAL_TOOLS.map(tool => probeToolSafely(tool, defaultRunner,
|
|
419
473
|
fresh ? { cacheMs: 0 } : {})))
|
|
420
474
|
}
|
|
421
475
|
|
|
476
|
+
function probeBanner(attempt) {
|
|
477
|
+
return `${attempt?.stdout ?? ''}\n${attempt?.stderr ?? ''}`.trim()
|
|
478
|
+
}
|
|
479
|
+
|
|
422
480
|
export function probeSnapshot() {
|
|
423
481
|
return [...probeCache.values()].map(entry => entry.value)
|
|
424
482
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.3 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
- 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`.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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 “修复官方执行入口”.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
3
13
|
## 0.16.2 — 2026-10-09
|
|
4
14
|
|
|
5
15
|
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.
|
package/INSTALLATION_GUIDE.zh.md
CHANGED
|
@@ -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
|
|
5
|
+
当前路由正式版为 **0.16.3**。本版恢复全部官方工具卡片的检测与安装:Windows 上不再把每条探测和 `npm` 命令预先加引号,单张卡片的失败不会清空整页。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.
|
|
13
|
+
| 分析复杂度、按质量和费用选模型、调用官方工具 | `@ljwei-stak/dsh-model-router@0.16.3` | 模型路由 |
|
|
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.
|
|
27
|
+
@ljwei-stak/dsh-model-router@0.16.3
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
只想安装 GAL 时填写:
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
4. 核对预览的包名、版本和宿主兼容信息,安装并启用。完全退出 Harness(包括托盘进程)再启动,确保 Host 加载新代码。
|
|
43
|
-
5. 检查插件详情与侧边栏:路由应为 **0.16.
|
|
43
|
+
5. 检查插件详情与侧边栏:路由应为 **0.16.3 / 模型路由**;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.
|
|
51
|
+
- [模型路由 Releases](https://github.com/Alice-Marx/dsh-model-router/releases)(也可运行 `npm pack @ljwei-stak/dsh-model-router@0.16.3` 获取同一安装包)
|
|
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.
|
|
57
|
+
D:\Plugins\ljwei-stak-dsh-model-router-0.16.3.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.
|
|
67
|
+
Get-FileHash -Algorithm SHA256 -LiteralPath 'D:\Plugins\ljwei-stak-dsh-model-router-0.16.3.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.
|
|
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.3`。不要两个同时安装(条目 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.
|
|
85
|
+
工作台自 0.15.0 起分成**任务与执行、模型配置、官方工具、预算与安全**四页;0.16.3 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
|
|
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.
|
|
117
|
+
| 路由独立安装 | 0.16.3 详情、模型路由入口、目录和模型档案可用;不出现旧合并 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.
|
|
128
|
+
历史 0.15.0 的自动测试验证为 220 项通过、3 项 Windows 平台跳过;组件 UI 预览检查使用模拟数据。**0.16.3 的发布验证见[更新记录](CHANGELOG.md)**,不把模拟公开源/模型流和组件预览当作真实付费调用或 Harness Desktop 端到端验收;上表中的实际安装、账号和工作区检查仍需在使用环境中完成。
|
|
129
129
|
|
|
130
130
|
## 8. 开发者从完整源码构建
|
|
131
131
|
|
|
132
|
-
路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.16.
|
|
132
|
+
路由需要 Node.js **22.19+** 与 **pnpm 10.34.6**。从完整源码仓库根目录构建;复现正式版 0.16.3 时检出对应发布标签。旧版(如 0.15.0)标签保留其当时内容。运行:
|
|
133
133
|
|
|
134
134
|
```powershell
|
|
135
135
|
pnpm install --frozen-lockfile --strict-peer-dependencies
|
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Model Router · DeepSeek Harness Desktop
|
|
2
2
|
|
|
3
|
-
Version: **0.16.
|
|
3
|
+
Version: **0.16.3**. 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.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 “修复官方执行入口”.
|
|
8
|
+
|
|
7
9
|
**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
10
|
|
|
9
11
|
**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
12
|
|
|
11
13
|
**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
14
|
|
|
13
|
-
Current version: **0.16.
|
|
15
|
+
Current version: **0.16.3** (`@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
16
|
|
|
15
17
|
**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
18
|
|
|
@@ -42,16 +44,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
|
|
|
42
44
|
Open **Plugins → Add plugin** and enter:
|
|
43
45
|
|
|
44
46
|
```text
|
|
45
|
-
@ljwei-stak/dsh-model-router@0.16.
|
|
47
|
+
@ljwei-stak/dsh-model-router@0.16.3
|
|
46
48
|
```
|
|
47
49
|
|
|
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.
|
|
50
|
+
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.3** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
|
|
49
51
|
|
|
50
52
|
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
53
|
|
|
52
54
|
### Upgrade from the old package name
|
|
53
55
|
|
|
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.
|
|
56
|
+
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.3`. 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
57
|
|
|
56
58
|
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
59
|
|
package/README.zh.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Model Router · DeepSeek Harness 模型路由插件
|
|
2
2
|
|
|
3
|
-
版本:**0.16.
|
|
3
|
+
版本:**0.16.3**。选模求解器概览见[路由研究说明](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.3**:恢复全部官方工具卡片的检测和安装。Windows 上的探测与 `npm` 再次按独立参数启动,避免 cmd 把 Node 指到不存在的脚本。只要输出版本号就视为已安装;单张卡片的检测错误留在该行;已经是最新版的 CLI 不再被收成「修复官方执行入口」。
|
|
8
|
+
|
|
7
9
|
**0.16.2**:官方工具的更新/安装按钮会保留本次点击启动的任务;已有探测结果时重新体检不再把按钮禁用成无响应;点击无法开始、或安装后探测版本仍低于最新版时,在该行显示错误。已安装的 Claude Code 先执行 `claude update`,再按注册表走 npm;已安装的 Windows MiniMax 走官方安装脚本。
|
|
8
10
|
|
|
9
11
|
**0.16.1 成熟度改进**:稳定反馈缓存、分模型/任务解释、刷新与设置竞态保护,以及费用历史不可读取或不完整时的预算门禁。[推导与验证](docs/ROUTING_MATURITY.zh.md)随安装包提供,机制测试不等于真实质量—成本收益。
|
|
10
12
|
|
|
11
13
|
**0.16.0 新增**:[动态数据与持续反馈](docs/ROUTING_ADAPTIVE_LEARNING.zh.md):可选公开价格/LiveBench快照刷新;按任务类型、时间衰减和低样本收缩调整个人偏好,质量门槛独立保留。设置在“预算与安全”,公开源默认关闭;价格源需提供按官方文档核对的固定USD单价JSON,不是通用官网抓价器。本地DSH_HOME共享、最近200运行窗口,暂无真实收益证明。
|
|
12
14
|
|
|
13
|
-
当前版本为 **0.16.
|
|
15
|
+
当前版本为 **0.16.3**,包名为 `@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
16
|
|
|
15
17
|
**0.15.0 更新**:工作台分为四页,路由建议紧接任务规划展示;执行输入变化时丢弃旧预览;裁剪历史或重试后仍保留费用累计;源码开发新增独立模拟 UI 预览。
|
|
16
18
|
|
|
@@ -42,16 +44,16 @@
|
|
|
42
44
|
打开 **插件 → 添加插件**,填写:
|
|
43
45
|
|
|
44
46
|
```text
|
|
45
|
-
@ljwei-stak/dsh-model-router@0.16.
|
|
47
|
+
@ljwei-stak/dsh-model-router@0.16.3
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.
|
|
50
|
+
镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.3**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
|
|
49
51
|
|
|
50
52
|
也可在插件管理器填写本地 `.tgz` 文件的绝对路径,或解压后的内层 `package` 目录;其中应包含 `package.json` 与 `.dsh-plugin`。[历史发布页](https://github.com/Alice-Marx/dsh-model-router/releases)提供归档;已克隆源码的开发方式见下文。
|
|
51
53
|
|
|
52
54
|
### 从旧包名升级
|
|
53
55
|
|
|
54
|
-
`@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.
|
|
56
|
+
`@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.3`。不要同时安装两个包:它们共用内部插件 id `model-router-galgame` 与工具名;id 保留使已有路由设置和执行历史能够延续。
|
|
55
57
|
|
|
56
58
|
从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](MIGRATION.md)。
|
|
57
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ljwei-stak/dsh-model-router",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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": {
|