dsh-plugin 1.3.7 → 1.3.9

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.
@@ -25,7 +25,9 @@ export const zh = {
25
25
  fork: 'Fork',
26
26
  loading: '正在加载插件数据…',
27
27
  failed: '插件数据加载失败',
28
- failedDesc: '请稍后重试,或点击右上角按钮在浏览器中打开插件中心。',
28
+ // 目录拉不出来时的精准提示:按「是否配置了代理」区分,引导排查网络 / 代理
29
+ failedDescNoProxy: '你的网络似乎不通,无法获取插件目录数据。请检查网络连接后重试;或点击右上角按钮在浏览器中打开插件中心。',
30
+ failedDescProxy: '你的网络似乎不通,或设置里的 HTTP 代理地址不可达,无法获取插件目录数据。请检查代理设置与网络连接后重试;或点击右上角按钮在浏览器中打开插件中心。',
29
31
  retry: '重试',
30
32
  install: '安装',
31
33
  // AI 识别可能需要命令行辅助的插件(webInstallable=false):仍可尝试一键安装,失败时复制命令去 dsh 终端
@@ -309,6 +311,14 @@ export const zh = {
309
311
  failNetworkTarget: '无法访问 {url} —— 你的网络连接不通',
310
312
  // 「去系统诊断」直达按钮:网络不通提示下方的品牌实底按钮,跳到设置 → 系统诊断跑连通性检测
311
313
  failNetworkRunDiag: '去系统诊断检测网络',
314
+ failDshMissingHint: '系统找不到 dsh 命令(dsh 未加入系统 PATH),无法调用安装器执行安装。请确认 DeepSeek Harness 已正确安装、dsh 已加入 PATH(或重新安装),然后重试。这不是插件本身的问题。',
315
+ failPnpmMissingHint: '安装器找到了 dsh,但系统里没有 pnpm 命令(dsh 用它来管理 profile 插件)。请先安装 pnpm(如 npm install -g pnpm),确认 pnpm 已加入 PATH 后重试。这不是插件本身的问题。',
316
+ // 自定义安装三卡片入口的渠道名(报错/日志溯源:本次安装从哪个入口发起)
317
+ installChannelNpm: 'NPM 包',
318
+ installChannelGit: 'GitHub 源码',
319
+ installChannelDsh: 'DSH 命令',
320
+ // 报错/通知中心的入口溯源行:前缀在错误正文顶部,精准指出自定义安装是从哪张卡片发起的
321
+ installEntryLine: '安装入口:{c}。',
312
322
  reportIssue: '去插件仓库提交 Issue',
313
323
  detail: '打开详情',
314
324
  noResult: '没有找到匹配的插件',
@@ -368,7 +378,9 @@ export const en = {
368
378
  fork: 'Fork',
369
379
  loading: 'Loading plugin data…',
370
380
  failed: 'Failed to load plugin data',
371
- failedDesc: 'Please retry, or open the hub in your browser with the button above.',
381
+ // Precise hints when the catalog cannot load: distinguished by whether a proxy is configured
382
+ failedDescNoProxy: 'Your network seems unreachable, so the plugin catalog could not be loaded. Please check your connection and retry; or open the hub in your browser with the button above.',
383
+ failedDescProxy: 'Your network seems unreachable, or the HTTP proxy in Settings is unreachable, so the plugin catalog could not be loaded. Please check the proxy address and your connection, then retry; or open the hub in your browser with the button above.',
372
384
  retry: 'Retry',
373
385
  install: 'Install',
374
386
  // AI-flagged plugins that may need command-line help (webInstallable=false): one-click install still allowed,
@@ -654,6 +666,14 @@ export const en = {
654
666
  failNetworkTarget: 'Cannot reach {url} — your network connection is down',
655
667
  // Diagnostics shortcut button under the network-down hint: jumps to Settings → System Diagnostics
656
668
  failNetworkRunDiag: 'Run a network diagnostic',
669
+ failDshMissingHint: 'The dsh command could not be found (it is not on the system PATH), so the installer could not run. Please make sure DeepSeek Harness is installed correctly and dsh is on your PATH (or reinstall it), then retry. This is not a problem with the plugin itself.',
670
+ failPnpmMissingHint: 'The installer found dsh, but pnpm is missing on this machine (dsh uses it to manage profile plugins). Please install pnpm first (e.g. npm install -g pnpm), make sure it is on your PATH, then retry. This is not a problem with the plugin itself.',
671
+ // Custom install entry channel names (the three cards) — used in error/log traceability
672
+ installChannelNpm: 'NPM package',
673
+ installChannelGit: 'GitHub source',
674
+ installChannelDsh: 'DSH command',
675
+ // Entry traceability line prefixed at the top of the error text: which card initiated this install
676
+ installEntryLine: 'Install entry: {c}.',
657
677
  reportIssue: 'Report an Issue to the plugin repo',
658
678
  detail: 'Open details',
659
679
  noResult: 'No plugins match your search',
@@ -151,15 +151,22 @@ export function removeNotification(id: number): NotificationRecord[] {
151
151
  return next
152
152
  }
153
153
 
154
- export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
154
+ export type FailureKind = 'npmTooOld' | 'dshMissing' | 'pnpmMissing' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
155
155
 
156
156
  /**
157
- * 失败归类,五态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
157
+ * 失败归类,七态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
158
158
  * 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
159
- * 一律引导提 Issue;唯一的两个例外是本机环境问题(npm 版本过低 / 网络不通):
159
+ * 一律引导提 Issue;唯一的例外是本机环境问题(npm 版本过低 / 找不到 dsh / 找不到 pnpm / 网络不通):
160
160
  * - npmTooOld:失败输出含 npm arborist 的 `edgesOut` 崩溃特征(build-ideal-tree.js 解 peer 依赖时
161
161
  * 内部抛错,npm 11.6.0 前必现的已知缺陷,npm/cli#8261、#9787),或服务端已核实本机版本低于
162
162
  * 阈值并打了 `[npm-too-low]` 标记 —— 是本机 npm 版本过低/自身缺陷,不是插件问题 → 引导升级 npm
163
+ * - dshMissing:安装器 spawn 的 `dsh` 命令找不到(Windows cmd「不是内部或外部命令」/ POSIX
164
+ * 「command not found」/ spawn ENOENT)—— 是本机 DSH 未正确安装或不在 PATH,不是插件问题
165
+ * → 提示检查 PATH/重装 DSH,不引导提 Issue
166
+ * - pnpmMissing:dsh 存在但调用的 `pnpm` 找不到(dsh 报 `pnpm not found on PATH`/POSIX
167
+ * 「pnpm: command not found」/ spawn ENOENT)—— 本机缺 pnpm(dsh 用 pnpm 管理 profile 插件),
168
+ * 不是插件问题(dsh-plugin-hub#13:Linux 下 `dsh: pnpm not found on PATH` 被误归插件侧失败)
169
+ * → 提示安装/开启 pnpm,不引导提 Issue
163
170
  * - network:安装前连通性预检拦截(服务端 `[network]` 标记)或底层连接失败
164
171
  * (ERR_PNPM_GIT_FETCH_FAILED / ETIMEDOUT / DNS 解析 / TLS 握手 / 代理拒绝)——
165
172
  * 是本机网络不通/被墙/代理有问题,不是插件问题 → 提示检查网络,不引导提 Issue
@@ -176,6 +183,19 @@ export function classifyFailure(message: string): FailureKind {
176
183
  // 不是插件问题 —— 必须最先判,否则该报错会被外层 ERR_PNPM_PREPARE_PACKAGE 吞成
177
184
  // 「插件打包分发问题」,误导用户去提 Issue
178
185
  if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
186
+ // 找不到 pnpm 命令(dsh 报 `pnpm not found on PATH` —— dsh 用它管理 profile 插件 /
187
+ // POSIX「pnpm: command not found」/ spawn pnpm ENOENT / Windows cmd 中英文报错):
188
+ // 本机缺 pnpm,不是插件问题。必须在 dshMissing 之前 —— dshMissing 正则含裸「command not
189
+ // found」,会把 `pnpm: command not found` 吞成「dsh 缺失」,误引导用户去装 DSH
190
+ // (dsh-plugin-hub#13:Linux 下 `dsh: pnpm not found on PATH` 被误归插件侧失败)
191
+ if (/\[pnpm-missing\]|pnpm not found|pnpm: command not found|spawn pnpm ENOENT|'pnpm' 不是内部或外部命令|"pnpm" 不是内部或外部命令|pnpm['"]?\s*is not recognized/i.test(message)) return 'pnpmMissing'
192
+ // 找不到 dsh 命令(服务端 [dsh-missing] 标记 —— 乱码免疫:Windows cmd 中文版输出 GBK,
193
+ // 经 UTF-8 解码成乱码无法匹配原文,故服务端在 spawn 前用 which/where 探测并打 ASCII 标记;
194
+ // 其余形态:Windows cmd「不是内部或外部命令」/ POSIX「command not found」/
195
+ // node spawn ENOENT):是本机 DSH 未正确安装或不在 PATH,不是插件问题 —— 必须先判,
196
+ // 否则会被外层 "Command failed" 吞成「插件打包问题」,误导用户去提 Issue
197
+ // (dsh-plugin-hub#12:Win 下 'dsh' 不在 PATH,cmd 报「不是内部或外部命令」被误归插件侧失败)
198
+ if (/\[dsh-missing\]|不是内部或外部命令|is not recognized as an internal or external command|command not found|spawn dsh ENOENT/i.test(message)) return 'dshMissing'
179
199
  // 构建脚本被 pnpm 白名单(allowBuilds)拦截:插件的 prepare 脚本或依赖里的原生模块构建
180
200
  // 被默认拒绝(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED / ERR_PNPM_IGNORED_BUILDS)。
181
201
  // 这类错误出现即说明 pnpm 已成功 fetch 到 tarball(网络是通的),主因是插件构建脚本
@@ -40,6 +40,8 @@ export function pluginSiteUrl(repo: string): string {
40
40
  function reasonTitleOf(kind: FailureKind): string {
41
41
  switch (kind) {
42
42
  case 'npmTooOld': return 'npm too old to install'
43
+ case 'dshMissing': return 'dsh command not found on the user machine'
44
+ case 'pnpmMissing': return 'pnpm command not found on the user machine'
43
45
  case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
44
46
  case 'pluginPrepare': return 'plugin distribution incomplete'
45
47
  case 'network': return 'network failure on the user side'
@@ -301,7 +301,6 @@ body[data-ds-dark-theme] .uninstallBtn:hover {
301
301
  }
302
302
 
303
303
  .retryBtn {
304
- margin-top: 4px;
305
304
  font-size: 12px;
306
305
  line-height: 18px;
307
306
  color: var(--hub-text-secondary);
@@ -318,6 +317,30 @@ body[data-ds-dark-theme] .uninstallBtn:hover {
318
317
  background: var(--hub-bg-hover);
319
318
  }
320
319
 
320
+ /* 失败态按钮行:重试 + 「去系统诊断检测网络」并排 */
321
+ .stateActions {
322
+ margin-top: 4px;
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 8px;
326
+ }
327
+
328
+ .diagBtn {
329
+ font-size: 12px;
330
+ line-height: 18px;
331
+ color: #fff;
332
+ border: none;
333
+ border-radius: 6px;
334
+ padding: 4px 12px;
335
+ background: var(--hub-brand);
336
+ cursor: pointer;
337
+ user-select: none;
338
+ }
339
+
340
+ .diagBtn:hover {
341
+ background: var(--hub-brand-hover);
342
+ }
343
+
321
344
  /* —— footer —— */
322
345
 
323
346
  .footer {
@@ -9,6 +9,10 @@
9
9
 
10
10
  export type LocaleId = 'zh' | 'en'
11
11
 
12
+ /** 自定义安装入口渠道:三张卡片(NPM 包 / GitHub 源码 / DSH 命令)。
13
+ * 客户端提交时显式上报,服务端落日志/报错提示据此溯源「从哪个入口发起」。 */
14
+ export type InstallChannel = 'npm' | 'git' | 'dsh'
15
+
12
16
  /** Translation function signature shared by the host binder and the in-component dictionary. */
13
17
  export type Translate = (key: string, params?: Record<string, string | number>) => string
14
18
 
@@ -646,6 +646,12 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
646
646
  const source = typeof body === 'object' && body !== null && typeof (body as { source?: unknown }).source === 'string'
647
647
  ? (body as { source: string }).source
648
648
  : ''
649
+ // 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
650
+ // 客户端显式上报,服务端日志/报错据此精准溯源「从哪个入口发起」;目录插件安装不传。
651
+ const installChannel = (() => {
652
+ const raw = body !== null && typeof body === 'object' ? (body as { installChannel?: unknown }).installChannel : undefined
653
+ return raw === 'git' || raw === 'dsh' || raw === 'npm' ? raw : undefined
654
+ })()
649
655
  const settings = loadSettings(profile)
650
656
  // 界面语言(客户端随请求携带):错误消息按用户当前语言提示(中文界面给中文,英文界面给英文)
651
657
  const lang = typeof body === 'object' && body !== null && (body as { lang?: unknown }).lang === 'en'
@@ -684,6 +690,8 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
684
690
  globalNpm,
685
691
  timeoutMs: COMMAND_TIMEOUT_MS,
686
692
  env: mutationEnv(settings),
693
+ // 全局 npm 安装由 NPM 包卡片/官方命令发起:日志体现入口渠道
694
+ installChannel: installChannel ?? 'npm',
687
695
  })
688
696
  sendJson(response, 200, { ok: true, task: task.id })
689
697
  return
@@ -816,6 +824,8 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
816
824
  env: mutationEnv(settings),
817
825
  // 覆盖更新:npm 通道命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
818
826
  updateNpm: mode === 'update',
827
+ // 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):日志/报错据此溯源
828
+ installChannel,
819
829
  })
820
830
  sendJson(response, 200, { ok: true, task: task.id })
821
831
  } catch (error) {
@@ -28,6 +28,9 @@ export interface InstallTask {
28
28
  /** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
29
29
  globalNpm?: string[]
30
30
  action: 'add' | 'remove' | 'update'
31
+ /** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
32
+ * 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装无此字段。 */
33
+ installChannel?: 'npm' | 'git' | 'dsh'
31
34
  /** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
32
35
  status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled'
33
36
  timedOut: boolean
@@ -51,7 +54,7 @@ export interface Invocation {
51
54
  /** 排队中的变更任务及其启动参数。 */
52
55
  export interface QueueItem {
53
56
  task: InstallTask
54
- options: { action: 'add' | 'remove' | 'update'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[] }
57
+ options: { action: 'add' | 'remove' | 'update'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[]; installChannel?: 'npm' | 'git' | 'dsh' }
55
58
  }
56
59
 
57
60
  /**
@@ -7,7 +7,7 @@
7
7
  * 每个任务一次 `dsh plugin --profile <profile> <action> <target>` 子进程,
8
8
  * 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
9
9
  */
10
- import { spawn } from 'node:child_process'
10
+ import { spawn, spawnSync } from 'node:child_process'
11
11
  import { readFileSync, statSync } from 'node:fs'
12
12
  import { dirname, join, resolve } from 'node:path'
13
13
  import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
@@ -44,6 +44,8 @@ export interface ActiveTaskInfo {
44
44
  lines: string[]
45
45
  /** 尝试过的安装方式(npm 反查 + 实际执行命令):失败时前端 issue 预填需要 */
46
46
  attempts: string[]
47
+ /** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):前端刷新恢复后仍能精准溯源 */
48
+ installChannel?: 'npm' | 'git' | 'dsh'
47
49
  }
48
50
 
49
51
  /** All non-terminal tasks in queue order (running first, then pending). Lets the client resume a queue after a page reload. */
@@ -55,12 +57,12 @@ export function activeTask(): ActiveTaskInfo[] {
55
57
  for (const item of queue) {
56
58
  const task = item.task
57
59
  if (task.status === 'pending') {
58
- found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts })
60
+ found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel })
59
61
  }
