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.
@@ -650,6 +650,12 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
650
650
  const source = typeof body === 'object' && body !== null && typeof body.source === 'string'
651
651
  ? body.source
652
652
  : '';
653
+ // 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
654
+ // 客户端显式上报,服务端日志/报错据此精准溯源「从哪个入口发起」;目录插件安装不传。
655
+ const installChannel = (() => {
656
+ const raw = body !== null && typeof body === 'object' ? body.installChannel : undefined;
657
+ return raw === 'git' || raw === 'dsh' || raw === 'npm' ? raw : undefined;
658
+ })();
653
659
  const settings = loadSettings(profile);
654
660
  // 界面语言(客户端随请求携带):错误消息按用户当前语言提示(中文界面给中文,英文界面给英文)
655
661
  const lang = typeof body === 'object' && body !== null && body.lang === 'en'
@@ -688,6 +694,8 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
688
694
  globalNpm,
689
695
  timeoutMs: COMMAND_TIMEOUT_MS,
690
696
  env: mutationEnv(settings),
697
+ // 全局 npm 安装由 NPM 包卡片/官方命令发起:日志体现入口渠道
698
+ installChannel: installChannel ?? 'npm',
691
699
  });
692
700
  sendJson(response, 200, { ok: true, task: task.id });
693
701
  return;
@@ -821,6 +829,8 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
821
829
  env: mutationEnv(settings),
822
830
  // 覆盖更新:npm 通道命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
823
831
  updateNpm: mode === 'update',
832
+ // 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):日志/报错据此溯源
833
+ installChannel,
824
834
  });
825
835
  sendJson(response, 200, { ok: true, task: task.id });
826
836
  }
@@ -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 { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.js';
@@ -40,12 +40,12 @@ export function activeTask() {
40
40
  for (const item of queue) {
41
41
  const task = item.task;
42
42
  if (task.status === 'pending') {
43
- 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 });
43
+ 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 });
44
44
  }
45
45
  }
46
46
  tasks.forEach((task) => {
47
47
  if (task.status === 'running') {
48
- 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 });
48
+ 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 });
49
49
  }
50
50
  });
51
51
  return found;
@@ -124,6 +124,23 @@ function cliInvocation() {
124
124
  }
125
125
  return { file: 'dsh', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' };
126
126
  }
