dsh-plugin 1.6.5 → 1.6.7

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.
Files changed (53) hide show
  1. package/README.en.md +11 -11
  2. package/README.md +11 -11
  3. package/client/client.js +421 -390
  4. package/client/client.js.map +1 -1
  5. package/lib/http/catalog.js +132 -0
  6. package/lib/http/diagnostics.js +201 -0
  7. package/lib/http/install.js +399 -0
  8. package/lib/http/logs.js +164 -0
  9. package/lib/http/routes.js +19 -1263
  10. package/lib/http/settings.js +77 -0
  11. package/lib/http/shared.js +169 -0
  12. package/lib/http/task.js +208 -0
  13. package/lib/services/install/install.js +1 -1
  14. package/lib/services/install/package-entry.js +59 -8
  15. package/lib/services/install/preflight.js +47 -17
  16. package/lib/services/install/task-queue.js +75 -31
  17. package/lib/services/probe.js +1 -1
  18. package/lib/types/http/catalog.d.ts +10 -0
  19. package/lib/types/http/diagnostics.d.ts +4 -0
  20. package/lib/types/http/install.d.ts +11 -0
  21. package/lib/types/http/logs.d.ts +2 -0
  22. package/lib/types/http/routes.d.ts +15 -20
  23. package/lib/types/http/settings.d.ts +2 -0
  24. package/lib/types/http/shared.d.ts +44 -0
  25. package/lib/types/http/task.d.ts +3 -0
  26. package/lib/types/services/install/install.d.ts +1 -1
  27. package/lib/types/services/install/package-entry.d.ts +42 -10
  28. package/lib/types/services/install/preflight.d.ts +9 -1
  29. package/lib/types/services/install/task-queue.d.ts +6 -4
  30. package/package.json +1 -1
  31. package/src/client/components/PluginHubSection.tsx +3 -0
  32. package/src/client/components/modals/NotificationsModal.tsx +34 -18
  33. package/src/client/components/modals/modals.tsx +38 -28
  34. package/src/client/components/views/InstalledView.tsx +23 -11
  35. package/src/client/hooks/useCatalog.ts +1 -1
  36. package/src/client/locales.ts +28 -12
  37. package/src/client/logic/failures.ts +36 -157
  38. package/src/client/logic/urls.ts +75 -4
  39. package/src/client/types.ts +1 -1
  40. package/src/server/http/catalog.ts +143 -0
  41. package/src/server/http/diagnostics.ts +193 -0
  42. package/src/server/http/install.ts +391 -0
  43. package/src/server/http/logs.ts +149 -0
  44. package/src/server/http/routes.ts +22 -1222
  45. package/src/server/http/settings.ts +73 -0
  46. package/src/server/http/shared.ts +191 -0
  47. package/src/server/http/task.ts +194 -0
  48. package/src/server/services/install/install-types.ts +9 -1
  49. package/src/server/services/install/install.ts +11 -1
  50. package/src/server/services/install/package-entry.ts +72 -9
  51. package/src/server/services/install/preflight.ts +47 -18
  52. package/src/server/services/install/task-queue.ts +73 -34
  53. package/src/server/services/probe.ts +1 -1
@@ -8,7 +8,7 @@
8
8
  * 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
9
9
  */
10
10
  import { spawn, spawnSync } from 'node:child_process';
11
- import { resolvePackageEntry } from './package-entry.js';
11
+ import { requiredDistributionFiles, resolveBundlePatchFiles } from './package-entry.js';
12
12
  import { existsSync, readFileSync, statSync } from 'node:fs';
13
13
  import { dirname, join, resolve } from 'node:path';
14
14
  import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.js';
@@ -41,12 +41,30 @@ export function activeTask() {
41
41
  for (const item of queue) {
42
42
  const task = item.task;
43
43
  if (task.status === 'pending') {
44
- 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
+ found.push({
45
+ id: task.id,
46
+ target: show(task),
47
+ action: task.action,
48
+ status: 'pending',
49
+ progress: task.progress,
50
+ lines: task.lines.slice(0, MAX_TASK_LINES),
51
+ attempts: task.attempts,
52
+ installChannel: task.installChannel,
53
+ });
45
54
  }
46
55
  }
47
56
  tasks.forEach((task) => {
48
57
  if (task.status === 'running') {
49
- 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 });
58
+ found.unshift({
59
+ id: task.id,
60
+ target: show(task),
61
+ action: task.action,
62
+ status: 'running',
63
+ progress: task.progress,
64
+ lines: task.lines.slice(0, MAX_TASK_LINES),
65
+ attempts: task.attempts,
66
+ installChannel: task.installChannel,
67
+ });
50
68
  }
51
69
  });
52
70
  return found;
