@ljwei-stak/dsh-model-router 0.18.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +177 -39
- package/.dsh-plugin/index.mjs +14 -7
- package/.dsh-plugin/official-tools-remote-service.mjs +7 -3
- package/.dsh-plugin/shared/cli-terminal-protocol.mjs +15 -5
- package/.dsh-plugin/shared/cli-terminal.mjs +126 -18
- package/.dsh-plugin/shared/official-tool-executor.mjs +1 -1
- package/.dsh-plugin/shared/official-tool-registry.mjs +2 -2
- package/.dsh-plugin/shared/official-tools-runtime.mjs +33 -13
- package/.dsh-plugin/shared/tool-install-preferences.mjs +22 -0
- package/.dsh-plugin/shared/zcode-bundle.mjs +29 -8
- package/.dsh-plugin/shared/zcode-installer.mjs +2 -2
- package/CHANGELOG.md +11 -0
- package/README.md +8 -6
- package/README.zh.md +7 -5
- package/docs/INSTALLATION_GUIDE.zh.md +6 -6
- package/docs/WORKBENCH_USER_GUIDE.zh.md +4 -0
- package/package.json +2 -2
|
@@ -44,6 +44,7 @@ const unavailable = () => { throw new Error('模型路由工作台服务尚未
|
|
|
44
44
|
export function installSummary(preferences = installPreferences(), options = currentInstallOptions()) {
|
|
45
45
|
return {
|
|
46
46
|
installDir: preferences.installDir ?? '',
|
|
47
|
+
zcodeInstallDir: preferences.zcodeInstallDir ?? '',
|
|
47
48
|
registry: preferences.registry ?? '',
|
|
48
49
|
allowScriptInstall: preferences.allowScriptInstall !== false,
|
|
49
50
|
scriptUrls: { ...(preferences.scriptUrls ?? {}) },
|
|
@@ -61,12 +62,14 @@ export function installSummary(preferences = installPreferences(), options = cur
|
|
|
61
62
|
}
|
|
62
63
|
if (tool.manager === 'signed-windows-installer') {
|
|
63
64
|
entry.methodId = 'signed-windows-installer'
|
|
65
|
+
entry.installDir = preferences.zcodeInstallDir ?? ''
|
|
64
66
|
if (options.platform === 'win32') {
|
|
65
67
|
entry.command = installCommandLine(tool)
|
|
66
68
|
entry.updateCommand = entry.command
|
|
67
|
-
entry.notices = [preferences.
|
|
68
|
-
?
|
|
69
|
-
: '
|
|
69
|
+
entry.notices = [preferences.zcodeInstallDir
|
|
70
|
+
? `TUI 使用已保存的 ZCode 桌面目录 ${preferences.zcodeInstallDir}。更新时请在官方安装窗口选择同一目录,完成后重新体检。`
|
|
71
|
+
: '请在下方单独配置 ZCode 桌面目录,或留空自动检测。内置 CLI 随桌面版更新,TUI 的登录、模型和插件在终端中自行配置。']
|
|
72
|
+
entry.notices.push('ZCode 桌面目录独立于统一 npm 安装根;不会把桌面资源放进其他工具的共享目录。')
|
|
70
73
|
} else entry.installError = entry.error = 'ZCode 签名安装器仅支持 Windows。'
|
|
71
74
|
entry.uninstallError = 'ZCode 是桌面安装版,请在系统“应用”中卸载它。'
|
|
72
75
|
return entry
|
|
@@ -191,6 +194,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
191
194
|
// The saved input can contain Host-only home/environment expansions. Keep
|
|
192
195
|
// it separate from the resolved directory used by the actual command.
|
|
193
196
|
install.requestedInstallDir = this.services?.installRequestedDirectory?.() ?? install.installDir
|
|
197
|
+
install.requestedZCodeInstallDir = this.services?.zcodeRequestedDirectory?.() ?? install.zcodeInstallDir
|
|
194
198
|
return { install, hostVersion: HOST_PLUGIN_VERSION }
|
|
195
199
|
}
|
|
196
200
|
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* into that interactive process, exactly as in their own terminal, so a
|
|
10
10
|
* session starts only after an explicit confirmation.
|
|
11
11
|
*/
|
|
12
|
-
import { getOfficialTool } from './official-tool-registry.mjs'
|
|
12
|
+
import { getOfficialTool, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
export const TERMINAL_TOOL_IDS = Object.freeze(
|
|
14
|
+
/** Follow the tool registry, including the TUI bundled with ZCode desktop. */
|
|
15
|
+
export const TERMINAL_TOOL_IDS = Object.freeze(OFFICIAL_TOOLS
|
|
16
|
+
.filter(tool => !tool.unsupported && (tool.probeExecutables?.length || tool.manager === 'signed-windows-installer'))
|
|
17
|
+
.map(tool => tool.id))
|
|
16
18
|
export const TERMINAL_SHELL_ID = 'shell'
|
|
17
19
|
export const TERMINAL_TARGETS = Object.freeze([TERMINAL_SHELL_ID, ...TERMINAL_TOOL_IDS])
|
|
18
20
|
export const TERMINAL_MODES = Object.freeze(['interactive', 'login'])
|
|
@@ -46,8 +48,16 @@ export const TERMINAL_LOGIN_ARGS = Object.freeze({
|
|
|
46
48
|
'mimo-code': Object.freeze(['auth', 'login']),
|
|
47
49
|
'grok-build': Object.freeze(['login']),
|
|
48
50
|
gemini: Object.freeze([]),
|
|
51
|
+
opencode: Object.freeze(['auth', 'login']),
|
|
52
|
+
stepcode: Object.freeze(['login']),
|
|
53
|
+
zcode: Object.freeze([]),
|
|
49
54
|
})
|
|
50
55
|
|
|
56
|
+
/** Tools without a separate login command authenticate from their TUI. */
|
|
57
|
+
export function terminalCanLogin(target) {
|
|
58
|
+
return Boolean(TERMINAL_LOGIN_ARGS[target]?.length)
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
const SESSION_ID = /^term-[a-z0-9]{6,40}$/
|
|
52
62
|
|
|
53
63
|
function plainObject(value, subject) {
|
|
@@ -82,7 +92,7 @@ export function terminalTargetLabel(target) {
|
|
|
82
92
|
export function terminalCommandPreview(target, mode = 'interactive', platform = 'win32') {
|
|
83
93
|
if (target === TERMINAL_SHELL_ID) return platform === 'win32' ? 'powershell.exe -NoLogo' : '$SHELL -l'
|
|
84
94
|
const tool = getOfficialTool(target)
|
|
85
|
-
const executable = tool?.probeExecutables?.[0] ?? String(target)
|
|
95
|
+
const executable = target === 'zcode' ? 'ZCode 桌面版内置 TUI' : tool?.probeExecutables?.[0] ?? String(target)
|
|
86
96
|
const args = mode === 'login' ? TERMINAL_LOGIN_ARGS[target] ?? [] : []
|
|
87
97
|
return [executable, ...args].join(' ')
|
|
88
98
|
}
|
|
@@ -92,7 +102,7 @@ export function parseTerminalStart(value) {
|
|
|
92
102
|
if (!TERMINAL_TARGETS.includes(request.target)) throw new TypeError('target must be the shell or a fixed official CLI')
|
|
93
103
|
const mode = request.mode === undefined ? 'interactive' : request.mode
|
|
94
104
|
if (!TERMINAL_MODES.includes(mode)) throw new TypeError('mode must be interactive or login')
|
|
95
|
-
if (mode === 'login' && request.target
|
|
105
|
+
if (mode === 'login' && !terminalCanLogin(request.target)) throw new TypeError('this target has no login mode')
|
|
96
106
|
if (!isAbsoluteDirectoryText(request.cwd)) throw new TypeError('cwd must be an absolute directory path')
|
|
97
107
|
if (request.confirmed !== true) throw new TypeError('starting a terminal requires the user\'s confirmation')
|
|
98
108
|
return {
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
15
|
import { randomBytes } from 'node:crypto'
|
|
16
|
-
import { stat } from 'node:fs/promises'
|
|
16
|
+
import { readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
17
17
|
import { createRequire } from 'node:module'
|
|
18
18
|
import { StringDecoder } from 'node:string_decoder'
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { extname, isAbsolute, join, relative, sep } from 'node:path'
|
|
21
|
+
import { getOfficialTool, installMethodFor } from './official-tool-registry.mjs'
|
|
22
|
+
import { resolveInstallPlan } from './tool-install-preferences.mjs'
|
|
23
|
+
import { discoverZCodeBundle } from './zcode-bundle.mjs'
|
|
21
24
|
import {
|
|
22
25
|
TERMINAL_LIMITS,
|
|
23
26
|
TERMINAL_LOGIN_ARGS,
|
|
@@ -84,6 +87,75 @@ export function pickWindowsExecutable(candidates) {
|
|
|
84
87
|
|
|
85
88
|
const quoteWindows = path => `"${String(path).replace(/"/g, '')}"`
|
|
86
89
|
|
|
90
|
+
function within(root, child) {
|
|
91
|
+
const part = relative(root, child)
|
|
92
|
+
return Boolean(part) && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Bind a configured npm/pnpm tool to its declared package entry, not a shared shim. */
|
|
96
|
+
export async function findTerminalPackageEntry(tool, plan, { platform = process.platform } = {}) {
|
|
97
|
+
let roots = plan.packageModuleRoots
|
|
98
|
+
let boundary
|
|
99
|
+
if (!Array.isArray(roots)) {
|
|
100
|
+
if (!plan.installDir) return null
|
|
101
|
+
if (plan.manager === 'pnpm') {
|
|
102
|
+
const global = join(plan.installDir, '.pnpm', 'global')
|
|
103
|
+
boundary = await realpath(global).catch(() => null)
|
|
104
|
+
const versions = await readdir(global, { withFileTypes: true }).catch(() => [])
|
|
105
|
+
roots = versions.filter(item => item.isDirectory() && /^\d+$/.test(item.name))
|
|
106
|
+
.sort((left, right) => Number(left.name) - Number(right.name))
|
|
107
|
+
.map(item => join(global, item.name, 'node_modules'))
|
|
108
|
+
} else {
|
|
109
|
+
const modules = platform === 'win32' ? join(plan.installDir, 'node_modules') : join(plan.installDir, 'lib', 'node_modules')
|
|
110
|
+
boundary = await realpath(modules).catch(() => null)
|
|
111
|
+
roots = [modules]
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
boundary = await realpath(plan.globalDir || plan.installDir || roots[0]).catch(() => null)
|
|
115
|
+
}
|
|
116
|
+
if (!boundary) return null
|
|
117
|
+
const entries = []
|
|
118
|
+
for (const modules of roots) {
|
|
119
|
+
try {
|
|
120
|
+
const packageRoot = await realpath(join(modules, ...tool.package.split('/')))
|
|
121
|
+
if (!within(boundary, packageRoot)) continue
|
|
122
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
|
|
123
|
+
if (manifest.name !== tool.package) continue
|
|
124
|
+
for (const executable of tool.probeExecutables) {
|
|
125
|
+
const raw = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[executable]
|
|
126
|
+
const bin = typeof raw === 'string' ? raw.replace(/\\/g, '/').replace(/^\.\//, '') : ''
|
|
127
|
+
if (!bin || bin.split('/').some(part => !part || part === '..') || isAbsolute(bin)) continue
|
|
128
|
+
const entry = await realpath(join(packageRoot, ...bin.split('/')))
|
|
129
|
+
if (!within(packageRoot, entry) || !(await stat(entry)).isFile()) continue
|
|
130
|
+
entries.push({ entry, packageRoot, version: String(manifest.version ?? '') })
|
|
131
|
+
}
|
|
132
|
+
} catch { /* not the selected tool's usable package */ }
|
|
133
|
+
}
|
|
134
|
+
// pnpm normally has one active global tree. Prefer the highest numbered
|
|
135
|
+
// layout when multiple layouts survive an upgrade, following its directories.
|
|
136
|
+
return entries.at(-1) ?? null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function terminalNode(workspace, minimum, acceptedVersion) {
|
|
140
|
+
const { findNodeExecutable } = await import('./official-tool-executor.mjs')
|
|
141
|
+
return findNodeExecutable(workspace, minimum, acceptedVersion)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function executableLaunch(file, args, display, { platform, env }) {
|
|
145
|
+
if (platform !== 'win32') return { file, args, verbatim: false, display }
|
|
146
|
+
const extension = extname(file).toLowerCase()
|
|
147
|
+
if (extension === '.cmd' || extension === '.bat') {
|
|
148
|
+
const comspec = env.ComSpec || env.COMSPEC || 'cmd.exe'
|
|
149
|
+
const tail = `/d /s /c "${[quoteWindows(file), ...args].join(' ')}"`
|
|
150
|
+
return { file: comspec, args: tail, verbatim: true, display }
|
|
151
|
+
}
|
|
152
|
+
if (extension === '.ps1') {
|
|
153
|
+
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows'
|
|
154
|
+
return { file: `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, args: ['-NoLogo', '-NoProfile', '-File', file, ...args], verbatim: false, display }
|
|
155
|
+
}
|
|
156
|
+
return { file, args, verbatim: false, display }
|
|
157
|
+
}
|
|
158
|
+
|
|
87
159
|
/**
|
|
88
160
|
* Build the fixed launch for one target. `locate(executable)` returns the
|
|
89
161
|
* candidate paths `where`/`which` printed. Returns
|
|
@@ -92,6 +164,9 @@ const quoteWindows = path => `"${String(path).replace(/"/g, '')}"`
|
|
|
92
164
|
*/
|
|
93
165
|
export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
94
166
|
platform = process.platform, env = process.env, locate = defaultLocate,
|
|
167
|
+
preferences = {}, home = homedir(), cwd = process.cwd(),
|
|
168
|
+
packageEntry = findTerminalPackageEntry, fileExists = async path => (await stat(path).catch(() => null))?.isFile() === true,
|
|
169
|
+
discoverBundle = discoverZCodeBundle, findNode = terminalNode,
|
|
95
170
|
} = {}) {
|
|
96
171
|
if (target === TERMINAL_SHELL_ID) {
|
|
97
172
|
if (platform === 'win32') {
|
|
@@ -104,9 +179,47 @@ export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
|
104
179
|
}
|
|
105
180
|
if (!TERMINAL_TOOL_IDS.includes(target)) throw new Error('只能启动系统终端或固定注册表中的官方 CLI。')
|
|
106
181
|
const tool = getOfficialTool(target)
|
|
182
|
+
if (target === 'zcode') {
|
|
183
|
+
if (platform !== 'win32') throw new Error('当前仅支持 Windows ZCode 桌面版内置 TUI。')
|
|
184
|
+
const bundle = await discoverBundle({ installDir: preferences.zcodeInstallDir, env })
|
|
185
|
+
if (!bundle) throw new Error('未找到通过智谱签名与组件核验的 ZCode 桌面版;请先配置 ZCode 独立安装目录并重新体检。')
|
|
186
|
+
const node = await findNode(cwd, [24, 14, 0], version => Number(String(version).split('.')[0]) === 24)
|
|
187
|
+
// The verified desktop's electron-node entry is usable independently of
|
|
188
|
+
// its GUI. It supplies its own runtime when external Node 24 is absent.
|
|
189
|
+
return { file: node || bundle.executable, args: [bundle.cliEntry], verbatim: false,
|
|
190
|
+
display: `ZCode TUI · ${bundle.cliEntry}`, ...(!node ? { env: { ELECTRON_RUN_AS_NODE: '1' } } : {}) }
|
|
191
|
+
}
|
|
107
192
|
const executable = tool.probeExecutables[0]
|
|
108
193
|
const args = mode === 'login' ? [...(TERMINAL_LOGIN_ARGS[target] ?? [])] : []
|
|
109
194
|
const display = [executable, ...args].join(' ')
|
|
195
|
+
const selectedScript = installMethodFor(tool, preferences.methods?.[target])?.kind === 'script'
|
|
196
|
+
const selectedPreferences = selectedScript ? { ...preferences, allowScriptInstall: true } : preferences
|
|
197
|
+
const plan = resolveInstallPlan(tool, undefined, selectedPreferences, { platform, env, home })
|
|
198
|
+
const configured = Boolean(preferences.installDir || preferences.methods?.[target])
|
|
199
|
+
if (plan.kind === 'package-manager' && configured) {
|
|
200
|
+
const boundPlan = plan.installDir ? plan : await (await import('./official-tools-runtime.mjs')).resolvePackageJobPlan(tool, plan, selectedPreferences, { options: { platform, env, home } })
|
|
201
|
+
const found = await packageEntry(tool, boundPlan, { platform })
|
|
202
|
+
if (!found) throw new Error(`未在所选 ${boundPlan.manager} 安装目录中找到 ${tool.label} 的真实命令入口;请重新体检或修复。`)
|
|
203
|
+
const extension = extname(found.entry).toLowerCase()
|
|
204
|
+
if (['.js', '.mjs', '.cjs'].includes(extension) || (platform === 'win32' && !extension)) {
|
|
205
|
+
const node = await findNode(cwd)
|
|
206
|
+
if (!node) throw new Error(`未找到可核验的 Node.js 运行时,无法启动 ${tool.label}。`)
|
|
207
|
+
return { file: node, args: [found.entry, ...args], verbatim: false, display }
|
|
208
|
+
}
|
|
209
|
+
return executableLaunch(found.entry, args, display, { platform, env })
|
|
210
|
+
}
|
|
211
|
+
if (plan.kind === 'script' && plan.binDir) {
|
|
212
|
+
for (const name of tool.probeExecutables) {
|
|
213
|
+
const filenames = platform === 'win32' ? WINDOWS_PREFERENCE.map(extension => `${name}${extension}`) : [name]
|
|
214
|
+
for (const filename of filenames) {
|
|
215
|
+
const file = join(plan.binDir, filename)
|
|
216
|
+
if (await fileExists(file)) return executableLaunch(file, args, display, { platform, env })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// A saved method or directory is authoritative: an older PATH copy cannot
|
|
220
|
+
// satisfy a missing install in the user's selected tool directory.
|
|
221
|
+
if (configured || tool.manager === 'script-installer') throw new Error(`未在所选安装目录 ${plan.binDir} 中找到 ${tool.label};请重新体检或安装。`)
|
|
222
|
+
}
|
|
110
223
|
const candidates = await locate(executable, { platform, env })
|
|
111
224
|
if (platform !== 'win32') {
|
|
112
225
|
const file = candidates.map(item => String(item).trim()).find(item => item.startsWith('/'))
|
|
@@ -115,18 +228,7 @@ export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
|
115
228
|
}
|
|
116
229
|
const file = pickWindowsExecutable(candidates)
|
|
117
230
|
if (!file) throw new Error(`未在 PATH 中找到 ${executable};请先在“官方工具 · 体检”中安装 ${tool.label}。`)
|
|
118
|
-
|
|
119
|
-
if (extension === '.cmd' || extension === '.bat') {
|
|
120
|
-
// npm shims only run under cmd.exe. Every token is fixed by the registry.
|
|
121
|
-
const comspec = env.ComSpec || env.COMSPEC || 'cmd.exe'
|
|
122
|
-
const tail = `/d /s /c "${[quoteWindows(file), ...args].join(' ')}"`
|
|
123
|
-
return { file: comspec, args: tail, verbatim: true, display }
|
|
124
|
-
}
|
|
125
|
-
if (extension === '.ps1') {
|
|
126
|
-
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows'
|
|
127
|
-
return { file: `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, args: ['-NoLogo', '-NoProfile', '-File', file, ...args], verbatim: false, display }
|
|
128
|
-
}
|
|
129
|
-
return { file, args, verbatim: false, display }
|
|
231
|
+
return executableLaunch(file, args, display, { platform, env })
|
|
130
232
|
}
|
|
131
233
|
|
|
132
234
|
/** Locate an executable with where.exe / which (fixed registry names only). */
|
|
@@ -161,7 +263,10 @@ function spawnPtyProcess(pty, launch, { cwd, cols, rows, env }) {
|
|
|
161
263
|
|
|
162
264
|
/** Pipe fallback: no TTY; the Host echoes input and maps Enter/Ctrl+C. */
|
|
163
265
|
export function spawnPipeProcess(launch, { cwd, env, platform = process.platform, spawnImpl = spawn }) {
|
|
164
|
-
|
|
266
|
+
// node-pty accepts a Windows command-line string; node:child_process needs
|
|
267
|
+
// an array even with windowsVerbatimArguments. Keep the prepared tail intact.
|
|
268
|
+
const args = typeof launch.args === 'string' ? [launch.args] : launch.args
|
|
269
|
+
const child = spawnImpl(launch.file, args, {
|
|
165
270
|
cwd, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, shell: false,
|
|
166
271
|
windowsVerbatimArguments: launch.verbatim === true,
|
|
167
272
|
})
|
|
@@ -220,6 +325,8 @@ export function createTerminalManager({
|
|
|
220
325
|
statPath = stat,
|
|
221
326
|
prepare = async () => {},
|
|
222
327
|
environment = () => terminalEnvironment(process.env),
|
|
328
|
+
preferences = () => ({}),
|
|
329
|
+
launchOptions = () => ({}),
|
|
223
330
|
onSessionEnd = () => {},
|
|
224
331
|
now = Date.now,
|
|
225
332
|
platform = process.platform,
|
|
@@ -329,10 +436,11 @@ export function createTerminalManager({
|
|
|
329
436
|
try { info = await statPath(request.cwd) } catch { info = null }
|
|
330
437
|
if (!info?.isDirectory?.()) throw new Error(`工作目录不存在或不是目录:${request.cwd}`)
|
|
331
438
|
await prepare()
|
|
332
|
-
const launch = await resolveLaunch(request.target, request.mode, { platform })
|
|
333
439
|
const env = environment()
|
|
440
|
+
const launch = await resolveLaunch(request.target, request.mode, { ...launchOptions(), platform, env,
|
|
441
|
+
preferences: preferences(), cwd: request.cwd })
|
|
334
442
|
const backend = backendInfo()
|
|
335
|
-
const options = { cwd: request.cwd, cols: request.cols, rows: request.rows, env, platform }
|
|
443
|
+
const options = { cwd: request.cwd, cols: request.cols, rows: request.rows, env: { ...env, ...launch.env }, platform }
|
|
336
444
|
let proc
|
|
337
445
|
try {
|
|
338
446
|
proc = backend.backend === 'pty'
|
|
@@ -586,7 +586,7 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
|
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
if (toolId === 'zcode') {
|
|
589
|
-
const found = await discoverZCodeBundle()
|
|
589
|
+
const found = await discoverZCodeBundle({ installDir: (selectionOptions?.preferences ?? installPreferences()).zcodeInstallDir })
|
|
590
590
|
if (!found) return { unsupported: '未找到具有智谱有效签名、且 CLI 脚本与首次核验记录一致的 ZCode 桌面版。' }
|
|
591
591
|
const nodeExecutable = await findNodeExecutable(workspace, [24, 14, 0], version =>
|
|
592
592
|
Number(String(version).split('.')[0]) === 24)
|
|
@@ -405,12 +405,12 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
405
405
|
id: 'zcode',
|
|
406
406
|
label: 'ZCode',
|
|
407
407
|
vendor: 'Z.ai',
|
|
408
|
-
purpose: '智谱官方 ZCode
|
|
408
|
+
purpose: '智谱官方 ZCode 桌面版,内含可独立运行的 GLM CLI/TUI。可单独配置桌面目录,内置 CLI 随桌面版更新。',
|
|
409
409
|
manager: 'signed-windows-installer',
|
|
410
410
|
installArgs: [],
|
|
411
411
|
uninstallArgs: [],
|
|
412
412
|
probeExecutables: [],
|
|
413
|
-
probeNote: '检测经过有效签名的 ZCode.exe 和同目录 GLM 资源;桌面安装器需人工选择安装位置。',
|
|
413
|
+
probeNote: '检测经过有效签名的 ZCode.exe 和同目录 GLM CLI/TUI 资源;桌面安装器需人工选择安装位置。',
|
|
414
414
|
providerHints: ['zai', 'z.ai', 'zcode', 'glm', 'zhipu', 'bigmodel'],
|
|
415
415
|
}),
|
|
416
416
|
])
|
|
@@ -60,7 +60,7 @@ export const CLAUDE_UPDATE_ARGS = Object.freeze(['update'])
|
|
|
60
60
|
* and script source). Refreshed on apply and live config updates, so a client
|
|
61
61
|
* can only ever pick a method id — these addresses never arrive over the wire.
|
|
62
62
|
*/
|
|
63
|
-
let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
|
|
63
|
+
let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true, zcodeInstallDir: '' }
|
|
64
64
|
|
|
65
65
|
export function setInstallPreferences(next) {
|
|
66
66
|
if (next && typeof next === 'object') {
|
|
@@ -868,10 +868,13 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
|
|
|
868
868
|
}
|
|
869
869
|
if (tool.manager === 'signed-windows-installer') {
|
|
870
870
|
let rejected = null
|
|
871
|
-
const bundle = await discoverZCodeBundle({
|
|
871
|
+
const bundle = await discoverZCodeBundle({ installDir: (selectedPreferences ?? preferences).zcodeInstallDir,
|
|
872
|
+
onRejected: value => { rejected ??= value } })
|
|
872
873
|
if (bundle) return { id: tool.id, installed: true, version: bundle.version, status: 'installed',
|
|
873
|
-
detail: `${tool.probeNote} 安装目录:${bundle.root}`, bannerLine: `${tool.label} ${bundle.buildVersion}
|
|
874
|
-
|
|
874
|
+
detail: `${tool.probeNote} 安装目录:${bundle.root}`, bannerLine: `${tool.label} 桌面版 ${bundle.buildVersion}`,
|
|
875
|
+
installDir: bundle.root, cliEntry: bundle.cliEntry, source: bundle.source }
|
|
876
|
+
if (rejected) return { id: tool.id, installed: false, version: null, status: 'not-installed',
|
|
877
|
+
detail: rejected.version ? `检测到 ZCode ${rejected.version}(${rejected.root}):${rejected.rejected}` : rejected.rejected }
|
|
875
878
|
const other = (await unverifiedZCodeInstalls().catch(() => []))[0]
|
|
876
879
|
return { id: tool.id, installed: false, version: null, status: 'not-installed',
|
|
877
880
|
detail: other
|
|
@@ -1589,30 +1592,43 @@ async function runInstallJob(installPlan, job) {
|
|
|
1589
1592
|
await finishVerifiedInstall(job, probe)
|
|
1590
1593
|
}
|
|
1591
1594
|
|
|
1592
|
-
async function runZCodeInstallJob(job
|
|
1595
|
+
export async function runZCodeInstallJob(job, {
|
|
1596
|
+
probe = probeToolWith, lookup = (...args) => latestVersions.lookup(...args),
|
|
1597
|
+
openInstaller = openZCodeInstaller, capture = runCapture,
|
|
1598
|
+
} = {}) {
|
|
1593
1599
|
if (job.status !== 'running') return
|
|
1594
1600
|
job.phase = 'preflight'
|
|
1595
1601
|
probeCache.delete(job.tool.id)
|
|
1596
|
-
const before = await
|
|
1602
|
+
const before = await probe(job.tool, defaultRunner, { preferences: job.preferences, cacheMs: 0 })
|
|
1597
1603
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
1598
|
-
const latest = await
|
|
1604
|
+
const latest = await lookup(job.tool, { fresh: true }).catch(() => null)
|
|
1599
1605
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
1600
|
-
|
|
1606
|
+
const desktopOrder = before.installed && latest?.version ? compareReleaseVersions(before.version, latest.version) : null
|
|
1607
|
+
if (before.installed && desktopOrder === null) {
|
|
1608
|
+
job.status = 'failed'
|
|
1609
|
+
job.phase = 'done'
|
|
1610
|
+
job.finishedAt = new Date().toISOString()
|
|
1611
|
+
job.error = '无法核实 ZCode 最新桌面版本;保留当前安装,网络恢复后重试检查更新。'
|
|
1612
|
+
return
|
|
1613
|
+
}
|
|
1614
|
+
if (desktopOrder === 1 || (job.operation !== 'repair' && desktopOrder === 0)) {
|
|
1601
1615
|
job.status = 'succeeded'
|
|
1602
1616
|
job.phase = 'done'
|
|
1603
1617
|
job.finishedAt = new Date().toISOString()
|
|
1604
1618
|
job.postInstallProbe = before
|
|
1605
|
-
pushLine(job,
|
|
1619
|
+
pushLine(job, desktopOrder === 1
|
|
1620
|
+
? `— ZCode ${before.version} 高于下载页的 ${latest.version},不会打开旧版安装器降级;TUI 使用当前桌面内置入口 —`
|
|
1621
|
+
: `— 已找到 ZCode ${before.version}(最新 ${latest.version}),无需重复打开安装器;TUI 使用当前桌面内置入口 —`)
|
|
1606
1622
|
return
|
|
1607
1623
|
}
|
|
1608
|
-
const prefix = await
|
|
1624
|
+
const prefix = await capture('npm', ['config', 'get', 'prefix'], {
|
|
1609
1625
|
timeoutMs: 5_000, useShell: IS_WINDOWS, signal: job.controller.signal,
|
|
1610
1626
|
})
|
|
1611
1627
|
if (job.cancelRequested) { markCancelled(job); return }
|
|
1612
1628
|
job.phase = 'downloading-and-verifying'
|
|
1613
1629
|
let lastProgress = 0
|
|
1614
1630
|
try {
|
|
1615
|
-
const opened = await
|
|
1631
|
+
const opened = await openInstaller({
|
|
1616
1632
|
latest,
|
|
1617
1633
|
npmPrefix: prefix.ok ? prefix.stdout.trim() : undefined,
|
|
1618
1634
|
signal: job.controller.signal,
|
|
@@ -1629,7 +1645,11 @@ async function runZCodeInstallJob(job) {
|
|
|
1629
1645
|
job.finishedAt = new Date().toISOString()
|
|
1630
1646
|
probeCache.delete(job.tool.id)
|
|
1631
1647
|
pushLine(job, `已验证发布者签名并打开 ZCode ${opened.version} 官方安装器:${opened.installerPath}`)
|
|
1632
|
-
|
|
1648
|
+
const desktopDirectory = before.installDir || (/[\\/]ZCode\.exe$/i.test(job.preferences.zcodeInstallDir ?? '')
|
|
1649
|
+
? dirname(job.preferences.zcodeInstallDir) : job.preferences.zcodeInstallDir)
|
|
1650
|
+
pushLine(job, desktopDirectory
|
|
1651
|
+
? `请在原厂安装界面选择原桌面安装目录 ${desktopDirectory};完成后重新体检,内置 TUI 随桌面版更新。`
|
|
1652
|
+
: '请在原厂安装界面选择独立的 ZCode 桌面目录;完成后保存该目录并重新体检,内置 TUI 随桌面版更新。')
|
|
1633
1653
|
} catch (error) {
|
|
1634
1654
|
if (job.cancelRequested || job.controller.signal.aborted) { markCancelled(job); return }
|
|
1635
1655
|
job.status = 'failed'
|
|
@@ -1700,5 +1720,5 @@ export function resetForTests() {
|
|
|
1700
1720
|
installJobs.clear()
|
|
1701
1721
|
installChain = Promise.resolve()
|
|
1702
1722
|
prefixPromise = null
|
|
1703
|
-
preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
|
|
1723
|
+
preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true, zcodeInstallDir: '' }
|
|
1704
1724
|
}
|
|
@@ -70,6 +70,26 @@ export function expandInstallDir(value, { home = '', env = {} } = {}) {
|
|
|
70
70
|
return expanded
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** ZCode's existing desktop bundle is selected independently of npm's prefix. */
|
|
74
|
+
export function normalizeZCodeInstallDir(value, options = {}) {
|
|
75
|
+
const input = String(value ?? '')
|
|
76
|
+
if (/[\x00-\x1f]/.test(input)) throw new Error('ZCode 安装位置包含无效字符。')
|
|
77
|
+
const raw = input.trim().replace(/^"(.*)"$/, '$1')
|
|
78
|
+
if (!raw) return ''
|
|
79
|
+
let path
|
|
80
|
+
try { path = expandInstallDir(raw, options) } catch (error) {
|
|
81
|
+
throw new Error(`ZCode 安装位置无效:${error.message}`)
|
|
82
|
+
}
|
|
83
|
+
// Discovery and Authenticode verification operate on local Windows files.
|
|
84
|
+
if (!/^[A-Za-z]:[\\/]/.test(path)) {
|
|
85
|
+
throw new Error('ZCode 安装位置必须是 Windows 本地绝对目录或 ZCode.exe 路径。')
|
|
86
|
+
}
|
|
87
|
+
if (/\.exe[\\/]*$/i.test(path) && !/[\\/]ZCode\.exe[\\/]*$/i.test(path)) {
|
|
88
|
+
throw new Error('ZCode 程序路径必须指向官方 ZCode.exe。')
|
|
89
|
+
}
|
|
90
|
+
return path.replace(/[\\/]+$/, '')
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
/** Only https mirrors are accepted; a plain-http script source can be tampered with in transit. */
|
|
74
94
|
export function normalizeSourceUrl(value, subject) {
|
|
75
95
|
const raw = String(value ?? '').trim()
|
|
@@ -116,6 +136,7 @@ function keepKnownToolIds(raw, known, subject) {
|
|
|
116
136
|
export function parseInstallPreferences(config = {}, { tools = [], home = '', env = {} } = {}) {
|
|
117
137
|
const known = new Set(tools.map(tool => tool.id))
|
|
118
138
|
const installDir = expandInstallDir(valueOf(config, 'toolInstallDir', ''), { home, env })
|
|
139
|
+
const zcodeInstallDir = normalizeZCodeInstallDir(valueOf(config, 'zcodeInstallDir', ''), { home, env })
|
|
119
140
|
const registry = normalizeSourceUrl(valueOf(config, 'toolNpmRegistry', DEFAULT_NPM_REGISTRY), 'npm 源') || DEFAULT_NPM_REGISTRY
|
|
120
141
|
const methodRaw = keepKnownToolIds(
|
|
121
142
|
parseJsonObject(valueOf(config, 'toolInstallMethodsJson', '{}'), '安装方式覆盖'), known, '安装方式覆盖')
|
|
@@ -134,6 +155,7 @@ export function parseInstallPreferences(config = {}, { tools = [], home = '', en
|
|
|
134
155
|
}
|
|
135
156
|
return {
|
|
136
157
|
installDir,
|
|
158
|
+
zcodeInstallDir,
|
|
137
159
|
registry,
|
|
138
160
|
methods,
|
|
139
161
|
scriptUrls,
|
|
@@ -46,6 +46,14 @@ function powershellPath(env) {
|
|
|
46
46
|
return join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** PowerShell 7's module path can prevent Windows PowerShell 5.1 loading Security. */
|
|
50
|
+
export function zcodeWindowsPowerShellEnvironment(env = process.env) {
|
|
51
|
+
return {
|
|
52
|
+
...env,
|
|
53
|
+
PSModulePath: join(env.SystemRoot || env.windir || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'Modules'),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
async function powershellJson(script, env, extraEnv = {}) {
|
|
50
58
|
if (process.platform !== 'win32') return null
|
|
51
59
|
const command = Buffer.from(
|
|
@@ -62,7 +70,7 @@ async function powershellJson(script, env, extraEnv = {}) {
|
|
|
62
70
|
encoding: 'utf8',
|
|
63
71
|
timeout: 10_000,
|
|
64
72
|
maxBuffer: 256_000,
|
|
65
|
-
env: { ...env, ...extraEnv },
|
|
73
|
+
env: { ...zcodeWindowsPowerShellEnvironment(env), ...extraEnv },
|
|
66
74
|
})
|
|
67
75
|
const output = stdout.trim().replace(/^\uFEFF/, '')
|
|
68
76
|
return output ? JSON.parse(output) : null
|
|
@@ -94,13 +102,21 @@ function rootFromCommand(value) {
|
|
|
94
102
|
return executable && win32.isAbsolute(executable) ? dirname(executable) : null
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
/** Explicit selection is authoritative, even when the selected bundle is missing. */
|
|
106
|
+
export function zcodeDiscoveryCandidates({ installDir = '', env = process.env, records = [] } = {}) {
|
|
98
107
|
const candidates = []
|
|
99
108
|
const add = (path, source, registryVersion = null) => {
|
|
100
109
|
if (typeof path !== 'string' || !path.trim()) return
|
|
101
110
|
candidates.push({ path: path.trim().replace(/^"|"$/g, ''), source, registryVersion })
|
|
102
111
|
}
|
|
103
|
-
|
|
112
|
+
if (installDir != null && String(installDir).trim()) {
|
|
113
|
+
add(installDir, 'operator-config')
|
|
114
|
+
return candidates
|
|
115
|
+
}
|
|
116
|
+
if (env[ZCODE_INSTALL_ENV_VAR] != null && String(env[ZCODE_INSTALL_ENV_VAR]).trim()) {
|
|
117
|
+
add(env[ZCODE_INSTALL_ENV_VAR], 'operator-env')
|
|
118
|
+
return candidates
|
|
119
|
+
}
|
|
104
120
|
for (const record of records) {
|
|
105
121
|
const version = String(record.displayVersion || '') || null
|
|
106
122
|
add(record.installLocation, 'uninstall-registry', version)
|
|
@@ -204,7 +220,7 @@ async function checkedBundle(candidate, env, trustPath) {
|
|
|
204
220
|
const input = candidate.path
|
|
205
221
|
if (!localWindowsPath(input)) return null
|
|
206
222
|
try {
|
|
207
|
-
const initial =
|
|
223
|
+
const initial = /[\\/]zcode\.exe$/i.test(input) ? dirname(input) : input
|
|
208
224
|
const root = await realpath(initial)
|
|
209
225
|
if (!localWindowsPath(root) || !(await stat(root)).isDirectory()) return null
|
|
210
226
|
const executable = await realpath(join(root, 'ZCode.exe'))
|
|
@@ -239,18 +255,23 @@ async function checkedBundle(candidate, env, trustPath) {
|
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
257
|
|
|
242
|
-
/**
|
|
243
|
-
export async function discoverZCodeBundle({ env = process.env, trustPath = zcodeTrustPath(env), onRejected } = {}) {
|
|
258
|
+
/** A configured desktop location never falls back to a different installation. */
|
|
259
|
+
export async function discoverZCodeBundle({ installDir = '', env = process.env, trustPath = zcodeTrustPath(env), onRejected } = {}) {
|
|
244
260
|
if (process.platform !== 'win32') return null
|
|
245
|
-
const
|
|
261
|
+
const explicit = Boolean(String(installDir || env[ZCODE_INSTALL_ENV_VAR] || '').trim())
|
|
262
|
+
const records = explicit ? [] : await uninstallRecords(env)
|
|
246
263
|
const seen = new Set()
|
|
247
|
-
for (const candidate of
|
|
264
|
+
for (const candidate of zcodeDiscoveryCandidates({ installDir, env, records })) {
|
|
248
265
|
const key = win32.normalize(candidate.path).toLowerCase()
|
|
249
266
|
if (seen.has(key)) continue
|
|
250
267
|
seen.add(key)
|
|
251
268
|
const bundle = await checkedBundle(candidate, env, trustPath)
|
|
252
269
|
if (bundle?.rejected) { onRejected?.(bundle); continue }
|
|
253
270
|
if (bundle) return bundle
|
|
271
|
+
if (explicit) onRejected?.({
|
|
272
|
+
root: candidate.path,
|
|
273
|
+
rejected: `指定的 ZCode 安装位置 ${candidate.path} 不存在,或未通过官方签名、桌面内置 CLI 和目录边界核验;请检查独立安装位置。`,
|
|
274
|
+
})
|
|
254
275
|
}
|
|
255
276
|
return null
|
|
256
277
|
}
|
|
@@ -19,7 +19,7 @@ import { dirname, isAbsolute, join, relative, sep } from 'node:path'
|
|
|
19
19
|
import { Readable, Transform } from 'node:stream'
|
|
20
20
|
import { pipeline } from 'node:stream/promises'
|
|
21
21
|
import { promisify } from 'node:util'
|
|
22
|
-
import { ZCODE_SIGNER } from './zcode-bundle.mjs'
|
|
22
|
+
import { ZCODE_SIGNER, zcodeWindowsPowerShellEnvironment } from './zcode-bundle.mjs'
|
|
23
23
|
import { getOfficialTool } from './official-tool-registry.mjs'
|
|
24
24
|
import { latestVersions } from './latest-versions.mjs'
|
|
25
25
|
|
|
@@ -118,7 +118,7 @@ async function verifyInstallerSignature(path, env) {
|
|
|
118
118
|
encoding: 'utf8',
|
|
119
119
|
timeout: 120_000,
|
|
120
120
|
maxBuffer: 16_384,
|
|
121
|
-
env: { ...env, MODEL_ROUTER_ZCODE_INSTALLER_PATH: path },
|
|
121
|
+
env: { ...zcodeWindowsPowerShellEnvironment(env), MODEL_ROUTER_ZCODE_INSTALLER_PATH: path },
|
|
122
122
|
})
|
|
123
123
|
const signature = JSON.parse(stdout.trim().replace(/^\uFEFF/, ''))
|
|
124
124
|
return signature.status === 'Valid'
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0 — 2026-10-12
|
|
4
|
+
|
|
5
|
+
Stable minor under the npm latest tag. Fully quit Harness (including its tray or WebUI Host process) and restart after upgrading.
|
|
6
|
+
|
|
7
|
+
- Synchronize interactive terminal targets with the complete official-tool registry, adding OpenCode, Step Code and the verified Windows ZCode desktop-bundled TUI. Terminal refresh discovers current installations; older requests and health snapshots cannot restore stale target lists.
|
|
8
|
+
- Resolve terminals from the saved package manager, package entry or private script directory. Refuse to launch a different PATH copy when the selected installation is missing. Fix Windows pipe fallback argument handling for command shims.
|
|
9
|
+
- Add independent ZCode desktop-directory configuration in its tool card and plugin settings. Explicit selection is authoritative; missing, unsigned or modified bundles cannot silently fall back to another desktop. The built-in CLI follows desktop updates, while users configure login, models and plugins in TUI.
|
|
10
|
+
- Offer separate desktop update checks and reinstall controls, preserve reviewed queued-job settings and refuse known downgrades or an unverified latest version. Opening an installer remains distinct from completed installation.
|
|
11
|
+
- Fix ZCode discovery/installer signature checks under inherited PowerShell 7 module paths by using the Windows PowerShell system module directory. Prefer signed external Node 24 for TUI; a verified desktop Electron runtime can run the same bundled entry when Node is unavailable.
|
|
12
|
+
- Add directory, refresh-race, desktop lifecycle and terminal launch regressions. Release notes record full validation and version-only probes of all ten installed CLI entries; no model requests were made.
|
|
13
|
+
|
|
3
14
|
## 0.18.2 — 2026-10-12
|
|
4
15
|
|
|
5
16
|
Stable patch under the npm latest tag. Upgrade and fully restart Harness before using Step install, repair or update.
|
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Model Router · DeepSeek Harness Desktop
|
|
2
2
|
|
|
3
|
-
Version: **0.
|
|
3
|
+
Version: **0.19.0**. See [routing research](docs/ROUTING_RESEARCH.zh.md), [mathematical derivation and experiments](docs/ROUTING_DERIVATION_EXPERIMENTS.zh.md), and the [LiveBench evidence audit and validation protocol](docs/ROUTING_LIVEBENCH_VALIDATION.zh.md). The latest public 2026-06-25 score/cost tables are aggregates; its paired question-quality/cost matrix has not been obtained, and no real routing gain is claimed.
|
|
4
4
|
|
|
5
5
|
A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe a task, compare your configured models, inspect the plan, and explicitly choose when to execute it.
|
|
6
6
|
|
|
7
|
+
**0.19.0:** interactive terminals follow the full registry and saved installation, including OpenCode, Step and the ZCode desktop-bundled TUI. ZCode has independent desktop-directory configuration and desktop update/reinstall controls; its bundled CLI follows desktop updates. Fresh terminal discovery discards stale responses.
|
|
8
|
+
|
|
7
9
|
**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
10
|
|
|
9
11
|
**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.
|
|
@@ -20,7 +22,7 @@ A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe
|
|
|
20
22
|
|
|
21
23
|
**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.
|
|
22
24
|
|
|
23
|
-
Current version: **0.
|
|
25
|
+
Current version: **0.19.0** (`@ljwei-stak/dsh-model-router`). Its declared host compatibility is **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2**. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
|
|
24
26
|
|
|
25
27
|
**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.
|
|
26
28
|
|
|
@@ -38,7 +40,7 @@ For self-testing, follow the [desktop acceptance and quality–cost experiment s
|
|
|
38
40
|
| Read-only execution | Preview and confirm a routed run from the workbench, or invoke it in an official Harness session. Supported headless CLIs and model-directory API calls return recorded results. |
|
|
39
41
|
| Editable CLI execution | One supported official CLI or a sequential CLI team, using approved isolated Git worktrees and conditional patch integration. |
|
|
40
42
|
| Official tools | Detect, install/update, check launch readiness and inspect login status for ten registry tools; installation jobs have logs and cancellation. |
|
|
41
|
-
| Interactive terminal | PowerShell/system shell and
|
|
43
|
+
| Interactive terminal | PowerShell/system shell and all ten registry CLI targets (ZCode TUI on verified Windows desktop installs), including fixed login commands, live output and multiple sessions. |
|
|
42
44
|
| Costs and subscriptions | Daily/monthly budget checks, economy downgrade or pause, subscription-first routing, quota cooldown and explicit handling of other subscription failures. |
|
|
43
45
|
| History and quality | Per-package results, dependency views, failure details, rerun/reassignment, ratings and optional stronger-model review. |
|
|
44
46
|
| Execution boundaries | Shows what each route can read/write and whether its process uses the Harness sandbox. |
|
|
@@ -52,16 +54,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
|
|
|
52
54
|
Open **Plugins → Add plugin** and enter:
|
|
53
55
|
|
|
54
56
|
```text
|
|
55
|
-
@ljwei-stak/dsh-model-router@0.
|
|
57
|
+
@ljwei-stak/dsh-model-router@0.19.0
|
|
56
58
|
```
|
|
57
59
|
|
|
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.
|
|
60
|
+
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.19.0** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
|
|
59
61
|
|
|
60
62
|
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.
|
|
61
63
|
|
|
62
64
|
### Upgrade from the old package name
|
|
63
65
|
|
|
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.
|
|
66
|
+
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.19.0`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
|
|
65
67
|
|
|
66
68
|
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).
|
|
67
69
|
|