dsh-plugin 1.6.4 → 1.6.6

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 (51) hide show
  1. package/README.en.md +11 -11
  2. package/README.md +11 -11
  3. package/client/client.js +450 -405
  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 -1261
  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/npm-resolve.js +44 -15
  15. package/lib/services/install/task-queue.js +45 -16
  16. package/lib/services/probe.js +18 -1
  17. package/lib/types/http/catalog.d.ts +10 -0
  18. package/lib/types/http/diagnostics.d.ts +4 -0
  19. package/lib/types/http/install.d.ts +11 -0
  20. package/lib/types/http/logs.d.ts +2 -0
  21. package/lib/types/http/routes.d.ts +15 -20
  22. package/lib/types/http/settings.d.ts +2 -0
  23. package/lib/types/http/shared.d.ts +44 -0
  24. package/lib/types/http/task.d.ts +3 -0
  25. package/lib/types/services/install/install.d.ts +1 -1
  26. package/lib/types/services/install/npm-resolve.d.ts +3 -2
  27. package/lib/types/services/probe.d.ts +8 -0
  28. package/package.json +1 -1
  29. package/src/client/components/PluginHubSection.tsx +7 -2
  30. package/src/client/components/modals/HubUpdateModal.tsx +19 -10
  31. package/src/client/components/modals/NotificationsModal.tsx +34 -18
  32. package/src/client/components/modals/modals.tsx +38 -28
  33. package/src/client/components/views/InstalledView.tsx +23 -11
  34. package/src/client/hooks/useCatalog.ts +13 -9
  35. package/src/client/locales.ts +28 -8
  36. package/src/client/logic/failures.ts +36 -157
  37. package/src/client/logic/urls.ts +72 -1
  38. package/src/client/types.ts +11 -0
  39. package/src/server/http/catalog.ts +143 -0
  40. package/src/server/http/diagnostics.ts +193 -0
  41. package/src/server/http/install.ts +391 -0
  42. package/src/server/http/logs.ts +149 -0
  43. package/src/server/http/routes.ts +22 -1220
  44. package/src/server/http/settings.ts +73 -0
  45. package/src/server/http/shared.ts +191 -0
  46. package/src/server/http/task.ts +194 -0
  47. package/src/server/services/install/install-types.ts +9 -1
  48. package/src/server/services/install/install.ts +11 -1
  49. package/src/server/services/install/npm-resolve.ts +41 -13
  50. package/src/server/services/install/task-queue.ts +45 -16
  51. package/src/server/services/probe.ts +19 -1