@@ -117,7 +135,7 @@ function cliInvocation(profile) {
117
135
  if (entry !== undefined) {
118
136
  // 打包桌面端(Electron):宿主入口在 app.asar 内,机器上根本没有 `dsh` 可执行文件
119
137
  //(Windows 那条 dsh.cmd shim 只存在于宿主进程自己的 PATH 里),直接回退 PATH 必然
120
- // 报 [dsh-missing](见 issue #71)。这里改用应用自己的 CLI 引导脚本调起同一份打包
138
+ // 报 [dsh-missing]。这里改用应用自己的 CLI 引导脚本调起同一份打包
121
139
  // CLI:Electron 可执行文件 + --expose-internals + desktop-cli.js,与内置终端、
122
140
  // 官方插件页安装完全同一条路径 —— desktop profile 由应用独占管理的授权也在其中。
123
141
  const bootstrap = desktopCliBootstrap(entry);
@@ -172,9 +190,8 @@ export function desktopCliBootstrap(entry) {
172
190
  }
173
191
  /**
174
192
  * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
175
- * Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
176
- * 无法匹配),POSIX 非 shell 模式才会抛 spawn dsh ENOENT —— 两种形态都不可靠,统一在
177
- * spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
193
+ * 命令缺失时各平台报错形态不同(Windows shell 只回 GBK 乱码、POSIX 才抛 spawn ENOENT),
194
+ * 统一在 spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
178
195
  * 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
179
196
  */
180
197
  function dshOnPath(env) {
@@ -233,7 +250,22 @@ export function startPluginMutation(options) {
233
250
  // 卸载入队时先快照「该包是否带客户端 UI」:卸载完成后包已从磁盘删除,读不到 package.json,
234
251
  // 只能在此刻取;卸载成功且曾存活时据此决定是否要刷新页面摘除插件面板
235
252
  const hadClientUi = options.action === 'remove' ? hasClientUi(options.profile, options.target) : undefined;
236
- 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, hadClientUi, installChannel: options.installChannel };
253
+ const task = {
254
+ id: nextTaskId,
255
+ target: options.target,
256
+ displayTarget: options.displayTarget,
257
+ globalNpm: options.globalNpm,
258
+ action: options.action,
259
+ status: 'pending',
260
+ timedOut: false,
261
+ exitCode: null,
262
+ progress: 0,
263
+ lines: [],
264
+ attempts: options.attempts ?? [],
265
+ needsRestart: false,
266
+ hadClientUi,
267
+ installChannel: options.installChannel,
268
+ };
237
269
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1;
238
270
  tasks.set(task.id, task);
239
271
  if (tasks.size > MAX_TASKS) {
@@ -367,8 +399,8 @@ function spawnMutation(options) {
367
399
  let child;
368
400
  try {
369
401
  // 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
370
- // 命令不存在时 Windows shell 模式只回传 GBK 乱码、POSIX 才抛 spawn dsh ENOENT —— 统一前置
371
- // 拦截并打 ASCII 标记 [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
402
+ // 命令缺失时各平台报错形态不同(GBK 乱码 / spawn ENOENT),统一前置拦截并打 ASCII 标记
403
+ // [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
372
404
  if (invocation.file === 'dsh' && !dshOnPath(env)) {
373
405
  const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)';
374
406
  if (task) {
@@ -494,12 +526,9 @@ function spawnMutation(options) {
494
526
  });
495
527
  return (async () => {
496
528
  let result = await spawnOnce(args);
497
- // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`):
498
- // 某些包的 peerDependencies 用「双段 prerelease 范围」(如 dsh-tui@0.9.0 的 `^0.1.0-rc.6 || ^0.1.1-rc.1`,
499
- // 26 个必需 peer 全是这种形状)会让 `npm install -g` 在解析阶段直接崩溃 —— 退出码 1、什么都没装,
500
- // 与是不是从我们界面发起无关(终端里同样复现)。首次失败后自动带 `--legacy-peer-deps` 重试一次:
501
- // 跳过 peer 自动解析即可绕过该 bug(peer 通常由同装的 @deepseek-ai/dsh 依赖树天然提供)。
502
- // 二次尝试仍失败则保留其错误信息,不掩盖真正的原因。
529
+ // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`)
530
+ // 会让 `npm install -g` 在解析阶段直接崩溃。首次失败后自动带 `--legacy-peer-deps` 重试一次:
531
+ // 跳过 peer 自动解析即可绕过该 bug;二次尝试仍失败则保留其错误信息,不掩盖真正的原因。
503
532
  if (isGlobalNpm && result.exitCode !== 0 && !result.timedOut && task?.status !== 'cancelled') {
504
533
  const retryArgs = ['install', '-g', '--legacy-peer-deps', ...(globalNpm ?? [])];
505
534
  const retryExecuted = `npm install -g --legacy-peer-deps ${(globalNpm ?? []).join(' ')}`;
@@ -562,10 +591,12 @@ function spawnMutation(options) {
562
591
  })();
563
592
  }
564
593
  /**
565
- * 安装后校验已装包的入口文件(package.json 的 main / exports["."].default)是否真实存在。
566
- * git: 分发常不提交构建产物(lib/ 等),pnpm 装完没有报错,但宿主重启加载插件树时会
567
- * ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在登记「待重启」前就把这类残缺包拦下。
568
- * 返回 { name, missing }:name 为空表示无法定位目标包(跳过校验);missing 为缺失的入口路径。
594
+ * 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
595
+ * dsh.bundle.patch)是否真实存在。git: 分发常不提交构建产物(lib/ 等),pnpm 装完没有
596
+ * 报错,但宿主重启加载插件树时会 ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在
597
+ * 登记「待重启」前就把这类残缺包拦下。没有声明过的文件不在这里追究:没声明入口的
598
+ * bundle 包(只挂技能)本来就不需要入口,宿主读的是它的 package.json 与声明的补丁。
599
+ * 返回 { name, missing }:name 为空表示无法定位目标包(跳过校验);missing 为缺失文件的路径。
569
600
  */
570
601
  export function verifyInstalledEntry(profile, target) {
571
602
  let profilePkg = null;
@@ -584,20 +615,33 @@ export function verifyInstalledEntry(profile, target) {
584
615
  if (!name)
585
616
  return { name: null, missing: null };
586
617
  const pkgDir = join(profileDirectory(profile), 'node_modules', name);
587
- let entry = null;
618
+ let required = [];
588
619
  try {
589
620
  const meta = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
590
- entry = resolvePackageEntry(meta);
621
+ // 补丁按本包名 mount 自己时入口也是必须的,所以先读已装包里的补丁(读不到就当没有)。
622
+ const patchTexts = resolveBundlePatchFiles(meta).flatMap((file) => {
623
+ try {
624
+ return [readFileSync(join(pkgDir, file), 'utf8')];
625
+ }
626
+ catch {
627
+ return [];
628
+ }
629
+ });
630
+ required = requiredDistributionFiles(meta, patchTexts);
591
631
  }
592
632
  catch {
593
633
  return { name, missing: null };
594
634
  }
595
- try {
596
- return { name, missing: statSync(join(pkgDir, entry)).isFile() ? null : entry };
597
- }
598
- catch {
599
- return { name, missing: entry };
635
+ for (const file of required) {
636
+ try {
637
+ if (!statSync(join(pkgDir, file)).isFile())
638
+ return { name, missing: file };
639
+ }
640
+ catch {
641
+ return { name, missing: file };
642
+ }
600
643
  }
644
+ return { name, missing: null };
601
645
  }
602
646
  /** 从 pnpm `Ignored build scripts:` 提示行解析被拦截的构建脚本 key(name@version,逗号分隔)。 */
603
647
  function ignoredBuildKeys(output) {
@@ -611,7 +655,7 @@ function ignoredBuildKeys(output) {
611
655
  }
612
656
  /** npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 把 [npm-too-low] 标记追加到任务输出,
613
657
  * 前端据此展示「本机 npm 版本过低」的准确原因 —— 避免该报错被外层 ERR_PNPM_PREPARE_PACKAGE
614
- * 误判成插件打包/分发问题、误导用户提 Issue。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
658
+ * 误判成插件打包/分发问题。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
615
659
  function annotateNpmTooLow(result, options) {
616
660
  if (options.action === 'remove' || result.exitCode === 0 || result.timedOut)
617
661
  return result;
@@ -673,21 +717,21 @@ export async function runPluginMutation(options) {
673
717
  }
674
718
  }
675
719
  }
676
- // pnpm 安装本身成功,但目标包入口文件缺失(git 分发缺构建产物)→ 判为失败:
720
+ // pnpm 安装本身成功,但目标包声明分发的文件缺失(git 分发缺构建产物 / 缺声明的补丁)→ 判为失败:
677
721
  // 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
678
722
  // 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
679
723
  if (options.action !== 'remove' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
680
724
  const verified = verifyInstalledEntry(options.profile, options.target);
681
725
  if (verified.name !== null && verified.missing !== null) {
682
726
  clearPendingRestart(options.displayTarget ?? options.target);
683
- const detail = `[packaging] ${verified.name}: entry file missing: ${verified.missing} (git distribution lacks build output — install the npm version or report to the author)`;
727
+ const detail = `[packaging] ${verified.name}: declared file missing: ${verified.missing} (git distribution lacks build output — install the npm version or report to the author)`;
684
728
  if (options.task && options.task.status !== 'cancelled') {
685
729
  options.task.status = 'failed';
686
730
  options.task.exitCode = null;
687
731
  options.task.progress = 0;
688
732
  pushLine(options.task, detail);
689
733
  }
690
- return { exitCode: 1, timedOut: false, error: `entry file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` };
734
+ return { exitCode: 1, timedOut: false, error: `declared file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` };
691
735
  }
692
736
  }
693
737
  // npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
@@ -140,7 +140,7 @@ function runCurl(args, env, timeoutMs) {
140
140
  * 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败;
141
141
  * 降级后仍被吊销检查拦死(CRYPT_E_NO_REVOCATION_CHECK)时,再用 --ssl-no-revoke
142
142
  * 彻底关掉吊销检查跑一次 —— 该参数 curl ≥ 7.19 即存在,覆盖面比降级参数更广,
143
- * 否则「能上网但取不到 CRL」的 Windows 机器会把所有通道误报成不可达(见 issue #71)。
143
+ * 否则「能上网但取不到 CRL」的 Windows 机器会把所有通道误报成不可达。
144
144
  * 吊销检查只影响本次探测自身的信任判断,不改变系统 TLS 配置。 */
145
145
  async function curlWithTlsFallback(args, env, timeoutMs) {
146
146
  const tlsArgs = curlTlsArgs();
@@ -0,0 +1,10 @@
1
+ import { type WebRoute, type WebServerService } from './shared.ts';
2
+ export declare function profileDirectory(profile: string): string;
3
+ /** Read non-official dependencies installed into one profile. */
4
+ export declare function readInstalled(profile: string): Record<string, string>;
5
+ /** 读已装依赖实际落在磁盘上的仓库身份(`owner/repo`):解析 node_modules/<name>/package.json
6
+ * 的 repository 字段。已装 spec 可能是纯版本号(丢了仓库身份),但真实包元数据仍指向来源仓库,
7
+ * 据此可区分「同名依赖其实是同一仓库的残留(应转 update)」还是「跨仓库包名冲突(应报错)」。
8
+ * 读不到 / 解析不出返回 null(交给调用方按冲突处理)。 */
9
+ export declare function installedRepoOf(profile: string, name: string): string | null;
10
+ export declare function registerCatalogRoutes(webServer: WebServerService, profile: string): WebRoute[];
@@ -0,0 +1,4 @@
1
+ import { type WebRoute, type WebServerService } from './shared.ts';
2
+ /** 宿主环境快照:提交 bug 时随 issue 附上,便于作者复现(前端 /env 端点返回)。 */
3
+ export declare function hostEnv(profile: string): Record<string, string | null>;
4
+ export declare function registerDiagnosticsRoutes(webServer: WebServerService, profile: string): WebRoute[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Install/uninstall routes: validate the target and spawn the official dsh CLI
7
+ * (see services/install/install.ts), reporting the captured result back.
8
+ */
9
+ import { type LoaderHandle } from '../services/install/install.ts';
10
+ import { type WebRoute, type WebServerService } from './shared.ts';
11
+ export declare function registerInstallRoutes(webServer: WebServerService, profile: string, loader: LoaderHandle | undefined): WebRoute[];
@@ -0,0 +1,2 @@
1
+ import { type WebRoute, type WebServerService } from './shared.ts';
2
+ export declare function registerLogsRoutes(webServer: WebServerService, profile: string): WebRoute[];
@@ -1,24 +1,15 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { readProfileArg, type LoaderHandle } from '../services/install/install.ts';
3
1
  /**
4
- * 宿主是否由桌面应用壳托管(`ELECTRON_RUN_AS_NODE=1`:壳用 Electron 二进制以 Node 模式跑宿主)。
5
- * 桌面端的宿主归应用壳管,插件重启不了它,也不该去 kill —— 详见 /restart 路由。
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Local HTTP routes exposing real installs to the in-app Plugin Hub UI.
7
+ * The client fetches the same-origin `/dsh-plugin-hub/*` endpoints; the
8
+ * install handler validates the target, then spawns the official dsh CLI
9
+ * (see services/install/install.ts) and reports the captured result back.
6
10
  */
7
- export declare function isDesktopHost(): boolean;
8
- export interface WebRoute {
9
- kind: 'exact';
10
- path: string;
11
- handler: (request: IncomingMessage, response: ServerResponse) => void | Promise<void>;
12
- }
13
- /** The subset of the host web-server service this plugin touches. */
14
- export interface WebServerService {
15
- register(route: WebRoute): () => void;
16
- }
17
- /** Read non-official dependencies installed into one profile. */
18
- export declare function readInstalled(profile: string): Record<string, string>;
19
- /** POST mutations are only accepted from the local web server origin or the desktop host page.
20
- * Exported for tests (tests/routes.test.ts) — the origin check is the whole CSRF defence. */
21
- export declare function isSameOrigin(request: IncomingMessage): boolean;
11
+ import { type LoaderHandle } from '../services/install/install.ts';
12
+ import { type WebServerService } from './shared.ts';
22
13
  /**
23
14
  * Register the Plugin Hub API on the host web server and return a disposer.
24
15
  * @param webServer - DSH web server service.
@@ -28,4 +19,8 @@ export declare function isSameOrigin(request: IncomingMessage): boolean;
28
19
  */
29
20
  export declare function mountPluginHubRoutes(webServer: WebServerService, profile: string, loader?: LoaderHandle): () => void;
30
21
  /** Profile resolution shared with the client route docs. */
31
- export { readProfileArg };
22
+ export { readProfileArg } from '../services/install/install.ts';
23
+ /** Re-exported so the host entry (index.ts) keeps importing it from this module. */
24
+ export type { WebServerService } from './shared.ts';
25
+ /** Re-exported so tests/routes.test.ts keeps importing these from this module. */
26
+ export { isDesktopHost, isSameOrigin } from './shared.ts';
@@ -0,0 +1,2 @@
1
+ import { type WebRoute, type WebServerService } from './shared.ts';
2
+ export declare function registerSettingsRoutes(webServer: WebServerService, profile: string): WebRoute[];
@@ -0,0 +1,44 @@
1
+ /**
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Shared HTTP utilities: route/service types, request validation helpers and
7
+ * the same-origin check guarding every POST mutation. Imported by the other
8
+ * route modules in this directory.
9
+ */
10
+ import type { IncomingMessage, ServerResponse } from 'node:http';
11
+ /**
12
+ * 跨平台宿主重启脚本(以 `node -e` 运行,独立于宿主进程)。
13
+ * 不用 `/bin/sh -c`:Windows 无 /bin/sh,且 lsof/nohup 仅 POSIX 存在。
14
+ * 由 `spawn(process.execPath, ['-e', SCRIPT, port], { detached, stdio:'ignore' })` 孵化,
15
+ * 宿主进程被 kill 后仍能完成「停旧 → 等端口释放 → 拉起新 dsh web」。
16
+ * 端口探测不到监听进程时不再假装重启(记一行日志后退出,见脚本内注释)。
17
+ */
18
+ export declare const HOST_RESTART_SCRIPT: string;
19
+ /**
20
+ * 宿主是否由桌面应用壳托管(`ELECTRON_RUN_AS_NODE=1`:壳用 Electron 二进制以 Node 模式跑宿主)。
21
+ * 桌面端的宿主归应用壳管,插件重启不了它,也不该去 kill —— 详见 /restart 路由。
22
+ */
23
+ export declare function isDesktopHost(): boolean;
24
+ export interface WebRoute {
25
+ kind: 'exact';
26
+ path: string;
27
+ handler: (request: IncomingMessage, response: ServerResponse) => void | Promise<void>;
28
+ }
29
+ /** The subset of the host web-server service this plugin touches. */
30
+ export interface WebServerService {
31
+ register(route: WebRoute): () => void;
32
+ }
33
+ export declare const PROFILE_RE: RegExp;
34
+ export declare const COMMAND_TIMEOUT_MS: number;
35
+ /** 目录/统计数据代理的本地缓存时长:1 小时内重复打开插件市场直接读盘,
36
+ * 不重复走 curl 拉远程 —— 重启宿主后首次打开同样秒开(缓存跨重启存活)。 */
37
+ export declare const CATALOG_CACHE_TTL_MS: number;
38
+ export declare function sendJson(response: ServerResponse, status: number, value: unknown): void;
39
+ /** POST mutations are only accepted from the local web server origin or the desktop host page.
40
+ * Exported for tests (tests/routes.test.ts) — the origin check is the whole CSRF defence. */
41
+ export declare function isSameOrigin(request: IncomingMessage): boolean;
42
+ export declare function requireMethod(request: IncomingMessage, response: ServerResponse, method: 'GET' | 'POST'): boolean;
43
+ export declare function requireTrustedPost(request: IncomingMessage, response: ServerResponse): boolean;
44
+ export declare function readJsonBody(request: IncomingMessage): Promise<unknown>;
@@ -0,0 +1,3 @@
1
+ import { type LoaderHandle } from '../services/install/install.ts';
2
+ import { type WebRoute, type WebServerService } from './shared.ts';
3
+ export declare function registerTaskRoutes(webServer: WebServerService, profile: string, loader: LoaderHandle | undefined): WebRoute[];
@@ -18,5 +18,5 @@ export type { InstallResult, InstallTask, Invocation, PendingRestart, QueueItem
18
18
  export type { LoaderHandle } from '../loader.ts';
19
19
  export { addPendingRestart, clearPendingRestart, listPendingRestarts } from '../profile/pending-restart.ts';
20
20
  export { dumpLoaderEntries, removeLoadedEntry } from '../loader.ts';
21
- export { addAllowBuildsKey, githubRefOf, githubRepoOf, githubTarget, globalNpmPackagesOf, installTargetOf, parseAllowBuildsKey, readProfileArg, validPackageName } from '../profile/profile.ts';
21
+ export { addAllowBuildsKey, githubRefOf, githubRepoOf, githubTarget, globalNpmPackagesOf, installTargetOf, parseAllowBuildsKey, readProfileArg, validPackageName, } from '../profile/profile.ts';
22
22
  export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.ts';
@@ -1,21 +1,53 @@
1
1
  /**
2
- * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * 包入口解析:从 package.json 的 exports["."] / main 推出用于校验的入口文件。
7
- * git 通道(tar 预检)与 npm 通道(装后 statSync 校验)共用,保证两条通道
8
- * 对同一个包判定一致。
6
+ * 分发文件解析:从 package.json 推出装完后必须真实存在的文件 —— 作者声明的入口
7
+ * (exports["."] / main)与 bundle 声明的补丁(dsh.bundle.patch)。git 通道(tar 预检)
8
+ * 与 npm 通道(装后 statSync 校验)共用,保证两条通道对同一个包判定一致。
9
+ *
10
+ * 这里不替作者补一个 index.js:宿主加载一个 bundle 读的是它的 package.json 和它声明的
11
+ * 补丁文件(官方 bundleManifest 的注释即「Read bundle metadata without loading its
12
+ * JavaScript」),从不 import 包本身。凭空要求一个作者没声明的入口,只会把完整的
13
+ * 技能包判成分发残缺,并把一条假缺陷推到作者仓库(见 QingYunA/answer-me-with-html#209)。
14
+ *
15
+ * 例外是补丁自己 mount 了本包:dsh 的《打包与安装插件》里组合包的常规写法就是
16
+ * `- insert: - name: <本包名>`,宿主随后按包名 import 本包,入口那时必须存在。
17
+ * 所以只有补丁真的写了自己的包名,才把这个入口算进分发要求。
9
18
  */
19
+ /** 解析所需的 manifest 字段;其余字段不读。 */
20
+ export interface PackageManifest {
21
+ name?: unknown;
22
+ main?: unknown;
23
+ exports?: unknown;
24
+ dsh?: unknown;
25
+ }
10
26
  /**
11
- * 解析包的入口文件(相对包根,不带 `./` 前缀)。
27
+ * 解析作者声明的入口文件(相对包根,不带 `./` 前缀)。manifest 未声明入口时返回 null:
28
+ * 没有声明就没有可缺失的入口,是否可 import 交给真实安装与宿主加载去暴露。
12
29
  *
13
30
  * Node 的 exports 规范要求字符串 target 以 `./` 开头(如 `"./lib/index.js"`),
14
31
  * 而 tar 成员名与磁盘路径都不带该前缀,因此统一剥掉;条件对象
15
32
  * (`{ "import": ..., "require": ... }`)按 default → import → require 取第一
16
- * 个字符串值。无 exports 时回退 main,再回退 index.js。
33
+ * 个字符串值。无 exports 时回退 main;两者都没有则没有入口。
17
34
  */
18
- export declare function resolvePackageEntry(meta: {
19
- main?: unknown;
20
- exports?: unknown;
21
- }): string;
35
+ export declare function resolvePackageEntry(meta: PackageManifest): string | null;
36
+ /**
37
+ * 解析 `dsh.bundle.patch` 声明的补丁文件(相对包根,不带 `./` 前缀)。
38
+ * 形状不合规范(既不是字符串也不是字符串数组)在这里不判定:那是 dsh 启动时
39
+ * bundlePatchFiles 的报错职责,预检只负责「声明了就要在包里存在」。
40
+ */
41
+ export declare function resolveBundlePatchFiles(meta: PackageManifest): string[];
42
+ /** 分发里必须存在的全部文件:作者声明的入口,加上 bundle 声明的补丁,入口在前。
43
+ * manifest 没声明入口时才看补丁:补丁按本包名 mount 自己(下面 mountsItself),
44
+ * 宿主就会 import 本包,Node 的旧式解析落到 index.js —— 那个入口同样必须有。 */
45
+ export declare function requiredDistributionFiles(meta: PackageManifest, patchTexts?: string[]): string[];
46
+ /**
47
+ * 补丁文本里插件行的 `name:` 值(DSH 补丁按行书写:`- name: <pkg>` / ` name: <pkg>`)。
48
+ * 刻意不做 YAML 解析:只认这一种行形状,认不出来就不判定 —— 漏判只是少一条要求,
49
+ * 误判会拦下能装的包,两边的代价不对称。
50
+ */
51
+ export declare function patchRowNamesOf(text: string): string[];
52
+ /** 补丁是否把本包自己 mount 进树(dsh 文档里组合包的常规写法),是则宿主会 import 本包。 */
53
+ export declare function mountsItself(meta: PackageManifest, patchTexts: string[]): boolean;
@@ -1,4 +1,5 @@
1
- /** 预检结果:ok=false 表示分发改入口文件缺失,missing 为该文件在包内的相对路径;
1
+ /** 预检结果:ok=false 表示分发改 package.json 声明的文件(入口或 bundle 补丁),
2
+ * missing 为该文件在包内的相对路径;
2
3
  * name 为 git 分发包 package.json 声明的包名(非 git 目标/无法确定时为 null),
3
4
  * 供安装路由做「包名冲突」检测 —— 同名包名已被其他来源占用时,pnpm 装前必然撞车,
4
5
  * 需要把晦涩的 CLI 报错转成明确的拦截。 */
@@ -13,3 +14,10 @@ export declare function preflightTarget(target: string): Promise<PreflightResult
13
14
  * single archive root; nested monorepo package manifests are intentionally
14
15
  * ignored. Exported for a small parser regression test. */
15
16
  export declare function rootPackageEntryOfTarList(listing: string): string | null;
17
+ /**
18
+ * 清单里第一个缺失的必需文件(成员名带包根前缀,由调用方拼好),全部都在则为 null。
19
+ * 清单不可读(tar 缺失/解压失败)时按第一个必需文件缺失处理:与逐文件查询时
20
+ * 「查不到就是缺」的判定一致,宁可拦下一次可疑安装,也不放一个空包过去。
21
+ * Exported for a small parser regression test.
22
+ */
23
+ export declare function missingDistributionFileOfTarList(listing: string | null, required: string[]): string | null;
@@ -67,10 +67,12 @@ export declare function startPluginMutation(options: {
67
67
  installChannel?: 'npm' | 'git' | 'dsh';
68
68
  }): InstallTask;
69
69
  /**
70
- * 安装后校验已装包的入口文件(package.json 的 main / exports["."].default)是否真实存在。
71
- * git: 分发常不提交构建产物(lib/ 等),pnpm 装完没有报错,但宿主重启加载插件树时会
72
- * ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在登记「待重启」前就把这类残缺包拦下。
73
- * 返回 { name, missing }:name 为空表示无法定位目标包(跳过校验);missing 为缺失的入口路径。
70
+ * 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
71
+ * dsh.bundle.patch)是否真实存在。git: 分发常不提交构建产物(lib/ 等),pnpm 装完没有
72
+ * 报错,但宿主重启加载插件树时会 ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在
73
+ * 登记「待重启」前就把这类残缺包拦下。没有声明过的文件不在这里追究:没声明入口的
74
+ * bundle 包(只挂技能)本来就不需要入口,宿主读的是它的 package.json 与声明的补丁。
75
+ * 返回 { name, missing }:name 为空表示无法定位目标包(跳过校验);missing 为缺失文件的路径。
74
76
  */
75
77
  export declare function verifyInstalledEntry(profile: string, target: string): {
76
78
  name: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 10000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):10000+ 人工精选社区插件,每日更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -460,6 +460,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
460
460
  // 支持系统文件管理器定位的平台(macOS / Linux)上,行内「打开详情」替换为
461
461
  // 打开安装目录按钮(行点击本身即打开详情弹窗);Windows 不支持则按钮不显示
462
462
  platform: env?.platform ?? '',
463
+ // 待重启列表(owner/repo):用于区分「待重启」与「已重启仍加载失败」,
464
+ // 后者不再无限显示「重启」,改为「未激活」灰点
465
+ pendingRestarts: queue.pendingRestarts.map((p) => p.target),
463
466
  // 行点击 / 详情按钮:打开已安装详情弹窗(完整元数据 + 运行时信息)
464
467
  onOpenDetail: (item) => setDetailItem(item),
465
468
  onReveal: (item) => { void revealFolder(item) },
@@ -20,7 +20,7 @@ import type { EnvInfo, Translate } from '../../types.ts'
20
20
  import type { NotificationRecord } from '../../logic/failures.ts'
21
21
  import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
22
22
  import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
23
- import { pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
23
+ import { pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
24
24
  import { CloseIcon } from '../ui/icons.tsx'
25
25
  import { ConfirmDialog } from './ConfirmDialog.tsx'
26
26
 
@@ -263,13 +263,13 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
263
263
  t(v ? 'failNpmTooLowV' : 'failNpmTooLow', v ? { v } : undefined))
264
264
  }
265
265
  if (kind === 'dshMissing') {
266
- // 找不到 dsh 命令(Win「不是内部或外部命令」/ POSIX「command not found」/ spawn ENOENT):
267
- // 本机 DeepSeek Harness 未正确安装或 dsh 不在 PATH,不是插件问题 —— 直接给环境修复指引,不引导提 Issue
266
+ // 找不到 dsh 命令:本机 DeepSeek Harness 未正确安装或 dsh 不在 PATH,
267
+ // 不是插件问题 —— 直接给环境修复指引,不引导提 Issue
268
268
  return h('div', { className: styles.failPrepareHint }, t('failDshMissingHint'))
269
269
  }
270
270
  if (kind === 'gitMissing') {
271
- // 找不到 git 命令(Win「'git' is not recognized」/ POSIX「git: command not found」/ spawn ENOENT):
272
- // 本机 Git 未安装或不在 PATH,不是插件问题 —— 直接给环境修复指引,不引导提 Issue
271
+ // 找不到 git 命令:本机 Git 未安装或不在 PATH,不是插件问题 ——
272
+ // 直接给环境修复指引,不引导提 Issue
273
273
  return h('div', { className: styles.failPrepareHint }, t('failGitMissingHint'))
274
274
  }
275
275
  if (kind === 'pnpmMissing') {
@@ -314,13 +314,25 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
314
314
  h('div', { className: styles.failPrepareHint }, t('failPnpmMissingDepHint')),
315
315
  ])
316
316
  }
317
+ if (kind === 'registryVersionStale') {
318
+ // 要装的版本在「实际执行安装的那个源」里查不到(ERR_PNPM_NO_MATCHING_VERSION / ETARGET):
319
+ // 常见于镜像滞后 —— 反查包名走设置里的 npmRegistry,实际安装沿用本机 ~/.npmrc 的源;
320
+ // 也可能只是版本号写错。与插件作者无关 —— 给对齐源 / 显式走官方源 / 离线包指引,不引导提 Issue
321
+ return h('div', { className: styles.failPrepareHint }, t('failRegistryVersionStaleHint'))
322
+ }
323
+ if (kind === 'pnpmLinkedDirMissing') {
324
+ // profile 里留着指向不存在目录的本地依赖(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND):
325
+ // 常见于 link:/本地路径装过开发中的插件、之后目录被删;与本次要装的插件无关
326
+ // —— 给删除悬空条目的指引,不引导提 Issue
327
+ return h('div', { className: styles.failPrepareHint }, t('failPnpmLinkedDirMissingHint'))
328
+ }
317
329
  if (kind === 'fileLocked') {
318
330
  // profile 里的文件被其他进程占用(Windows os error 32):通常是宿主或杀毒软件
319
331
  // 持有句柄 —— 给出完全退出宿主的指引,不引导提 Issue
320
332
  return h('div', { className: styles.failPrepareHint }, t('failFileLockedHint'))
321
333
  }
322
334
  if (kind === 'accessDenied') {
323
- // profile 里的文件被系统拒绝写入(Windows os error 5「拒绝访问」/ EPERM / EACCES):
335
+ // profile 里的文件被系统拒绝写入(os error 5 / EPERM / EACCES):
324
336
  // 占用、只读属性、目录 ACL 或杀软拦截 —— 给退出宿主 + 检查权限的指引,不引导提 Issue
325
337
  return h('div', { className: styles.failPrepareHint }, t('failAccessDeniedHint'))
326
338
  }
@@ -351,13 +363,11 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
351
363
  }, t('failNetworkRunDiag')) : null,
352
364
  ])
353
365
  }
354
- if (kind === 'pluginPrepare' || kind === 'pnpmIgnoredBuild') {
366
+ if (kind === 'pluginPrepare') {
355
367
  // [packaging](预检/装后校验拦截:git 分发缺产物)与
356
- // prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
368
+ // prepare 构建脚本实际执行失败:唯一确有证据的插件打包分发问题 —— 引导到插件仓库提 Issue
357
369
  return h('div', null, [
358
- h('div', { className: styles.failPrepareHint }, kind === 'pnpmIgnoredBuild'
359
- ? t('failIgnoredBuild')
360
- : /\[packaging\]/i.test(r.message) ? t('failPackagingHint') : t('failPrepareHint')),
370
+ h('div', { className: styles.failPrepareHint }, /\[packaging\]/i.test(r.message) ? t('failPackagingHint') : t('failPrepareHint')),
361
371
  r.repo && shownRepo ? h('a', {
362
372
  className: styles.failBigIssue,
363
373
  href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
@@ -367,13 +377,19 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
367
377
  }, t('failIssueBig')) : null,
368
378
  ])
369
379
  }
370
- return r.repo && shownRepo ? h('a', {
371
- className: styles.failBigIssue,
372
- href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
373
- target: '_blank',
374
- rel: 'noopener noreferrer',
375
- title: t('failIssueHint'),
376
- }, t('failIssueBig')) : null
380
+ // pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
381
+ return h('div', null, [
382
+ kind === 'pnpmIgnoredBuild'
383
+ ? h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild'))
384
+ : h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint')),
385
+ h('a', {
386
+ className: styles.failBigIssue,
387
+ href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts),
388
+ target: '_blank',
389
+ rel: 'noopener noreferrer',
390
+ title: t('failHubIssueHint'),
391
+ }, t('failHubIssueBig')),
392
+ ])
377
393
  })(),
378
394
  ),
379
395
  ),