@ljwei-stak/dsh-model-router 0.16.3 → 0.17.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.
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Turns the user's plugin settings into one concrete, reviewable install plan.
3
+ *
4
+ * Two separate "addresses" are configurable, and both stay on the Host: a
5
+ * unified install directory and a set of download sources (an npm registry plus
6
+ * per-tool installer-script URLs). The client never sends either — the RPC
7
+ * carries only a registry id and a method id, so a hostile client cannot point
8
+ * this plugin at an arbitrary package or URL. Everything here is Host config
9
+ * the user typed into their own settings form.
10
+ *
11
+ * Script installs are never piped into an interpreter. A plan carries the
12
+ * vendor URL plus the markers that still identify that vendor's script; the
13
+ * runtime downloads it with a size cap, checks the markers, stages a copy and
14
+ * runs that. So a mirror may serve different bytes, but it may not serve a
15
+ * different program.
16
+ *
17
+ * This module is imported by the browser bundle too, so it must stay free of
18
+ * Node built-ins: paths are handled with plain string rules, and the Host passes
19
+ * the home directory in when it needs one.
20
+ */
21
+ import {
22
+ DEFAULT_NPM_REGISTRY,
23
+ defaultInstallMethod,
24
+ installMethodBlockReason,
25
+ installMethodFor,
26
+ installMethodsFor,
27
+ methodHonoursInstallDir,
28
+ } from './official-tool-registry.mjs'
29
+
30
+ export const MAX_INSTALL_DIR_CHARS = 4_096
31
+ export const MAX_SOURCE_URL_CHARS = 2_048
32
+ export const MAX_OVERRIDES = 64
33
+
34
+ /** Windows drive paths, UNC paths, or anything rooted at `/`. */
35
+ export function isAbsolutePath(value) {
36
+ const raw = String(value ?? '')
37
+ if (!raw) return false
38
+ if (raw.startsWith('/')) return true
39
+ if (/^[A-Za-z]:[\\/]/.test(raw)) return true
40
+ return /^\\\\[^\\]+\\[^\\]+/.test(raw)
41
+ }
42
+
43
+ /** Join path segments with the separator the first absolute segment already uses. */
44
+ export function joinPath(base, ...segments) {
45
+ const separator = String(base ?? '').includes('\\') && !String(base ?? '').includes('/') ? '\\' : '/'
46
+ const parts = [String(base ?? '').replace(/[\\/]+$/, ''), ...segments.map(item => String(item ?? '').replace(/^[\\/]+|[\\/]+$/g, ''))]
47
+ return parts.filter(Boolean).join(separator) || separator
48
+ }
49
+
50
+ /** Expand `~` and environment references the way a shell would on this host. */
51
+ export function expandInstallDir(value, { home = '', env = {} } = {}) {
52
+ const raw = String(value ?? '').trim()
53
+ if (!raw) return ''
54
+ if (raw.includes('\0')) throw new Error('安装目录包含无效字符。')
55
+ let expanded = raw
56
+ const userHome = String(home ?? '')
57
+ if (expanded === '~') expanded = userHome
58
+ else if (expanded.startsWith('~/') || expanded.startsWith('~\\')) expanded = joinPath(userHome, expanded.slice(2))
59
+ // `%VAR%` on Windows, `$VAR` elsewhere — both forms the vendor scripts accept.
60
+ expanded = expanded
61
+ .replace(/%([A-Za-z_][A-Za-z0-9_]*)%/g, (match, name) => env[name] ?? match)
62
+ .replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, name) => env[name] ?? match)
63
+ if (expanded.length > MAX_INSTALL_DIR_CHARS) throw new Error('安装目录过长。')
64
+ if (!isAbsolutePath(expanded)) throw new Error('安装目录必须是绝对路径。')
65
+ // A bare root ("/", "D:\\", "\\\\server\\share") would take the machine with it.
66
+ const withoutTrailing = expanded.replace(/[\\/]+$/, '')
67
+ if (!withoutTrailing) throw new Error('安装目录不能是文件系统根目录。')
68
+ if (/^[A-Za-z]:$/.test(withoutTrailing)) throw new Error('安装目录不能是文件系统根目录。')
69
+ if (/^\\\\[^\\]+\\[^\\]+$/.test(withoutTrailing)) throw new Error('安装目录不能是网络共享根目录。')
70
+ return expanded
71
+ }
72
+
73
+ /** Only https mirrors are accepted; a plain-http script source can be tampered with in transit. */
74
+ export function normalizeSourceUrl(value, subject) {
75
+ const raw = String(value ?? '').trim()
76
+ if (!raw) return ''
77
+ if (raw.length > MAX_SOURCE_URL_CHARS) throw new Error(`${subject}地址过长。`)
78
+ let url
79
+ try { url = new URL(raw) } catch { throw new Error(`${subject}不是有效网址。`) }
80
+ if (url.protocol !== 'https:') throw new Error(`${subject}必须使用 https。`)
81
+ if (url.username || url.password) throw new Error(`${subject}不能包含账号密码。`)
82
+ return url.toString()
83
+ }
84
+
85
+ function parseJsonObject(value, subject) {
86
+ const raw = String(value ?? '').trim()
87
+ if (!raw) return {}
88
+ let parsed
89
+ try { parsed = JSON.parse(raw) } catch { throw new Error(`${subject}不是有效 JSON。`) }
90
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
91
+ throw new Error(`${subject}必须是 JSON 对象。`)
92
+ }
93
+ const entries = Object.entries(parsed)
94
+ if (entries.length > MAX_OVERRIDES) throw new Error(`${subject}条目过多。`)
95
+ return Object.fromEntries(entries.filter(([, item]) => typeof item === 'string' && item.trim()))
96
+ }
97
+
98
+ function valueOf(config, key, fallback) {
99
+ const value = config?.[key]
100
+ return value !== undefined && typeof value?.get === 'function' ? value.get() : value ?? fallback
101
+ }
102
+
103
+ /** Registry entries used by the installer are the only keys an override may name. */
104
+ function keepKnownToolIds(raw, known, subject) {
105
+ for (const key of Object.keys(raw)) {
106
+ if (!known.has(key)) throw new Error(`${subject}中的 ${key} 不是官方工具 ID。`)
107
+ }
108
+ return raw
109
+ }
110
+
111
+ /**
112
+ * Read the plugin settings into a normalized, already-validated shape.
113
+ * Throws with a user-facing message on the first bad value, so the settings
114
+ * form and the installer fail the same way.
115
+ */
116
+ export function parseInstallPreferences(config = {}, { tools = [] } = {}) {
117
+ const known = new Set(tools.map(tool => tool.id))
118
+ const installDir = expandInstallDir(valueOf(config, 'toolInstallDir', ''))
119
+ const registry = normalizeSourceUrl(valueOf(config, 'toolNpmRegistry', DEFAULT_NPM_REGISTRY), 'npm 源') || DEFAULT_NPM_REGISTRY
120
+ const methodRaw = keepKnownToolIds(
121
+ parseJsonObject(valueOf(config, 'toolInstallMethodsJson', '{}'), '安装方式覆盖'), known, '安装方式覆盖')
122
+ const scriptRaw = keepKnownToolIds(
123
+ parseJsonObject(valueOf(config, 'toolScriptUrlsJson', '{}'), '安装脚本源覆盖'), known, '安装脚本源覆盖')
124
+ const methods = {}
125
+ const scriptUrls = {}
126
+ for (const [id, methodId] of Object.entries(methodRaw)) methods[id] = methodId.trim()
127
+ for (const [id, url] of Object.entries(scriptRaw)) scriptUrls[id] = normalizeSourceUrl(url, `${id} 安装脚本源`)
128
+ for (const [id, methodId] of Object.entries(methods)) {
129
+ if (!installMethodFor({ id, installMethods: tools.find(tool => tool.id === id)?.installMethods }, methodId)) {
130
+ throw new Error(`${id} 不支持安装方式 ${methodId}。`)
131
+ }
132
+ }
133
+ return {
134
+ installDir,
135
+ registry,
136
+ methods,
137
+ scriptUrls,
138
+ /** Script execution stays a deliberate act; a config file alone must not run one. */
139
+ allowScriptInstall: valueOf(config, 'toolAllowScriptInstall', true) !== false,
140
+ }
141
+ }
142
+
143
+ /** Where a script install puts the binary: the user's directory, else the vendor default. */
144
+ export function scriptInstallDir(tool, preferences, { home = '' } = {}) {
145
+ if (!tool?.defaultScriptInstallDir) return ''
146
+ return preferences?.installDir || vendorScriptDir(tool, { home })
147
+ }
148
+
149
+ /** The location the vendor script uses on its own, ignoring any configured directory. */
150
+ export function vendorScriptDir(tool, { home = '' } = {}) {
151
+ return joinPath(home, ...String(tool?.defaultScriptInstallDir ?? '').split('/').filter(Boolean))
152
+ }
153
+
154
+ /** argv for a package-manager method, honouring the configured registry and prefix. */
155
+ export function packageManagerArgs(method, preferences, { installDir = '' } = {}) {
156
+ const registry = preferences?.registry || DEFAULT_NPM_REGISTRY
157
+ const args = method.installArgs.filter(arg => !arg.startsWith('--registry='))
158
+ args.push(`--registry=${registry}`)
159
+ if (installDir) args.push('--prefix', installDir)
160
+ return [...args]
161
+ }
162
+
163
+ function baseUrlOf(scriptUrl) {
164
+ try {
165
+ const url = new URL(scriptUrl)
166
+ return `${url.origin}${url.pathname.replace(/\/[^/]*$/, '')}`
167
+ } catch { return '' }
168
+ }
169
+
170
+ /**
171
+ * The method to use: the requested one, else the user's saved choice, else the
172
+ * registry default — and if none of those run on this platform, the first
173
+ * declared method that does, so a Windows user is not asked to pick between
174
+ * two Unix shells.
175
+ */
176
+ function pickMethod(tool, methodId, preferences, platform) {
177
+ const requested = methodId ?? preferences?.methods?.[tool.id]
178
+ if (!requested) {
179
+ return installMethodsFor(tool).find(method => !installMethodBlockReason(method, platform)) ?? defaultInstallMethod(tool)
180
+ }
181
+ // A named method is never silently swapped. If the tool does not declare it,
182
+ // or it does not run here, the caller gets the reason instead of a surprise.
183
+ const chosen = installMethodFor(tool, requested)
184
+ if (!chosen) throw new Error(`${tool.label} 没有名为 ${requested} 的安装方式。`)
185
+ return chosen
186
+ }
187
+
188
+ function unsupportedMethodError(tool) {
189
+ return tool?.manager === 'signed-windows-installer'
190
+ ? 'ZCode 使用官方签名桌面安装器,由用户在安装窗口选择目录。'
191
+ : `${tool?.label ?? '此工具'} 没有可用的安装方式。`
192
+ }
193
+
194
+ /**
195
+ * One concrete plan for one tool and one method. A thrown Error means the user
196
+ * asked for something this tool cannot do, with a reason they can act on.
197
+ */
198
+ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform = 'linux', home = '' } = {}) {
199
+ if (!tool || tool.unsupported) throw new Error(tool?.unsupportedReason ?? '此工具不支持一键安装。')
200
+ const method = pickMethod(tool, methodId, preferences, platform)
201
+ if (!method) throw new Error(unsupportedMethodError(tool))
202
+ const blocked = installMethodBlockReason(method, platform)
203
+ if (blocked) throw new Error(`${tool.label}:${blocked}`)
204
+ if (method.kind === 'script' && preferences?.allowScriptInstall === false) {
205
+ throw new Error('安装脚本方式已在插件设置中关闭;请改用包管理器方式,或在设置里重新允许。')
206
+ }
207
+ const installDir = preferences?.installDir || ''
208
+ if (method.kind === 'script') {
209
+ const scriptUrl = preferences?.scriptUrls?.[tool.id] || method.scriptUrl
210
+ const target = methodHonoursInstallDir(method) ? scriptInstallDir(tool, preferences, { home }) : ''
211
+ return {
212
+ toolId: tool.id,
213
+ method,
214
+ methodId: method.id,
215
+ kind: 'script',
216
+ label: method.label,
217
+ shell: method.shell,
218
+ scriptUrl,
219
+ verify: method.verify,
220
+ installArg: method.installArgs,
221
+ installDir: target,
222
+ env: scriptEnv(method, { installDir: target, scriptUrl }),
223
+ notices: target ? [] : installDir
224
+ ? [`${method.label} 无法使用自定义安装目录(官方脚本写死了安装路径),将安装到 ${vendorScriptDir(tool, { home })}。`]
225
+ : [],
226
+ }
227
+ }
228
+ return {
229
+ toolId: tool.id,
230
+ method,
231
+ methodId: method.id,
232
+ kind: 'package-manager',
233
+ label: method.label,
234
+ manager: method.manager,
235
+ args: packageManagerArgs(method, preferences, { installDir }),
236
+ installDir,
237
+ env: {},
238
+ notices: [],
239
+ }
240
+ }
241
+
242
+ /** Same plan shape for removal, so a UI can show exactly what will be deleted. */
243
+ export function resolveUninstallPlan(tool, methodId, preferences = {}, options = {}) {
244
+ const platform = options.platform ?? 'linux'
245
+ const method = pickMethod(tool, methodId, preferences, platform)
246
+ if (!method) throw new Error(tool?.manager === 'signed-windows-installer'
247
+ ? 'ZCode 是桌面安装版,请在系统“应用”中卸载它。'
248
+ : `${tool?.label ?? '此工具'} 没有可用的卸载方式。`)
249
+ const blocked = installMethodBlockReason(method, platform)
250
+ if (blocked) throw new Error(`${tool.label}:${blocked}`)
251
+ if (method.kind === 'script') {
252
+ return { toolId: tool.id, method, methodId: method.id, kind: 'script', label: method.label, ...resolveInstallLocation(tool, method, preferences, options) }
253
+ }
254
+ return {
255
+ toolId: tool.id,
256
+ method,
257
+ methodId: method.id,
258
+ kind: 'package-manager',
259
+ label: method.label,
260
+ manager: method.manager,
261
+ args: [
262
+ ...method.uninstallArgs,
263
+ ...(preferences?.registry ? [`--registry=${preferences.registry}`] : []),
264
+ ...(preferences?.installDir ? ['--prefix', preferences.installDir] : []),
265
+ ],
266
+ installDir: preferences?.installDir || '',
267
+ }
268
+ }
269
+
270
+ /** Where the tool's binary lives, so a later uninstall can find it. */
271
+ export function resolveInstallLocation(tool, method, preferences = {}, { home = '' } = {}) {
272
+ if (!method) return null
273
+ if (method.kind === 'script') {
274
+ // A method that cannot honour the configured directory always lands in the
275
+ // vendor's own location, whatever the user typed.
276
+ const dir = methodHonoursInstallDir(method)
277
+ ? scriptInstallDir(tool, preferences, { home })
278
+ : vendorScriptDir(tool, { home })
279
+ return { kind: 'script', dir, binaries: [...(tool.scriptBinaryNames ?? [])] }
280
+ }
281
+ return { kind: 'package-manager', dir: preferences?.installDir || '', binaries: [] }
282
+ }
283
+
284
+ function scriptEnv(method, { installDir, scriptUrl }) {
285
+ const env = {}
286
+ if (installDir && method.env?.includes('STEP_INSTALL_DIR')) env.STEP_INSTALL_DIR = installDir
287
+ // The step installers read their release base from this variable, so a mirror
288
+ // URL also moves the binaries and the checksum manifest, not just the script.
289
+ if (method.env?.includes('STEP_RELEASE_BASE_URL') && scriptUrl) {
290
+ const base = baseUrlOf(scriptUrl)
291
+ if (base) env.STEP_RELEASE_BASE_URL = base
292
+ }
293
+ return env
294
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0 — 2026-10-11
4
+
5
+ Stable minor under the npm `latest` tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading so the Host reloads. Existing 0.16.4 artifacts remain unchanged.
6
+
7
+ - **Two new official tools.** `opencode` (OpenCode, `opencode-ai` on npm or the official `curl -fsSL … | bash` installer) and `stepcode` (Step Code from StepFun, installed only by the vendor's `curl | bash` / `irm | iex` script — it publishes no npm package). Both are covered by the registry's probe, version lookup, health check and login guidance. Step Code's latest version is read from the same `latest.json` release manifest its own installer uses. Neither has a verified restricted execution entry yet, and both say so rather than claiming one.
8
+ - **Every tool can now declare several install methods.** `installMethods` is the closed list the Host may use: `npm` and `pnpm` everywhere a package exists, plus the vendor installer script where the vendor ships one. The existing `manager` / `installArgs` stay the default method, so nothing about the old one-click path changed.
9
+ - **Unified install settings.** New volatile config: `toolInstallDir`, `toolNpmRegistry`, `toolInstallMethodsJson`, `toolScriptUrlsJson`, `toolAllowScriptInstall`. They are editable from the **官方工具** page and from the plugin settings form, and apply to one-click install, repair and uninstall as well as `/tools` and the agent tools. An install directory becomes `--prefix` for npm/pnpm and `-InstallDir` / `--install-dir` for Step Code. OpenCode's official script hardcodes `~/.opencode/bin` and takes no directory, so for that tool the row says where it will actually land.
10
+ - **Vendor scripts are verified before they run, never piped.** The plugin downloads the script with a size cap, requires the vendor's own markers, refuses an HTML error page or another vendor's installer, writes a staged copy and runs that. A configured mirror may rehost the same script; it may not substitute another program. A Step Code script mirror also redirects that vendor's binary and checksum downloads. Script installs can be switched off entirely.
11
+ - **One-click repair and uninstall.** Repair re-runs the same fixed official command even when the tool already reports the latest version, which is the case a broken install actually presents. Uninstall removes only the program: package-manager installs go back through the same manager, script installs delete the single executable the vendor installer placed plus that install's own `# stepcode` PATH block in the shell profile. Configuration, credentials and session history are always kept. `checkedRemovableFile` re-checks the basename, the install directory and the root/home guards on every delete.
12
+ - **The RPC stays closed.** `installTool`, `uninstallTool` and `repairTool` take one strict `{ tool, method? }` request; an unknown field, an unknown tool id or a method the tool does not declare is refused. Install addresses still come only from Host config, so a compromised client cannot redirect an install. `list()` now also reports the effective install directory, source and per-tool commands so the panel can show what will run.
13
+ - `/tools` accepts `install`, `uninstall`, `repair`, `cancel` and `method`, each with an optional method id. New agent tools `model_router_tool_repair` and `model_router_tool_uninstall`, plus a `method` parameter on `model_router_tool_install`. Install, repair and uninstall each ask for approval, and uninstall's prompt states that settings and login are kept.
14
+ - The workbench panel shows the settings block, a per-row method selector with the platform reason for anything unavailable, the resolved command, and repair/uninstall buttons. Install settings also appear in the plugin settings card with the same client-side validation as the Host.
15
+ - Release validation on Windows 11: 649 tests passed, 3 conditional skips, `npm run build:client` and `npm run check:client` passed. No real vendor CLI was installed, no vendor script was executed, and no paid model call was made as part of this verification.
16
+
17
+ ## 0.16.4 — 2026-10-09
18
+
19
+ 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.
20
+
21
+ - 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.
22
+ - Capture now always records stdout and stderr, then forwards text to `onOutput` when a caller supplied one. Install logs are unchanged.
23
+ - The 0.16.3 spawn shape (no pre-quoted Windows command line) and the 0.16.2 update-button job ownership remain.
24
+ - Linux validation: 631 tests passed, 4 conditional skips, `npm run check:client` passed. This environment did not re-run the Windows PATH probe.
25
+
3
26
  ## 0.16.3 — 2026-10-09
4
27
 
5
28
  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.
@@ -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.3**。本版恢复全部官方工具卡片的检测与安装:Windows 上不再把每条探测和 `npm` 命令预先加引号,单张卡片的失败不会清空整页。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.3` | 模型路由 |
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.3
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.3 / 模型路由**;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.3` 获取同一安装包)
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.3.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.3.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.3`。不要两个同时安装(条目 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.3 沿用该布局。切换页面保留安装任务、终端会话和未完成的输入。
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.3 详情、模型路由入口、目录和模型档案可用;不出现旧合并 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.3 的发布验证见[更新记录](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.3 时检出对应发布标签。旧版(如 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,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness Desktop
2
2
 
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.
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
+
7
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 “修复官方执行入口”.
8
10
 
9
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.
@@ -12,7 +14,7 @@ A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Describe
12
14
 
13
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.
14
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.
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.
16
18
 
17
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.
18
20
 
@@ -44,16 +46,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
44
46
  Open **Plugins → Add plugin** and enter:
45
47
 
46
48
  ```text
47
- @ljwei-stak/dsh-model-router@0.16.3
49
+ @ljwei-stak/dsh-model-router@0.16.4
48
50
  ```
49
51
 
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.
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.
51
53
 
52
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.
53
55
 
54
56
  ### Upgrade from the old package name
55
57
 
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.
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.
57
59
 
58
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).
59
61
 
@@ -141,12 +143,22 @@ The CLI team runner is the plugin's own sequential dependency executor. It does
141
143
  | `mimo-code` | MiMo Code | `read-only`, approved `workspace-write` |
142
144
  | `grok-build` | Grok Build | `read-only`, approved `workspace-write` |
143
145
  | `gemini` | Gemini CLI | Routed headless adapter; not the managed single-tool/team runner |
146
+ | `opencode` | OpenCode | Install, probe and login check; no verified restricted execution entry yet |
147
+ | `stepcode` | Step Code | Install, probe and login check; no verified restricted execution entry yet |
144
148
  | `zcode` | ZCode | Windows; approved `workspace-write` |
145
149
 
146
- Installers use fixed official npm packages at each vendor's `@latest`, or the official publisher-signed Windows ZCode installer. An installation newer than the reported latest is not downgraded. Latest-version lookups are cached for about 12 hours; unavailable lookups leave the version unknown. New vendor releases are not automatically validated by this project. ZCode's installation window still requires you to choose a directory and finish installation.
150
+ Installers use fixed official npm packages at each vendor's `@latest`, the vendor's own installer script, or the official publisher-signed Windows ZCode installer. An installation newer than the reported latest is not downgraded. Latest-version lookups are cached for about 12 hours; unavailable lookups leave the version unknown. New vendor releases are not automatically validated by this project. ZCode's installation window still requires you to choose a directory and finish installation.
151
+
152
+ **Unified install settings.** The top of the **官方工具** page (also in the plugin settings form) sets the install address for every tool at once: one install directory (empty means each vendor's default location), an npm source, and per-tool overrides for vendor installer-script mirrors. Directories must be absolute; sources must be https. Each row picks its own install method from `npm`, `pnpm` or the vendor script (`curl | bash` or `irm | iex`); a method that cannot run on this platform says so on the row instead of being silently swapped. The exact command is shown above the buttons before anything runs.
153
+
154
+ **Script installs never pipe remote content into an interpreter.** The plugin downloads the vendor script, checks it is still that vendor's official installer (vendor markers required, HTML pages and other vendors' scripts refused), and runs the local copy. A mirror may rehost the same script; it may not substitute a different program. Step Code's script accepts an install directory; the OpenCode script hardcodes `~/.opencode/bin`, so a custom directory only applies to it through npm/pnpm's `--prefix`. Script installs can be switched off entirely, leaving only the package managers.
155
+
156
+ **One-click repair and uninstall.** Repair re-runs the same fixed official command, which is what a broken install needs even when it already reports the latest version. Uninstall removes only the program: a package-manager install goes back through the same manager, and a script install deletes just the executable the vendor installer placed plus that install's own PATH entry in the shell profile. **Configuration, credentials and session history are always kept.**
147
157
 
148
158
  Installation, trusted launch readiness, account login and model entitlement are separate checks. An unknown login status is not proof of logout. On Windows, supported managed launch entries are checked against publisher signatures or official npm-tarball attestations; ZCode also records trust for its bundled script per signed build. See [the registry](.dsh-plugin/shared/official-tool-registry.mjs) and [executor](.dsh-plugin/shared/official-tool-executor.mjs) for the exact platform rules.
149
159
 
160
+ In a session, `/tools install|uninstall|repair|method <tool-id> [method]` does the same; agents can call `model_router_tool_install`, `model_router_tool_repair` and `model_router_tool_uninstall`. The install directory and download sources always come from the user's own plugin settings and never travel over the RPC, so no call can redirect an install at an arbitrary package or URL.
161
+
150
162
  `model_router_execute` has fixed headless adapters for Claude, Codex and Gemini. Other suppliers use the model-directory API unless a supported subscription route applies. Missing/unusable CLIs can fall back to that API; subscription failures follow the policy below.
151
163
 
152
164
  Editable execution needs a clean Git repository, Host approval and the Harness process sandbox. Changes are made in an isolated worktree and applied back only after successful execution and a matching clean source workspace. Failed or conflicting integration is reported. Git-ignored outputs need separate review. Read-only CLI flags do not necessarily limit file reads to the workspace; check **安全边界**, especially Codex's broader readable scope.
package/README.zh.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Model Router · DeepSeek Harness 模型路由插件
2
2
 
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 聚合评分/费用;最新逐题质量—费用矩阵尚未取得,不声称真实路由收益。
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
+
7
9
  **0.16.3**:恢复全部官方工具卡片的检测和安装。Windows 上的探测与 `npm` 再次按独立参数启动,避免 cmd 把 Node 指到不存在的脚本。只要输出版本号就视为已安装;单张卡片的检测错误留在该行;已经是最新版的 CLI 不再被收成「修复官方执行入口」。
8
10
 
9
11
  **0.16.2**:官方工具的更新/安装按钮会保留本次点击启动的任务;已有探测结果时重新体检不再把按钮禁用成无响应;点击无法开始、或安装后探测版本仍低于最新版时,在该行显示错误。已安装的 Claude Code 先执行 `claude update`,再按注册表走 npm;已安装的 Windows MiniMax 走官方安装脚本。
@@ -12,7 +14,7 @@
12
14
 
13
15
  **0.16.0 新增**:[动态数据与持续反馈](docs/ROUTING_ADAPTIVE_LEARNING.zh.md):可选公开价格/LiveBench快照刷新;按任务类型、时间衰减和低样本收缩调整个人偏好,质量门槛独立保留。设置在“预算与安全”,公开源默认关闭;价格源需提供按官方文档核对的固定USD单价JSON,不是通用官网抓价器。本地DSH_HOME共享、最近200运行窗口,暂无真实收益证明。
14
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) 已拆成两个独立插件。
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) 已拆成两个独立插件。
16
18
 
17
19
  **0.15.0 更新**:工作台分为四页,路由建议紧接任务规划展示;执行输入变化时丢弃旧预览;裁剪历史或重试后仍保留费用累计;源码开发新增独立模拟 UI 预览。
18
20
 
@@ -44,16 +46,16 @@
44
46
  打开 **插件 → 添加插件**,填写:
45
47
 
46
48
  ```text
47
- @ljwei-stak/dsh-model-router@0.16.3
49
+ @ljwei-stak/dsh-model-router@0.16.4
48
50
  ```
49
51
 
50
- 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.3**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
52
+ 镜像缺少该精确版本时使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。安装并启用后,完全退出 Harness(包括托盘进程)再启动。插件详情应显示 **0.16.4**,侧栏出现**模型路由**。仅运行全局 `npm install -g` 不会把插件注册到桌面版 profile。
51
53
 
52
54
  也可在插件管理器填写本地 `.tgz` 文件的绝对路径,或解压后的内层 `package` 目录;其中应包含 `package.json` 与 `.dsh-plugin`。[历史发布页](https://github.com/Alice-Marx/dsh-model-router/releases)提供归档;已克隆源码的开发方式见下文。
53
55
 
54
56
  ### 从旧包名升级
55
57
 
56
- `@ljwei-stak/model-router-galgame` 的最后版本为 0.13.0。保留 profile 与应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.16.3`。不要同时安装两个包:它们共用内部插件 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 保留使已有路由设置和执行历史能够延续。
57
59
 
58
60
  从 0.11.x 合并版迁移时,先备份 profile 并导出希望保留的 GAL 进度。剧情、立绘、音乐、自由模式和存档属于独立 `@ljwei-stak/dsh-galgame` 插件。导出范围及同 profile 迁移的限制见 [MIGRATION.md](MIGRATION.md)。
59
61
 
@@ -141,12 +143,22 @@ CLI 团队是插件自有的顺序依赖执行器。Harness 内建 Agent Teams
141
143
  | `mimo-code` | MiMo Code | `read-only`、经审批的 `workspace-write` |
142
144
  | `grok-build` | Grok Build | `read-only`、经审批的 `workspace-write` |
143
145
  | `gemini` | Gemini CLI | 路由执行的无界面适配器;不属于单工具/团队托管执行器 |
146
+ | `opencode` | OpenCode | 安装、检测与登录体检;尚未完成受限执行入口核验 |
147
+ | `stepcode` | Step Code | 安装、检测与登录体检;尚未完成受限执行入口核验 |
144
148
  | `zcode` | ZCode | Windows;经审批的 `workspace-write` |
145
149
 
146
- 安装使用固定官方 npm 包的 `@latest`,或 ZCode 官方、经过发布者签名验证的 Windows 安装器。已安装版本比查询到的最新版更高时不会降级。最新版查询缓存约 12 小时;离线/查询失败时保持未知。厂商新版本没有自动获得本项目验证。ZCode 打开安装窗口后,仍需手动选目录并完成安装。
150
+ 安装使用固定官方 npm 包的 `@latest`、厂商官方安装脚本,或 ZCode 官方、经过发布者签名验证的 Windows 安装器。已安装版本比查询到的最新版更高时不会降级。最新版查询缓存约 12 小时;离线/查询失败时保持未知。厂商新版本没有自动获得本项目验证。ZCode 打开安装窗口后,仍需手动选目录并完成安装。
151
+
152
+ **统一安装设置。** 在**官方工具**页顶部(或插件设置页)可以一次设定所有工具的安装目录与下载源:统一安装目录(留空=各厂商默认位置)、npm 源地址,以及按工具 ID 覆盖厂商安装脚本的镜像地址。目录必须是绝对路径,下载地址必须是 https。每个工具都可以在**安装方式**下拉框里单独选择 `npm`、`pnpm`、厂商安装脚本(`curl | bash` 或 `irm | iex`);选了某个平台不支持的方式时,行内会写明原因而不是悄悄换成别的。每行上方实时显示将要执行的完整命令。
153
+
154
+ **脚本方式不会把远程内容直接管道给解释器。** 插件先下载厂商安装脚本、核对它仍然是该厂商的官方脚本(校验厂商标记、拒绝 HTML 页面和其他厂商的脚本),再执行本地副本。镜像可以重新托管同一个脚本,但不能替换成别的程序。阶跃的脚本还支持安装目录参数,OpenCode 的官方脚本写死了 `~/.opencode/bin`,因此自定义安装目录对它只能通过 npm/pnpm 的 `--prefix` 生效。脚本方式可以整体关闭,只保留包管理器方式。
155
+
156
+ **一键修复与一键卸载。** 修复会用同一条固定官方命令重新安装一遍,适合修复损坏或不完整的安装;卸载只删除程序本身——包管理器安装走同一个管理器卸载,脚本安装只删除厂商安装器放下的那一个可执行文件,并顺带清掉它在 shell 配置里写的 PATH 记录。**配置、登录信息和历史记录一律保留**,重新安装后仍在。
147
157
 
148
158
  **已安装、可信入口就绪、账号已登录、模型有权限是不同状态。** 登录未知不代表已退出。在 Windows,托管入口按发布者签名或官方 npm tarball 校验;ZCode 另对签名构建中的脚本记录首次信任摘要。平台细则见[注册表](.dsh-plugin/shared/official-tool-registry.mjs)和[执行器](.dsh-plugin/shared/official-tool-executor.mjs)。
149
159
 
160
+ 会话内可用 `/tools install|uninstall|repair|method <工具id> [安装方式]`;Agent 可调用 `model_router_tool_install`、`model_router_tool_repair` 和 `model_router_tool_uninstall`。安装目录与下载源始终来自用户自己的插件设置,不会由客户端或 Agent 传入,因此任何一条调用都无法把安装指向自定义包名或任意地址。
161
+
150
162
  `model_router_execute` 的固定无界面适配器为 Claude、Codex 和 Gemini;其他供应商经模型目录 API 或适用的订阅路线执行。CLI 缺失/不可用时可回退 API;真正尝试后的订阅失败按下方策略处理。
151
163
 
152
164
  可编辑执行要求干净 Git 仓库、Harness 审批与进程沙箱。修改先在独立工作树进行,执行成功且原仓库仍干净并匹配时才整合源码补丁;执行或整合失败会明确报告。Git 忽略的输出另行检查。只读参数不一定把可读文件限制在工作区,尤其需在**安全边界**核对 Codex 的较大可读范围。
@@ -97,14 +97,44 @@
97
97
 
98
98
  ## 官方工具:体检、安装与登录
99
99
 
100
- 当前注册表包括 Kimi、Claude、Codex、MiniMax、MiMo、Grok、Gemini 和 ZCode。点**重新体检**查看实际安装、版本、入口及账号状态;最新版查询失败时保持未知,不表示已无更新。
100
+ 当前注册表包括 Kimi、Claude、Codex、MiniMax、MiMo、Grok、Gemini、OpenCode、Step Code 和 ZCode。点**重新体检**查看实际安装、版本、入口及账号状态;最新版查询失败时保持未知,不表示已无更新。
101
101
 
102
102
  1. 未安装时可从卡片固定官方来源安装;npm 工具安装厂商 `@latest`。新厂商版本没有自动通过本项目测试。
103
103
  2. 安装中可查看日志或取消;取消后重新体检确认状态。
104
104
  3. 有更新时可更新;已安装版本比查询结果更高时不会降级。
105
- 4. 已安装但未就绪时查看入口/平台原因,使用适用的修复动作;仅检测到文件不能证明可信执行。
106
- 5. ZCode 打开官方安装窗口后,还需要手动选目录完成安装,再体检。它属于 Windows 桌面应用。
107
- 6. 按**去登录**提示,在对应 CLI/终端完成厂商授权,然后用小任务验证权限。
105
+ 4. 已安装但未就绪时点**一键修复**,用同一条固定命令重新安装一遍。
106
+ 5. 不再需要时点**一键卸载**,只删除程序本身;配置、登录信息和历史记录保留,重新安装后仍在。
107
+ 6. ZCode 打开官方安装窗口后,还需要手动选目录完成安装,再体检。它属于 Windows 桌面应用,由系统“应用”卸载。
108
+ 7. 按**去登录**提示,在对应 CLI/终端完成厂商授权,然后用小任务验证权限。
109
+
110
+ ### 统一安装设置
111
+
112
+ 卡片顶部**统一安装设置**一次设定所有工具的安装地址,改动后下一次安装、修复、卸载立即使用。插件设置页里有同样的字段。
113
+
114
+ | 设置 | 作用 | 限制 |
115
+ | --- | --- | --- |
116
+ | 统一安装目录 | 所有一键安装的目标目录 | 必须是绝对路径,不能是磁盘或共享根目录;留空=各厂商默认位置 |
117
+ | npm 源地址 | npm / pnpm 方式的下载源 | 必须 https,可填国内镜像 |
118
+ | 安装脚本源覆盖 | 按工具 ID 指定厂商安装脚本的镜像 | JSON,键是官方工具 ID,值是 https 链接 |
119
+ | 允许 curl / irm 安装脚本 | 是否允许执行厂商安装脚本 | 关闭后只剩 npm / pnpm |
120
+
121
+ 几个容易踩的点:
122
+
123
+ - **不同工具对"自定义目录"的支持不一样。** npm 与 pnpm 都接受 `--prefix`,所以一定生效。阶跃的安装脚本接受 `--install-dir`(Windows 上是 `-InstallDir`),也生效。OpenCode 的官方脚本把路径写死成 `~/.opencode/bin`,**无法**接受自定义目录——如果设置里填了目录,它的行内会明确写出将装到哪里。
124
+ - **脚本方式不是把远程内容直接交给解释器。** 插件会先下载脚本、核对它仍然是该厂商的官方安装脚本,再执行本地副本;HTML 报错页和其他厂商的脚本都会被拒绝执行。镜像可以重新托管同一个脚本,不能替换成别的程序。阶跃的脚本源覆盖还会同时改写它下载二进制与校验和的地址,所以镜像需要提供完整的发布文件。
125
+ - **每行可以单独选安装方式。** 下拉框里列出该工具声明的全部方式;当前系统不支持的方式会写明原因,而不是悄悄换成别的。选中某个工具的方式会保存成该工具的默认,`/tools` 和 Agent 工具也会照此执行。
126
+ - **命令是可见的。** 每行上方实时显示这条设置下真正会执行的完整命令,点击前就能核对。
127
+
128
+ ### 安装方式的会话与 Agent 等价物
129
+
130
+ | 工作台操作 | 会话命令 | Agent 工具 |
131
+ | --- | --- | --- |
132
+ | 一键安装 / 更新 | `/tools install <工具id> [安装方式]` | `model_router_tool_install` |
133
+ | 一键修复 | `/tools repair <工具id> [安装方式]` | `model_router_tool_repair` |
134
+ | 一键卸载 | `/tools uninstall <工具id> [安装方式]` | `model_router_tool_uninstall` |
135
+ | 查看可用方式 | `/tools method <工具id>` | `model_router_tools` 的 `installMethods` |
136
+
137
+ 安装目录与下载源只来自你自己的插件设置,不会由客户端或 Agent 传入;任何一条调用都无法把安装指向自定义包名或任意地址。安装和卸载都需要 Harness 审批。
108
138
 
109
139
  注册表 ID 和托管模式如下:
110
140
 
@@ -113,6 +143,7 @@
113
143
  | `claude-code`、`codex`、`mimo-code`、`grok-build` | 只读,以及经审批的可编辑执行。 |
114
144
  | `kimi-code`、`minimax-code`、`zcode` | 经审批的可编辑执行。 |
115
145
  | `gemini` | 路由无界面适配器,不进入单工具/团队托管执行器。 |
146
+ | `opencode`、`stepcode` | 尚未核验受限执行入口;插件负责安装、检测与登录体检,执行请在终端中直接进行。 |
116
147
 
117
148
  ### 交互终端
118
149