@@ -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(' ')}`;
@@ -611,7 +640,7 @@ function ignoredBuildKeys(output) {
611
640
  }
612
641
  /** npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 把 [npm-too-low] 标记追加到任务输出,
613
642
  * 前端据此展示「本机 npm 版本过低」的准确原因 —— 避免该报错被外层 ERR_PNPM_PREPARE_PACKAGE
614
- * 误判成插件打包/分发问题、误导用户提 Issue。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
643
+ * 误判成插件打包/分发问题。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
615
644
  function annotateNpmTooLow(result, options) {
616
645
  if (options.action === 'remove' || result.exitCode === 0 || result.timedOut)
617
646
  return result;
@@ -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();
@@ -235,6 +235,23 @@ export function gitLsRemote(url, proxy, timeoutMs) {
235
235
  const timer = setTimeout(() => { child.kill(); finish(probeFail()); }, timeoutMs + 1000);
236
236
  });
237
237
  }
238
+ /**
239
+ * 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
240
+ * 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
241
+ * 即规范地址。与其它 GitHub 访问同口径 —— curl 子进程注入代理 env(Node 内置
242
+ * http(s) 读不到系统代理,直连在部分网络下不可达)。
243
+ * 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
244
+ */
245
+ export async function finalUrl(url, proxy, timeoutMs) {
246
+ const discard = process.platform === 'win32' ? 'NUL' : '/dev/null';
247
+ const r = await curlWithTlsFallback([
248
+ '-sS', '-I', '-L', '-o', discard, '-w', '%{url_effective}',
249
+ '--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))),
250
+ url,
251
+ ], proxyEnv(proxy), timeoutMs);
252
+ const final = r.stdout.trim();
253
+ return r.code === 0 && final !== '' ? final : null;
254
+ }
238
255
  /**
239
256
  * curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数兜底)。
240
257
  * 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
@@ -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';
@@ -6,9 +6,10 @@
6
6
  * 安装仍会重试反查,避免一次瞬时故障把整个会话锁死在 git 通道。
7
7
  * registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
8
8
  * 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
9
+ * proxy 参数:GitHub 改名跳转探测走的代理(与安装/诊断同一口径)。
9
10
  * 慢网络下失败不阻塞安装。
10
11
  */
11
- export declare function resolveNpmPackage(repo: string, registry?: string): Promise<string | null>;
12
+ export declare function resolveNpmPackage(repo: string, registry?: string, proxy?: string): Promise<string | null>;
12
13
  type NpmSearchPackage = {
13
14
  name?: unknown;
14
15
  keywords?: unknown;
@@ -22,6 +23,6 @@ type NpmSearchObject = {
22
23
  package?: NpmSearchPackage;
23
24
  };
24
25
  export declare function isDshPackageMetadataForRepo(pkg: NpmSearchPackage, repo: string): boolean;
25
- export declare function isDshNpmPackageForRepo(packageName: string, repo: string, registry?: string): Promise<boolean>;
26
+ export declare function isDshNpmPackageForRepo(packageName: string, repo: string, registry?: string, proxy?: string): Promise<boolean>;
26
27
  export declare function selectNpmPackageForRepo(objects: NpmSearchObject[], repo: string): string | null;
27
28
  export {};
@@ -59,6 +59,14 @@ export declare function probeUrl(url: string, proxy: string, timeoutMs: number):
59
59
  * 不打 17MB 的 dsh-plugin-hub 主页)。
60
60
  */
61
61
  export declare function gitLsRemote(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult>;
62
+ /**
63
+ * 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
64
+ * 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
65
+ * 即规范地址。与其它 GitHub 访问同口径 —— curl 子进程注入代理 env(Node 内置
66
+ * http(s) 读不到系统代理,直连在部分网络下不可达)。
67
+ * 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
68
+ */
69
+ export declare function finalUrl(url: string, proxy: string, timeoutMs: number): Promise<string | null>;
62
70
  /**
63
71
  * curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数兜底)。
64
72
  * 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
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) },
@@ -533,12 +536,14 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
533
536
  onConsumedOpenSection: () => setSettingsSection(null),
534
537
  }),
535
538
  // Hub 版本信息弹窗:版本号/徽标点击打开 —— 有更新显示新版本 + 变更记录 + 「直接更新」,
536
- // 无更新显示当前版本 + 记录 + 「已是最新」。Worker 拉取失败时兜底展示当前版本号。
539
+ // 暂缓提示显示同一标题但只给「稍后再说」,无更新显示当前版本 + 记录 + 「已是最新」。
540
+ // 版本号与本机版本一律由 hubUpdateState / hubCurrentVersion 决定,不用远端版号冒充本机。
537
541
  showHubUpdate && h(HubUpdateModal, {
538
542
  info: catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
543
+ state: catalog.hubUpdateState,
544
+ currentVersion: catalog.hubCurrentVersion,
539
545
  lang,
540
546
  t,
541
- hasUpdate: catalog.hubHasUpdate,
542
547
  onClose: () => setShowHubUpdate(false),
543
548
  onProceed: () => {
544
549
  setShowHubUpdate(false)
@@ -4,30 +4,39 @@
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
6
  * Hub 版本信息 / 自我更新说明弹窗:
7
- * - 有更新(hasUpdate=true):标题「有新版本」,展示新版本号、发布时间与 Worker 下发的
7
+ * - 有更新(state='available'):标题「有新版本」,展示新版本号、发布时间与 Worker 下发的
8
8
  * Markdown 变更记录(renderMarkdown 渲染,双语言按界面语言取 {zh,en} 对象),
9
9
  * 按钮「稍后再说 / 直接更新」,确认后进入安装弹窗执行覆盖重装。
10
- * - 无更新(hasUpdate=false):标题「当前版本」,展示当前版本信息与更新记录,已是最新。
10
+ * - 暂缓提示(state='pending'):远端确有新版本,但发布未满供应链安全门槛,pnpm 此时会
11
+ * 静默回退到旧版本 —— 仍按「有新版本」展示,只给「稍后再说」,并说明稍后会自动提示。
12
+ * - 无更新 / 信息缺失(state='latest' / 'none'):标题「当前版本」,版本号与本机版本取自
13
+ * currentVersion(构建注入的 PLUGIN_VERSION),**不得**用远端最新版号冒充本机版本。
11
14
  * 点击头部版本号(或「可更新」徽标)打开 —— 无论有无更新都能看到更新内容。
12
15
  */
13
16
  import { createElement as h } from 'react'
14
17
  import type { MouseEvent } from 'react'
15
18
  import styles from '../../styles/Modal.module.css'
16
- import type { HubUpdateInfo } from '../../types.ts'
19
+ import type { HubUpdateInfo, HubUpdateState } from '../../types.ts'
17
20
  import type { LocaleId, Translate } from '../../types.ts'
18
21
  import { renderMarkdown } from '../../logic/renderMarkdown.ts'
19
22
  import { CloseIcon } from '../ui/icons.tsx'
20
23
 
21
- export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }: {
24
+ export function HubUpdateModal({ info, state, currentVersion, lang, t, onProceed, onClose }: {
22
25
  info: HubUpdateInfo
23
26
  lang: LocaleId
24
27
  t: Translate
25
- /** 是否有可用更新:有则显示「直接更新」,无则显示「已是最新」 */
26
- hasUpdate: boolean
28
+ /** Hub 自我更新状态(见 types.ts):决定标题 / 正文 / meta 版本号 / 按钮 */
29
+ state: HubUpdateState
30
+ /** 本机实际运行的版本 —— 「当前版本」与「暂缓提示」的版本号一律用它 */
31
+ currentVersion: string
27
32
  /** 点「直接更新」:关闭本弹窗,进入安装弹窗的更新流程 */
28
33
  onProceed: () => void
29
34
  onClose: () => void
30
35
  }) {
36
+ const hasUpdate = state === 'available'
37
+ const pending = state === 'pending'
38
+ // 展示的版本号:提示更新/暂缓提示时是远端新版本;否则是本机当前版本
39
+ const shownVersion = hasUpdate || pending ? info.version : currentVersion || info.version
31
40
  // 变更记录:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则不出记录区
32
41
  const notesRaw = typeof info.notes === 'string'
33
42
  ? info.notes
@@ -54,7 +63,7 @@ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }:
54
63
  },
55
64
  h('div', { className: `${styles.modal} ${styles.hubUpdateModal}`, role: 'dialog', 'aria-modal': 'true' },
56
65
  h('div', { className: styles.modalHead },
57
- h('div', { className: styles.modalTitle }, t(hasUpdate ? 'hubUpdateTitle' : 'hubCurrentTitle')),
66
+ h('div', { className: styles.modalTitle }, t(hasUpdate || pending ? 'hubUpdateTitle' : 'hubCurrentTitle')),
58
67
  h('button', {
59
68
  className: styles.modalClose,
60
69
  'aria-label': t('confirmCancel'),
@@ -62,9 +71,9 @@ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }:
62
71
  }, h(CloseIcon)),
63
72
  ),
64
73
  h('div', { className: styles.modalDesc },
65
- t(hasUpdate ? 'hubUpdateDesc' : 'hubCurrentDesc', { version: info.version })),
74
+ t(hasUpdate ? 'hubUpdateDesc' : pending ? 'hubPendingDesc' : 'hubCurrentDesc', { version: shownVersion })),
66
75
  h('div', { className: styles.hubUpdateMeta },
67
- h('span', { className: styles.hubUpdateMetaItem }, `${t('version')} ${info.version}`),
76
+ h('span', { className: styles.hubUpdateMetaItem }, `${t('version')} ${shownVersion}`),
68
77
  published
69
78
  ? h('span', { className: styles.hubUpdateMetaItem }, `${t('hubUpdatePublished')} ${published}`)
70
79
  : null,
@@ -81,7 +90,7 @@ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }:
81
90
  h('button', { className: styles.restartLater, onClick: onClose }, t('hubUpdateLater')),
82
91
  h('button', { className: styles.modalInstall, onClick: onProceed }, t('updateNow')),
83
92
  ]
84
- : h('button', { className: styles.modalInstall, onClick: onClose }, t('hubUpToDate')),
93
+ : h('button', { className: styles.modalInstall, onClick: onClose }, t(pending ? 'hubUpdateLater' : 'hubUpToDate')),
85
94
  ),
86
95
  ),
87
96
  )
@@ -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
  ),