60
62
  }
61
63
  tasks.forEach((task) => {
62
64
  if (task.status === 'running') {
63
- found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts })
65
+ found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel })
64
66
  }
65
67
  })
66
68
  return found
@@ -140,6 +142,23 @@ function cliInvocation(): Invocation {
140
142
  return { file: 'dsh', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
141
143
  }
142
144
 
145
+ /**
146
+ * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
147
+ * Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
148
+ * 无法匹配),POSIX 非 shell 模式才会抛 spawn dsh ENOENT —— 两种形态都不可靠,统一在
149
+ * spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
150
+ * 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
151
+ */
152
+ function dshOnPath(env?: NodeJS.ProcessEnv): boolean {
153
+ try {
154
+ const probe = process.platform === 'win32' ? 'where' : 'which'
155
+ const res = spawnSync(probe, ['dsh'], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] })
156
+ return res.status === 0 && (res.stdout ?? '').trim().length > 0
157
+ } catch {
158
+ return true
159
+ }
160
+ }
161
+
143
162
  function stopChild(child: ReturnType<typeof spawn>): void {
144
163
  if (process.platform === 'win32' && child.pid !== undefined) {
145
164
  const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' })
@@ -168,6 +187,15 @@ function mutationVerb(action: 'add' | 'remove' | 'update'): string {
168
187
  return action === 'remove' ? '开始卸载' : action === 'update' ? '开始更新' : '开始安装'
169
188
  }
170
189
 
190
+ /** 系统日志入口渠道后缀:自定义安装(带渠道)在日志里体现「从哪张卡片发起」,
191
+ * 目录插件安装(无渠道)返回空串。日志中文语境,渠道名用中文口语。 */
192
+ function channelLabel(channel: 'npm' | 'git' | 'dsh' | undefined): string {
193
+ if (channel === 'git') return '(GitHub 源码入口)'
194
+ if (channel === 'dsh') return '(DSH 命令入口)'
195
+ if (channel === 'npm') return '(NPM 包入口)'
196
+ return ''
197
+ }
198
+
171
199
  /**
172
200
  * Enqueue a plugin mutation and return its task. Tasks run strictly serially:
173
201
  * the queue worker starts the next one only after the previous finishes.
@@ -189,8 +217,11 @@ export function startPluginMutation(options: {
189
217
  attempts?: string[]
190
218
  /** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
191
219
  updateNpm?: boolean
220
+ /** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
221
+ * 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装不传 */
222
+ installChannel?: 'npm' | 'git' | 'dsh'
192
223
  }): InstallTask {
193
- const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false }
224
+ const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false, installChannel: options.installChannel }
194
225
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
195
226
  tasks.set(task.id, task)