127
+ /**
128
+ * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
129
+ * Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
130
+ * 无法匹配),POSIX 非 shell 模式才会抛 spawn dsh ENOENT —— 两种形态都不可靠,统一在
131
+ * spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
132
+ * 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
133
+ */
134
+ function dshOnPath(env) {
135
+ try {
136
+ const probe = process.platform === 'win32' ? 'where' : 'which';
137
+ const res = spawnSync(probe, ['dsh'], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] });
138
+ return res.status === 0 && (res.stdout ?? '').trim().length > 0;
139
+ }
140
+ catch {
141
+ return true;
142
+ }
143
+ }
127
144
  function stopChild(child) {
128
145
  if (process.platform === 'win32' && child.pid !== undefined) {
129
146
  const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
@@ -150,13 +167,24 @@ function mutationCategory(action) {
150
167
  function mutationVerb(action) {
151
168
  return action === 'remove' ? '开始卸载' : action === 'update' ? '开始更新' : '开始安装';
152
169
  }
170
+ /** 系统日志入口渠道后缀:自定义安装(带渠道)在日志里体现「从哪张卡片发起」,
171
+ * 目录插件安装(无渠道)返回空串。日志中文语境,渠道名用中文口语。 */
172
+ function channelLabel(channel) {
173
+ if (channel === 'git')
174
+ return '(GitHub 源码入口)';
175
+ if (channel === 'dsh')
176
+ return '(DSH 命令入口)';
177
+ if (channel === 'npm')
178
+ return '(NPM 包入口)';
179
+ return '';
180
+ }
153
181
  /**
154
182
  * Enqueue a plugin mutation and return its task. Tasks run strictly serially:
155
183
  * the queue worker starts the next one only after the previous finishes.
156
184
  * Progress is visible through `getTask(id)` / `activeTask()` until done.
157
185
  */
158
186
  export function startPluginMutation(options) {
159
- const task = { 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 };
187
+ const task = { 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 };
160
188
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1;
161
189
  tasks.set(task.id, task);
162
190
  if (tasks.size > MAX_TASKS) {
@@ -169,14 +197,18 @@ export function startPluginMutation(options) {
169
197
  });
170
198
  }
171
199
  queue.push({ task, options: { ...options } });
172
- // 系统日志:记录任务入队(安装/卸载/更新开始)
200
+ // 任务输出首行标记入口渠道(ASCII 标记,乱码免疫):进度面板/失败明细里能溯源「从哪张卡片发起」
201
+ if (options.installChannel !== undefined) {
202
+ pushLine(task, `[install-channel] ${options.installChannel}`);
203
+ }
204
+ // 系统日志:记录任务入队(安装/卸载/更新开始),自定义安装带入口渠道后缀
173
205
  const category = mutationCategory(options.action);
174
206
  appendLog(options.profile, {
175
207
  at: Date.now(),
176
208
  level: 'info',
177
209
  category,
178
210
  event: `${category}.start`,
179
- message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}`,
211
+ message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}${channelLabel(options.installChannel)}`,
180
212
  });
181
213
  pumpQueue();
182
214
  return task;
@@ -202,7 +234,7 @@ function pumpQueue() {
202
234
  level: 'success',
203
235
  category,
204
236
  event: `${category}.done`,
205
- message: `${verb('成功')} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
237
+ message: `${verb('成功')} ${show}${channelLabel(options.installChannel)}${task.needsRestart ? '(待重启生效)' : ''}`,
206
238
  });
207
239
  }
208
240
  else if (task.status === 'failed') {
@@ -211,7 +243,7 @@ function pumpQueue() {
211
243
  level: 'error',
212
244
  category,
213
245
  event: `${category}.fail`,
214
- message: `${verb('失败')} ${show}`,
246
+ message: `${verb('失败')} ${show}${channelLabel(options.installChannel)}`,
215
247
  });
216
248
  }
217
249
  else if (task.status === 'cancelled') {
@@ -220,7 +252,7 @@ function pumpQueue() {
220
252
  level: 'warn',
221
253
  category,
222
254
  event: 'task.cancel',
223
- message: `已取消 ${show}`,
255
+ message: `已取消 ${show}${channelLabel(options.installChannel)}`,
224
256
  });
225
257
  }
226
258
  // 已终态的任务移出队列:既防 queue 数组无限增长(内存泄漏),也让下一轮 pump 干净
@@ -273,6 +305,20 @@ function spawnMutation(options) {
273
305
  let settled = false;
274
306
  let child;
275
307
  try {
308
+ // 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
309
+ // 命令不存在时 Windows shell 模式只回传 GBK 乱码、POSIX 才抛 spawn dsh ENOENT —— 统一前置
310
+ // 拦截并打 ASCII 标记 [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
311
+ if (invocation.file === 'dsh' && !dshOnPath(env)) {
312
+ const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)';
313
+ if (task) {
314
+ task.status = 'failed';
315
+ task.exitCode = null;
316
+ task.progress = 0;
317
+ pushLine(task, detail);
318
+ }
319
+ resolveOnce({ exitCode: 1, timedOut: false, error: detail, stdout: '', stderr: detail });
320
+ return;
321
+ }
276
322
  child = spawn(invocation.file, attemptArgs, {
277
323
  cwd: invocation.cwd,
278
324
  env,
@@ -26,6 +26,9 @@ export interface InstallTask {
26
26
  /** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
27
27
  globalNpm?: string[];
28
28
  action: 'add' | 'remove' | 'update';
29
+ /** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
30
+ * 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装无此字段。 */
31
+ installChannel?: 'npm' | 'git' | 'dsh';
29
32
  /** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
30
33
  status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
31
34
  timedOut: boolean;
@@ -54,6 +57,7 @@ export interface QueueItem {
54
57
  timeoutMs?: number;
55
58
  env?: NodeJS.ProcessEnv;
56
59
  globalNpm?: string[];
60
+ installChannel?: 'npm' | 'git' | 'dsh';
57
61
  };
58
62
  }
59
63
  /**
@@ -11,6 +11,8 @@ export interface ActiveTaskInfo {
11
11
  lines: string[];
12
12
  /** 尝试过的安装方式(npm 反查 + 实际执行命令):失败时前端 issue 预填需要 */
13
13
  attempts: string[];
14
+ /** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):前端刷新恢复后仍能精准溯源 */
15
+ installChannel?: 'npm' | 'git' | 'dsh';
14
16
  }
15
17
  /** All non-terminal tasks in queue order (running first, then pending). Lets the client resume a queue after a page reload. */
16
18
  export declare function activeTask(): ActiveTaskInfo[];
@@ -52,6 +54,9 @@ export declare function startPluginMutation(options: {
52
54
  attempts?: string[];
53
55
  /** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
54
56
  updateNpm?: boolean;
57
+ /** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
58
+ * 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装不传 */
59
+ installChannel?: 'npm' | 'git' | 'dsh';
55
60
  }): InstallTask;
56
61
  /**
57
62
  * 安装后校验已装包的入口文件(package.json 的 main / exports["."].default)是否真实存在。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 4000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):4000+ 人工精选社区插件,每日更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -12,7 +12,7 @@ import { createElement as h, useEffect, useRef, useState } from 'react'
12
12
  import type { MouseEvent } from 'react'
13
13
  import styles from '../styles/Header.module.css'
14
14
  import modalStyles from '../styles/Modal.module.css'
15
- import type { EnvInfo, HubPlugin, SectionProps, ToastState } from '../types.ts'
15
+ import type { EnvInfo, HubPlugin, InstallChannel, SectionProps, ToastState } from '../types.ts'
16
16
  import { installCommandOf, repoFromInstallTarget } from '../logic/install-command.ts'
17
17
  import { PLUGIN_VERSION } from '../logic/constants.ts'
18
18
  import { ensurePluginCss } from '../logic/ensure-plugin-css.ts'
@@ -81,6 +81,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
81
81
  /** 命令行安装确认弹窗:记录待安装的裸目标(npm 包名 / GitHub 地址,DSH 命令已剥成裸目标)——
82
82
  * 与应用商店同一套确认/进度/结果弹窗,提供时 plugin 传 null 走 customTarget 模式 */
83
83
  const [confirmCustomTarget, setConfirmCustomTarget] = useState<string | null>(null)
84
+ /** 自定义安装入口渠道(三张卡片之一):随确认弹窗记录,提交时上报服务端,
85
+ * 报错/日志据此精准溯源「从哪个入口发起」(npm 全局安装固定为 'npm') */
86
+ const [confirmCustomChannel, setConfirmCustomChannel] = useState<InstallChannel | null>(null)
84
87
  /** 全局 npm 安装确认弹窗(官方 `npm install -g <pkgs>`):记录原始命令 + 解析出的包列表,
85
88
  * 走与应用商店同一套确认/进度/结果弹窗(plugin 传 null,InstallModal 走 globalNpm 模式) */
86
89
  const [confirmGlobalNpm, setConfirmGlobalNpm] = useState<{ raw: string; pkgs: string[] } | null>(null)
@@ -212,6 +215,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
212
215
  if (e.key === 'Escape') {
213
216
  setConfirmPlugin(null)
214
217
  setConfirmCustomTarget(null)
218
+ setConfirmCustomChannel(null)
215
219
  setConfirmGlobalNpm(null)
216
220
  setUninstallPlugin(null)
217
221
  setUninstallItem(null)
@@ -412,6 +416,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
412
416
  queue.clearModalTask()
413
417
  setUninstallPlugin(p)
414
418
  },
419
+ // 目录加载失败时的精准提示:设置里配了代理 → 提示代理不可达;没配 → 提示网络不通
420
+ hasProxy: (hubSettings.proxy ?? '').trim() !== '',
421
+ // 「去系统诊断检测网络」直达(复用网络失败弹窗的同一跳转:关弹窗 → 设置 → 系统诊断自动跑连通性检测)
422
+ onOpenDiagnostics: openDiagnostics,
415
423
  })
416
424
  : view === 'installed'
417
425
  ? h(InstalledView, {
@@ -467,11 +475,14 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
467
475
  // 不进插件列表,不做「已安装→更新」判定 —— npm install -g 对已装包是原位覆盖更新,天然幂等)
468
476
  if (opts?.globalNpm && opts.globalNpm.length > 0) {
469
477
  setConfirmCustomTarget(null)
478
+ setConfirmCustomChannel(null)
470
479
  setConfirmIsUpdate(false)
471
480
  setConfirmGlobalNpm({ raw, pkgs: opts.globalNpm })
472
481
  return
473
482
  }
474
483
  setConfirmGlobalNpm(null)
484
+ // 记录入口渠道:报错/日志据此精准提示「从哪张卡片发起的安装」(NPM 包 / GitHub 源码 / DSH 命令)
485
+ setConfirmCustomChannel(opts.installChannel)
475
486
  setConfirmIsUpdate(customTargetInstalled(target))
476
487
  setConfirmCustomTarget(target)
477
488
  },
@@ -631,12 +642,17 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
631
642
  onClose: () => {
632
643
  setInstallDone(false)
633
644
  setConfirmCustomTarget(null)
645
+ setConfirmCustomChannel(null)
634
646
  },
635
647
  onCopy: () => {
636
648
  doCopy(`pnpm add ${confirmCustomTarget}`)
637
649
  setToast({ id: Date.now(), kind: 'copied' })
638
650
  },
639
- onInstall: () => void queue.installCustom(confirmCustomTarget, confirmIsUpdate ? { update: true } : undefined),
651
+ onInstall: () => void queue.installCustom(confirmCustomTarget, {
652
+ update: confirmIsUpdate,
653
+ // 入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):上报服务端,报错/日志据此溯源
654
+ channel: confirmCustomChannel ?? 'npm',
655
+ }),
640
656
  onRestart: () => setShowRestartConfirm(true),
641
657
  }),
642
658
  // 全局 npm 安装确认弹窗(官方 `npm install -g <pkgs>`):与应用商店同一套确认/进度/结果流程。
@@ -657,12 +673,13 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
657
673
  onClose: () => {
658
674
  setInstallDone(false)
659
675
  setConfirmGlobalNpm(null)
676
+ setConfirmCustomChannel(null)
660
677
  },
661
678
  onCopy: () => {
662
679
  doCopy(`npm install -g ${confirmGlobalNpm.pkgs.join(' ')}`)
663
680
  setToast({ id: Date.now(), kind: 'copied' })
664
681
  },
665
- onInstall: () => void queue.installGlobalNpm(confirmGlobalNpm.pkgs),
682
+ onInstall: () => void queue.installGlobalNpm(confirmGlobalNpm.pkgs, { channel: 'npm' }),
666
683
  onRestart: () => setShowRestartConfirm(true),
667
684
  }),
668
685
  // 卸载确认弹窗:确认/进度/结果视图逻辑收敛在 modals.tsx。
@@ -15,7 +15,7 @@ import { SITE_URL } from '../../logic/constants.ts'
15
15
  import { useIncrementalList } from '../../hooks/useIncrementalList.ts'
16
16
  import { PluginCard } from './PluginCard.tsx'
17
17
 
18
- export function CatalogList({ plugins, failed, visible, total, t, langPath, reload, category, copied, installedName, installedVersion, hasUpdate, langKey, onInstall, onUninstall }: {
18
+ export function CatalogList({ plugins, failed, visible, total, t, langPath, reload, category, copied, installedName, installedVersion, hasUpdate, langKey, onInstall, onUninstall, hasProxy, onOpenDiagnostics }: {
19
19
  plugins: HubPlugin[] | null
20
20
  failed: boolean
21
21
  visible: HubPlugin[]
@@ -31,6 +31,10 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
31
31
  langKey: LocaleId
32
32
  onInstall: (p: HubPlugin, opts?: { update?: boolean }) => void
33
33
  onUninstall: (p: HubPlugin) => void
34
+ /** 设置里是否配置了 HTTP 代理:目录拉不出来时据此精准提示(网络不通 vs 代理不可达) */
35
+ hasProxy: boolean
36
+ /** 「去系统诊断检测网络」直达:跳到设置 → 系统诊断自动跑连通性检测 */
37
+ onOpenDiagnostics: () => void
34
38
  }) {
35
39
  /** 列表滚动容器:分类/搜索切换后列表内容替换但 scrollTop 保留,会让用户误以为列表没更新,需重置回顶部 */
36
40
  const listRef = useRef<HTMLDivElement | null>(null)
@@ -46,8 +50,12 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
46
50
  plugins === null && !failed && h('div', { className: styles.state }, t('loading')),
47
51
  failed && h('div', { className: styles.state },
48
52
  h('div', { className: styles.stateTitle }, t('failed')),
49
- h('div', { className: styles.stateDesc }, t('failedDesc')),
50
- h('button', { className: styles.retryBtn, onClick: () => reload() }, t('retry')),
53
+ // 精准提示:配了代理 → 提示代理可能不可达;没配 → 直接提示网络不通
54
+ h('div', { className: styles.stateDesc }, t(hasProxy ? 'failedDescProxy' : 'failedDescNoProxy')),
55
+ h('div', { className: styles.stateActions },
56
+ h('button', { className: styles.retryBtn, onClick: () => reload() }, t('retry')),
57
+ h('button', { className: styles.diagBtn, onClick: onOpenDiagnostics }, t('failNetworkRunDiag')),
58
+ ),
51
59
  ),
52
60
  plugins !== null && !failed && visible.length === 0 && h('div', { className: styles.state },
53
61
  h('div', { className: styles.stateTitle }, t('noResult')),
@@ -266,6 +266,16 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
266
266
  return h('div', { className: styles.failPrepareHint },
267
267
  t(v ? 'failNpmTooLowV' : 'failNpmTooLow', v ? { v } : undefined))
268
268
  }
269
+ if (kind === 'dshMissing') {
270
+ // 找不到 dsh 命令(Win「不是内部或外部命令」/ POSIX「command not found」/ spawn ENOENT):
271
+ // 本机 DeepSeek Harness 未正确安装或 dsh 不在 PATH,不是插件问题 —— 直接给环境修复指引,不引导提 Issue
272
+ return h('div', { className: styles.failPrepareHint }, t('failDshMissingHint'))
273
+ }
274
+ if (kind === 'pnpmMissing') {
275
+ // 找到了 dsh 但缺 pnpm(dsh 用它管理 profile 插件;dsh 报 pnpm not found on PATH):
276
+ // 本机缺 pnpm,不是插件问题 —— 给安装指引,不引导提 Issue
277
+ return h('div', { className: styles.failPrepareHint }, t('failPnpmMissingHint'))
278
+ }
269
279
  if (kind === 'network') {
270
280
  // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
271
281
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
@@ -387,7 +387,15 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
387
387
  failureKind === 'npmTooOld'
388
388
  ? h('div', { className: styles.failPrepareHint },
389
389
  t(npmVersion ? 'failNpmTooLowV' : 'failNpmTooLow', npmVersion ? { v: npmVersion } : undefined))
390
- : failureKind === 'network'
390
+ : failureKind === 'dshMissing'
391
+ // 找不到 dsh 命令(Win「不是内部或外部命令」/ POSIX「command not found」/ spawn ENOENT):
392
+ // 本机 DeepSeek Harness 未正确安装或 dsh 不在 PATH,不是插件问题 —— 直接给环境修复指引,不引导提 Issue
393
+ ? h('div', { className: styles.failPrepareHint }, t('failDshMissingHint'))
394
+ : failureKind === 'pnpmMissing'
395
+ // 找到了 dsh 但缺 pnpm(dsh 用它管理 profile 插件;dsh 报 pnpm not found on PATH):
396
+ // 本机缺 pnpm,不是插件问题 —— 给安装指引,不引导提 Issue
397
+ ? h('div', { className: styles.failPrepareHint }, t('failPnpmMissingHint'))
398
+ : failureKind === 'network'
391
399
  // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
392
400
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
393
401
  // 不引导提 Issue
@@ -20,7 +20,7 @@ import { createElement as h, useState } from 'react'
20
20
  import type { FormEvent, MouseEvent } from 'react'
21
21
  import styles from '../../styles/CustomInstallView.module.css'
22
22
  import modal from '../../styles/Modal.module.css'
23
- import type { Translate } from '../../types.ts'
23
+ import type { InstallChannel, Translate } from '../../types.ts'
24
24
  import { CloseIcon, HelpIcon } from '../ui/icons.tsx'
25
25
 
26
26
  /* —— 自定义安装输入预检 ——
@@ -113,8 +113,10 @@ type HelpTarget = keyof typeof HELP_CARDS
113
113
  export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, enableDsh, onOpenSettings, profile }: {
114
114
  t: Translate
115
115
  /** 命令行安装:输入 npm 包名 / npm、pnpm 安装命令 / GitHub 地址 / dsh plugin 命令即装
116
- * (custom 源,受安全设置开关控制);opts.globalNpm 表示官方 npm 全局安装命令(npm install -g),附带解析出的包列表 */
117
- onInstallCustom: (raw: string, opts?: { globalNpm?: string[] }) => void
116
+ * (custom 源,受安全设置开关控制)。installChannel 必填 = 入口渠道(三张卡片之一):
117
+ * 服务端日志/报错提示据此溯源「从哪个入口发起」;opts.globalNpm 表示官方 npm 全局安装
118
+ * 命令(npm install -g),附带解析出的包列表(仍属 NPM 包入口)。 */
119
+ onInstallCustom: (raw: string, opts: { installChannel: InstallChannel; globalNpm?: string[] }) => void
118
120
  /** 安全信任三通道开关:关掉后对应输入卡片禁用(不提交),并引导去设置打开 */
119
121
  enableNpm: boolean
120
122
  enableGit: boolean
@@ -155,9 +157,9 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
155
157
  const parsed = parseNpmInput(raw)
156
158
  if (parsed === null) { setNpmError(t('npmInstallInvalid')); return }
157
159
  if (parsed.kind === 'global') {
158
- onInstallCustom(raw, { globalNpm: parsed.pkgs })
160
+ onInstallCustom(raw, { installChannel: 'npm', globalNpm: parsed.pkgs })
159
161
  } else {
160
- onInstallCustom(parsed.target)
162
+ onInstallCustom(parsed.target, { installChannel: 'npm' })
161
163
  }
162
164
  setNpmQuery('')
163
165
  setNpmError('')
@@ -166,7 +168,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
166
168
  const target = gitQuery.trim()
167
169
  if (!target) return
168
170
  if (!isGitHubInput(target)) { setGitError(t('gitInstallInvalid')); return }
169
- onInstallCustom(target)
171
+ onInstallCustom(target, { installChannel: 'git' })
170
172
  setGitQuery('')
171
173
  setGitError('')
172
174
  }
@@ -177,7 +179,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
177
179
  if (parsed === null) { setCmdError(t('dshCmdInvalid')); return }
178
180
  // add / update 统一走安装流程:目标已安装时弹窗自动转「更新」覆盖重装(服务端 mode=update 放行),
179
181
  // 未安装时即为普通安装 —— 粘贴 `dsh plugin --profile web update dsh-plugin` 这类命令即可更新到最新
180
- onInstallCustom(parsed.target)
182
+ onInstallCustom(parsed.target, { installChannel: 'dsh' })
181
183
  setCmdQuery('')
182
184
  setCmdError('')
183
185
  }
@@ -17,7 +17,7 @@ import { CatalogList } from '../catalog/CatalogList.tsx'
17
17
 
18
18
  type CatalogState = ReturnType<typeof useCatalog>
19
19
 
20
- export function MarketView({ catalog, t, langPath, langKey, copied, resultText, onInstall, onUninstall }: {
20
+ export function MarketView({ catalog, t, langPath, langKey, copied, resultText, onInstall, onUninstall, hasProxy, onOpenDiagnostics }: {
21
21
  catalog: CatalogState
22
22
  t: (key: string, params?: Record<string, string | number>) => string
23
23
  langPath: string
@@ -26,6 +26,8 @@ export function MarketView({ catalog, t, langPath, langKey, copied, resultText,
26
26
  resultText: string | null
27
27
  onInstall: (p: HubPlugin, opts?: { update?: boolean }) => void
28
28
  onUninstall: (p: HubPlugin) => void
29
+ hasProxy: boolean
30
+ onOpenDiagnostics: () => void
29
31
  }) {
30
32
  return h(Fragment, null,
31
33
  // 分类行保持原位原样:全部 + 各分类 chips
@@ -61,6 +63,8 @@ export function MarketView({ catalog, t, langPath, langKey, copied, resultText,
61
63
  langKey,
62
64
  onInstall,
63
65
  onUninstall,
66
+ hasProxy,
67
+ onOpenDiagnostics,
64
68
  }),
65
69
  )
66
70
  }
@@ -10,7 +10,7 @@
10
10
  * uninstall, cancel) plus the modal task lookups used by the dialogs.
11
11
  */
12
12
  import { useEffect, useRef, useState } from 'react'
13
- import type { HubPlugin, LocaleId, Translate } from '../types.ts'
13
+ import type { HubPlugin, InstallChannel, LocaleId, Translate } from '../types.ts'
14
14
  import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
15
15
  import type { InstalledItem } from '../logic/installed.ts'
16
16
 
@@ -28,6 +28,9 @@ export interface QueueTask {
28
28
  desc?: string
29
29
  /** 所属插件仓库(owner/repo):失败时错误弹窗据此提供「去仓库反馈」入口;卸载为 null */
30
30
  repo: string | null
31
+ /** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令三张卡片):失败时错误/通知精准
32
+ * 溯源「从哪个入口发起」;目录插件安装无此字段(非自定义入口)。 */
33
+ channel?: InstallChannel
31
34
  /** 实际动作(add/update/remove):成功通知据此区分「安装成功」与「更新成功」。
32
35
  * 服务端 /active 带回真实动作;本地乐观入队按 update 标记推断。 */
33
36
  action?: 'add' | 'update' | 'remove'
@@ -89,6 +92,15 @@ export interface TaskQueueOptions {
89
92
  resolvePending: (repo: string) => { desc?: string; version?: string } | null
90
93
  }
91
94
 
95
+ /** 入口渠道的 i18n key:NPM 包 / GitHub 源码 / DSH 命令。 */
96
+ const channelKeyOf = (channel: InstallChannel): string =>
97
+ channel === 'git' ? 'installChannelGit' : channel === 'dsh' ? 'installChannelDsh' : 'installChannelNpm'
98
+
99
+ /** 失败消息的入口溯源前缀:自定义安装(带渠道)在错误正文顶部精准指出从哪张卡片发起;
100
+ * 目录插件安装(无渠道)返回 null,错误正文保持原样。 */
101
+ const entryLineOf = (t: Translate, channel: InstallChannel | undefined): string | null =>
102
+ channel ? t('installEntryLine', { c: t(channelKeyOf(channel)) }) : null
103
+
92
104
  export function useTaskQueue(opts: TaskQueueOptions) {
93
105
  const {
94
106
  t, langKey, refreshInstalled, onInstallDone, onUninstallDone, onError,
@@ -205,10 +217,12 @@ export function useTaskQueue(opts: TaskQueueOptions) {
205
217
  }
206
218
  } else {
207
219
  // 失败:完整展示全部输出行(最新在前,逆序为日志阅读顺序),不裁剪
208
- const detail = lines.length > 0
220
+ const rawDetail = lines.length > 0
209
221
  ? [...lines].reverse().join('\n')
210
222
  : q.kind === 'uninstall' ? t('uninstallFail') : t('installFail')
211
- onError(detail, q.repo, q.kind, q.command, q.attempts, q.action === 'update')
223
+ // 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」,目录安装保持原样
224
+ const entry = entryLineOf(t, q.channel)
225
+ onError(entry ? `${entry}\n${rawDetail}` : rawDetail, q.repo, q.kind, q.command, q.attempts, q.action === 'update')
212
226
  }
213
227
  maybeStopPoll()
214
228
  }
@@ -283,7 +297,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
283
297
  const res = await fetch('/dsh-plugin-hub/active', { cache: 'no-store' })
284
298
  if (!res.ok) throw new Error(`active ${res.status}`)
285
299
  const data = await res.json() as {
286
- tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown }[]
300
+ tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown; installChannel?: unknown }[]
287
301
  pendingRestarts?: unknown
288
302
  }
289
303
  const active = (data.tasks ?? []).filter((a) => typeof a.id === 'number')
@@ -313,6 +327,10 @@ export function useTaskQueue(opts: TaskQueueOptions) {
313
327
  lines: Array.isArray(a.lines) ? (a.lines as string[]) : (prevTask?.lines ?? []),
314
328
  attempts: Array.isArray(a.attempts) ? (a.attempts as string[]) : (prevTask?.attempts ?? []),
315
329
  needsRestart: typeof a.needsRestart === 'boolean' ? a.needsRestart : (prevTask?.needsRestart ?? true),
330
+ // 入口渠道:本地乐观入队已带;服务端 /active 也带回(刷新恢复用),服务端优先
331
+ channel: (a.installChannel === 'npm' || a.installChannel === 'git' || a.installChannel === 'dsh')
332
+ ? a.installChannel
333
+ : prevTask?.channel,
316
334
  })
317
335
  }
318
336
  // 保留乐观条目:服务端尚未登记(preflight 中),无真实 id,不能因 active 缺它就被合并吞掉
@@ -353,7 +371,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
353
371
  const res = await fetch('/dsh-plugin-hub/active', { cache: 'no-store' })
354
372
  if (!res.ok) return
355
373
  const data = await res.json() as {
356
- tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown }[]
374
+ tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown; displayTarget?: unknown; installChannel?: unknown }[]
357
375
  pendingRestarts?: unknown
358
376
  }
359
377
  if (cancelled) return
@@ -379,6 +397,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
379
397
  lines: Array.isArray(a.lines) ? (a.lines as string[]) : [],
380
398
  attempts: Array.isArray((a as { attempts?: unknown }).attempts) ? ((a as { attempts: string[] }).attempts) : [],
381
399
  needsRestart: typeof (a as { needsRestart?: unknown }).needsRestart === 'boolean' ? (a as { needsRestart: boolean }).needsRestart : true,
400
+ // 服务端登记的入口渠道:自定义安装刷新后也能精准溯源(目录安装无此字段)
401
+ channel: (a.installChannel === 'npm' || a.installChannel === 'git' || a.installChannel === 'dsh') ? a.installChannel : undefined,
382
402
  }
383
403
  })
384
404
  if (items.length > 0 || pendingRef.current.length > 0) {
@@ -410,6 +430,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
410
430
  update?: boolean
411
431
  /** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
412
432
  globalNpm?: string[]
433
+ /** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):随请求上报服务端落日志,
434
+ * 失败弹窗/通知据此精准溯源「从哪个入口发起」;目录安装不传。 */
435
+ channel?: InstallChannel
413
436
  }) => {
414
437
  // 防重复入队:同一目标已在排队/执行中则忽略
415
438
  if (queueRef.current.some((q) => q.kind === 'install' && q.target === input.repo)) return
@@ -428,6 +451,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
428
451
  version: input.version, updatedAt: input.updatedAt,
429
452
  status: 'running', progress: 0, lines: [], optimistic: true,
430
453
  command: input.command,
454
+ channel: input.channel,
431
455
  action: input.update ? 'update' : 'add',
432
456
  // 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
433
457
  attempts: [], needsRestart: true,
@@ -446,6 +470,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
446
470
  mode: input.update ? 'update' : undefined,
447
471
  // 界面语言带给服务端:网络预检等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
448
472
  ...(langKey ? { lang: langKey } : {}),
473
+ // 自定义安装入口渠道(三张卡片之一):服务端日志/报错提示据此溯源
474
+ ...(input.channel ? { installChannel: input.channel } : {}),
449
475
  ...(input.globalNpm && input.globalNpm.length > 0 ? { globalNpm: input.globalNpm } : {}),
450
476
  }),
451
477
  signal: controller.signal,
@@ -455,8 +481,11 @@ export function useTaskQueue(opts: TaskQueueOptions) {
455
481
  // 请求层失败(重复安装 409 / 参数错误 / 安全开关 403 等):移除乐观条目 + 完整错误弹窗
456
482
  applyQueue((prev) => prev.filter((x) => x.id !== tempId))
457
483
  // 同步 400(如预检拦截)服务端会附上已尝试的安装方式,issue 预填一并展示;
458
- // 用 || 而非 ?? :error 为空字符串时回退 HTTP 状态,避免复制出来是空白
459
- onError(data.error || `HTTP ${res.status}`, input.repo, 'install', input.command, data.attempts)
484
+ // 用 || 而非 ?? :error 为空字符串时回退 HTTP 状态,避免复制出来是空白。
485
+ // 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」
486
+ const entry = entryLineOf(t, input.channel)
487
+ const msg = data.error || `HTTP ${res.status}`
488
+ onError(entry ? `${entry}\n${msg}` : msg, input.repo, 'install', input.command, data.attempts)
460
489
  return
461
490
  }
462
491
  const taskId = data.task
@@ -476,7 +505,10 @@ export function useTaskQueue(opts: TaskQueueOptions) {
476
505
  const reason = aborted
477
506
  ? t('requestTimeout')
478
507
  : err instanceof Error && err.message ? err.message : String(err ?? '')
479
- onError(reason ? `${t('installFail')} — ${reason}` : t('installFail'), input.repo, 'install', input.command)
508
+ // 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」
509
+ const entry = entryLineOf(t, input.channel)
510
+ const msg = reason ? `${t('installFail')} — ${reason}` : t('installFail')
511
+ onError(entry ? `${entry}\n${msg}` : msg, input.repo, 'install', input.command)
480
512
  } finally {
481
513
  window.clearTimeout(timeout)
482
514
  submittingRef.current.delete(input.repo)
@@ -505,9 +537,11 @@ export function useTaskQueue(opts: TaskQueueOptions) {
505
537
 
506
538
  /** 命令行安装:用户手输 npm 包名或 GitHub 地址,走 custom 源(受安全开关控制)。
507
539
  * update=true 时对已安装目标放行覆盖重装(与目录插件「更新」同一语义)。
540
+ * channel=入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):上报服务端落日志,
541
+ * 失败弹窗/通知据此精准溯源。
508
542
  * 队列展示/防重一律用归一化身份(owner/repo 或 npm 包名):与服务端 /active 合并、
509
543
  * 弹窗目标匹配同口径,npm 包名透传不受影响。 */
510
- const installCustom = async (raw: string, opts?: { update?: boolean }) => {
544
+ const installCustom = async (raw: string, opts?: { update?: boolean; channel?: InstallChannel }) => {
511
545
  const target = raw.trim()
512
546
  if (!target) return
513
547
  const repo = repoFromInstallTarget(target)
@@ -517,12 +551,13 @@ export function useTaskQueue(opts: TaskQueueOptions) {
517
551
  command: `pnpm add ${target}`,
518
552
  source: 'custom',
519
553
  update: opts?.update,
554
+ channel: opts?.channel,
520
555
  })
521
556
  }
522
557
 
523
558
  /** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
524
- * 队列展示/防重用包列表空格拼接的身份(与服务端 target 同口径)。 */
525
- const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean }) => {
559
+ * 队列展示/防重用包列表空格拼接的身份(与服务端 target 同口径);入口渠道固定为 npm(NPM 包卡片)。 */
560
+ const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean; channel?: InstallChannel }) => {
526
561
  const list = (pkgs ?? []).map((p) => p.trim()).filter((p) => p !== '')
527
562
  if (list.length === 0) return
528
563
  const identity = list.join(' ')
@@ -533,6 +568,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
533
568
  source: 'custom',
534
569
  globalNpm: list,
535
570
  update: opts?.update,
571
+ channel: opts?.channel ?? 'npm',
536
572
  })
537
573
  }
538
574