196
227
  if (tasks.size > MAX_TASKS) {
@@ -203,14 +234,18 @@ export function startPluginMutation(options: {
203
234
  })
204
235
  }
205
236
  queue.push({ task, options: { ...options } })
206
- // 系统日志:记录任务入队(安装/卸载/更新开始)
237
+ // 任务输出首行标记入口渠道(ASCII 标记,乱码免疫):进度面板/失败明细里能溯源「从哪张卡片发起」
238
+ if (options.installChannel !== undefined) {
239
+ pushLine(task, `[install-channel] ${options.installChannel}`)
240
+ }
241
+ // 系统日志:记录任务入队(安装/卸载/更新开始),自定义安装带入口渠道后缀
207
242
  const category = mutationCategory(options.action)
208
243
  appendLog(options.profile, {
209
244
  at: Date.now(),
210
245
  level: 'info',
211
246
  category,
212
247
  event: `${category}.start`,
213
- message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}`,
248
+ message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}${channelLabel(options.installChannel)}`,
214
249
  })
215
250
  pumpQueue()
216
251
  return task
@@ -235,7 +270,7 @@ function pumpQueue(): void {
235
270
  level: 'success',
236
271
  category,
237
272
  event: `${category}.done`,
238
- message: `${verb('成功')} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
273
+ message: `${verb('成功')} ${show}${channelLabel(options.installChannel)}${task.needsRestart ? '(待重启生效)' : ''}`,
239
274
  })
240
275
  } else if (task.status === 'failed') {
241
276
  appendLog(options.profile, {
@@ -243,7 +278,7 @@ function pumpQueue(): void {
243
278
  level: 'error',
244
279
  category,
245
280
  event: `${category}.fail`,
246
- message: `${verb('失败')} ${show}`,
281
+ message: `${verb('失败')} ${show}${channelLabel(options.installChannel)}`,
247
282
  })
248
283
  } else if (task.status === 'cancelled') {
249
284
  appendLog(options.profile, {
@@ -251,7 +286,7 @@ function pumpQueue(): void {
251
286
  level: 'warn',
252
287
  category,
253
288
  event: 'task.cancel',
254
- message: `已取消 ${show}`,
289
+ message: `已取消 ${show}${channelLabel(options.installChannel)}`,
255
290
  })
256
291
  }
257
292
  // 已终态的任务移出队列:既防 queue 数组无限增长(内存泄漏),也让下一轮 pump 干净
@@ -319,6 +354,20 @@ function spawnMutation(options: {
319
354
  let settled = false
320
355
  let child: ReturnType<typeof spawn>
321
356
  try {
357
+ // 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
358
+ // 命令不存在时 Windows shell 模式只回传 GBK 乱码、POSIX 才抛 spawn dsh ENOENT —— 统一前置
359
+ // 拦截并打 ASCII 标记 [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
360
+ if (invocation.file === 'dsh' && !dshOnPath(env)) {
361
+ const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)'
362
+ if (task) {
363
+ task.status = 'failed'
364
+ task.exitCode = null
365
+ task.progress = 0
366
+ pushLine(task, detail)
367
+ }
368
+ resolveOnce({ exitCode: 1, timedOut: false, error: detail, stdout: '', stderr: detail })
369
+ return
370
+ }
322
371
  child = spawn(invocation.file, attemptArgs, {
323
372
  cwd: invocation.cwd,
324
373
  env,