dsh-plugin 1.6.10 → 1.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
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
+ * 官方 registry 存在性复核:把「包在 registry 上不存在」这类失败的责任方分开。
7
+ *
8
+ * 背景:安装失败日志出现 `ERR_PNPM_FETCH_404` / `Not Found - 404` 时,光看日志分不清三种情形:
9
+ * ① 缺的包是 **profile 自己声明的依赖**(历史残留条目,指向一个从未发布的包)—— 用户环境问题:
10
+ * pnpm 每次解析依赖都会先卡在它上面,任何插件都装不进来(issue #73),提示删掉该条目即可;
11
+ * ② 缺的包来自**插件的依赖树**,且该包在官方 registry 上也不存在 —— 作者侧问题(漏声明依赖、
12
+ * 依赖被删除或从未发布),应引导用户投插件作者仓库;
13
+ * ③ 缺的包来自插件依赖树,但官方 registry 上有 —— 用户本机 registry 指向的镜像/私有源未同步
14
+ * 该包,属用户环境问题,提示换源即可。
15
+ * 故本模块先排除 ①(读 profile 的 package.json),再用**官方** registry(registry.npmjs.org,
16
+ * 绕开本机可能配置的镜像)复查存在性,产出 ② / ③ 的标记行。
17
+ *
18
+ * 与 npm-check.ts 同构:本模块只产出「标记行」,由 task-queue 追加到任务输出,前端按标记分类。
19
+ * fetch 与 profile 目录都可注入,便于单测覆盖全部结论而不依赖网络。
20
+ */
21
+ import { readFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ /** pnpm 输出里 scope 包是 URL 编码形态(`@scope%2Fname`):比较依赖名、拼查询 URL 前先还原成
24
+ * `@scope/name`。 */
25
+ function decodePackageName(raw) {
26
+ return raw.replace(/%40/gi, '@').replace(/%2f/gi, '/');
27
+ }
28
+ /** 从失败输出里提取「registry 上不存在的包名」(含 scope,如 `@scope/name`):读 404 行的请求路径。
29
+ * 提取不到返回 null。 */
30
+ export function missingPackageOf(output) {
31
+ for (const line of output.split(/\r?\n/)) {
32
+ if (!/ERR_PNPM_FETCH_404|404/.test(line))
33
+ continue;
34
+ const m = line.match(/GET\s+https?:\/\/[^\s/]+\/((?:@[^/\s:]+\/)?[^/\s:]+)/);
35
+ if (m)
36
+ return decodePackageName(m[1]);
37
+ }
38
+ return null;
39
+ }
40
+ /** 读 profile 自己声明的依赖名(profile 目录下 package.json 的 dependencies 键)。读不到返回空数组。 */
41
+ export function profileDependencyNames(profileDir) {
42
+ try {
43
+ const raw = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
44
+ return Object.keys(raw.dependencies ?? {});
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ }
50
+ /** 官方 registry 上是否存在该包:明确 404 → false(不存在),2xx → true,其余(限流/网络异常)→ null
51
+ * —— 判不了就不判,绝不妄下结论。 */
52
+ async function existsOnOfficialRegistry(name, fetchImpl, timeoutMs) {
53
+ // scope 包的路径要编码:`@scope/name` → `%40scope%2Fname`
54
+ const url = `https://registry.npmjs.org/${name.replace('@', '%40').replace('/', '%2F')}`;
55
+ try {
56
+ const res = await fetchImpl(url, {
57
+ headers: { accept: 'application/vnd.npm.install-v1+json' },
58
+ signal: AbortSignal.timeout(timeoutMs),
59
+ });
60
+ if (res.status === 404)
61
+ return false;
62
+ return res.ok ? true : null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * 产出「包不存在」失败的责任方标记行;判不了返回 null(前端维持既有提示)。
70
+ * - `[registry-missing]` 缺的包在官方源上也不存在 → 作者侧问题(前端引导投作者)
71
+ * - `[registry-mirror-lag]` 官方源上有 → 本机镜像源未同步(前端提示换源,不投任何一方)
72
+ */
73
+ export async function registryMissingMarker(output, opts) {
74
+ const name = missingPackageOf(output);
75
+ if (name === null)
76
+ return null;
77
+ // ① profile 自己声明的残留依赖(#73):用户环境问题,交给既有的 pnpmMissingDep 提示(该提示就是
78
+ // 教用户删 profile 里那条依赖),这里不再产出标记、不改归属
79
+ if (profileDependencyNames(opts.profileDir).some((dep) => dep.toLowerCase() === name.toLowerCase()))
80
+ return null;
81
+ const exists = await existsOnOfficialRegistry(name, opts.fetchImpl ?? fetch, opts.timeoutMs ?? 8000);
82
+ if (exists === false) {
83
+ return `[registry-missing] ${name} is not published on the official npm registry (registry.npmjs.org) — a dependency of this plugin is missing or was removed; author-side issue`;
84
+ }
85
+ if (exists === true) {
86
+ return `[registry-mirror-lag] ${name} exists on the official npm registry — the configured registry or mirror has not synced it; local environment issue`;
87
+ }
88
+ return null;
89
+ }
@@ -13,6 +13,7 @@ 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';
15
15
  import { npmTooLowMarker } from './npm-check.js';
16
+ import { registryMissingMarker } from './registry-check.js';
16
17
  import { backfillOperationLog } from './operation-log.js';
17
18
  import { cleanLine, estimateProgress } from '../profile/progress.js';
18
19
  import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
@@ -681,6 +682,22 @@ function annotateNpmTooLow(result, options) {
681
682
  pushLine(options.task, line);
682
683
  return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
683
684
  }
685
+ /** 包在 registry 不存在(404)→ 用官方源复查一次,把「作者依赖缺失/包被删」与「本机镜像源滞后」
686
+ * 分开(两者的提示与去向不同:前者引导投作者、后者只提示换源)。检测逻辑在独立的
687
+ * registry-check.ts;这里之所以是异步,是因为要联网查官方 registry —— 只在安装失败且日志含 404
688
+ * 时发生,代价可忽略。 */
689
+ async function annotateRegistryMissing(result, options) {
690
+ if (options.action === 'remove' || result.exitCode === 0 || result.timedOut)
691
+ return result;
692
+ const line = await registryMissingMarker(`${result.stderr}\n${result.stdout}`, {
693
+ profileDir: profileDirectory(options.profile),
694
+ });
695
+ if (line === null)
696
+ return result;
697
+ if (options.task && options.task.status !== 'cancelled')
698
+ pushLine(options.task, line);
699
+ return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
700
+ }
684
701
  /**
685
702
  * Run a plugin mutation with one recovery path: when `add` fails because
686
703
  * pnpm's allowBuilds gate blocks a build script — either the git-hosted
@@ -765,6 +782,8 @@ export async function runPluginMutation(options) {
765
782
  result = { ...result, stderr: `${result.stderr}\n${backfilled}` };
766
783
  }
767
784
  }
785
+ // 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
786
+ result = await annotateRegistryMissing(result, options);
768
787
  // npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
769
788
  return annotateNpmTooLow(result, options);
770
789
  }
@@ -0,0 +1,15 @@
1
+ /** 从失败输出里提取「registry 上不存在的包名」(含 scope,如 `@scope/name`):读 404 行的请求路径。
2
+ * 提取不到返回 null。 */
3
+ export declare function missingPackageOf(output: string): string | null;
4
+ /** 读 profile 自己声明的依赖名(profile 目录下 package.json 的 dependencies 键)。读不到返回空数组。 */
5
+ export declare function profileDependencyNames(profileDir: string): string[];
6
+ /**
7
+ * 产出「包不存在」失败的责任方标记行;判不了返回 null(前端维持既有提示)。
8
+ * - `[registry-missing]` 缺的包在官方源上也不存在 → 作者侧问题(前端引导投作者)
9
+ * - `[registry-mirror-lag]` 官方源上有 → 本机镜像源未同步(前端提示换源,不投任何一方)
10
+ */
11
+ export declare function registryMissingMarker(output: string, opts: {
12
+ profileDir: string;
13
+ fetchImpl?: typeof fetch;
14
+ timeoutMs?: number;
15
+ }): Promise<string | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.6.10",
3
+ "version": "1.6.11",
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",
@@ -18,7 +18,7 @@ import type { MouseEvent } from 'react'
18
18
  import styles from '../../styles/Modal.module.css'
19
19
  import type { EnvInfo, Translate } from '../../types.ts'
20
20
  import type { NotificationRecord } from '../../logic/failures.ts'
21
- import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
21
+ import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
22
22
  import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
23
23
  import { pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
24
24
  import { CloseIcon } from '../ui/icons.tsx'
@@ -311,7 +311,24 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
311
311
  const pkg = missingRegistryPackageOf(r.message)
312
312
  return h('div', null, [
313
313
  pkg ? h('div', { className: styles.failNetworkTarget }, t('failPnpmMissingDepTarget', { pkg })) : null,
314
- h('div', { className: styles.failPrepareHint }, t('failPnpmMissingDepHint')),
314
+ h('div', { className: styles.failPrepareHint }, t(registryMissingHintOf(r.message))),
315
+ ])
316
+ }
317
+ if (kind === 'pluginMissingDep') {
318
+ // 服务端用**官方** registry 复核过:插件依赖树里的包在官方源上也不存在,且不是用户
319
+ // profile 的残留依赖 → 作者侧问题(依赖没发布/已被删除),引导到插件作者仓库提 Issue
320
+ //(判据是源上的客观存在性,不是我们的启发式判断,故不设「市场误判」次要入口)
321
+ const pkg = missingRegistryPackageOf(r.message)
322
+ return h('div', null, [
323
+ pkg ? h('div', { className: styles.failNetworkTarget }, t('failPluginMissingDepTarget', { pkg })) : null,
324
+ h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
325
+ r.repo && shownRepo ? h('a', {
326
+ className: styles.failBigIssue,
327
+ href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
328
+ target: '_blank',
329
+ rel: 'noopener noreferrer',
330
+ title: t('failIssueHint'),
331
+ }, t('failIssueBig')) : null,
315
332
  ])
316
333
  }
317
334
  if (kind === 'registryVersionStale') {
@@ -17,7 +17,7 @@ import { CloseIcon, ConfirmIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
17
17
  import { ProgressView } from './ProgressView.tsx'
18
18
  import { installCommandOf } from '../../logic/install-command.ts'
19
19
  import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
20
- import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
20
+ import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
21
21
 
22
22
  /** 完成结果视图:绿色对勾 + 标题/描述;
23
23
  * 终态只有两种:
@@ -444,7 +444,25 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
444
444
  const pkg = missingRegistryPackageOf(message)
445
445
  return pkg ? h('div', { className: styles.failNetworkTarget }, t('failPnpmMissingDepTarget', { pkg })) : null
446
446
  })(),
447
- h('div', { className: styles.failPrepareHint }, t('failPnpmMissingDepHint')),
447
+ h('div', { className: styles.failPrepareHint }, t(registryMissingHintOf(message))),
448
+ ])
449
+ : failureKind === 'pluginMissingDep'
450
+ // 服务端用**官方** registry 复核过:插件依赖树里的某个包在官方源上也不存在,且不是用户
451
+ // profile 的残留依赖 → 作者侧问题(依赖没发布/已被删除)。判据是源上的客观存在性,
452
+ // 不是我们的启发式判断,故不给「市场误判」次要入口,直接引导到插件作者仓库提 Issue。
453
+ ? h('div', null, [
454
+ (() => {
455
+ const pkg = missingRegistryPackageOf(message)
456
+ return pkg ? h('div', { className: styles.failNetworkTarget }, t('failPluginMissingDepTarget', { pkg })) : null
457
+ })(),
458
+ h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
459
+ repo ? h('a', {
460
+ className: styles.failBigIssue,
461
+ href: pluginIssueUrl(repo, message, env, command, attempts),
462
+ target: '_blank',
463
+ rel: 'noopener noreferrer',
464
+ title: t('failIssueHint'),
465
+ }, t('failIssueBig')) : null,
448
466
  ])
449
467
  : failureKind === 'registryVersionStale'
450
468
  // 要装的版本在「实际执行安装的那个源」里查不到(ERR_PNPM_NO_MATCHING_VERSION / ETARGET):
@@ -357,6 +357,9 @@ export const zh = {
357
357
  // 404 依赖:点名 registry 上不存在的那个包(取不到包名时跳过这行),再给删除条目的指引
358
358
  failPnpmMissingDepTarget: '{pkg} 在 npm registry 上不存在(404)',
359
359
  failPnpmMissingDepHint: '安装器调用 pnpm 时报「包不存在」(ERR_PNPM_FETCH_404):你的 profile 里留着一条指向 registry 上未发布包的依赖,pnpm 每次解析依赖都会先卡在它上面,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 package.json 里删除该依赖条目(同时删掉对应的 pnpm-lock.yaml 条目或整个锁文件)后重试。',
360
+ failRegistryMirrorLagHint: '安装器调用 pnpm 时报「包不存在」(ERR_PNPM_FETCH_404):该包在官方 npm 源上是有的,但你本机配置的 npm/pnpm 源(registry 或镜像)没有同步到它。请把 npm/pnpm 的 registry 指回官方源 https://registry.npmjs.org(或换一个已同步该包的镜像)后重试。这不是插件本身的问题。',
361
+ failPluginMissingDepTarget: '{pkg} 在官方 npm 源上不存在',
362
+ failPluginMissingDepHint: '该插件依赖的包在官方 npm 源上并不存在(已删除或从未发布):这是插件作者侧的依赖问题,不是你环境的问题。请向插件作者仓库反馈,请作者补上或修正该依赖,或改为随插件一起分发。',
360
363
  failRegistryVersionStaleHint: '安装器调用 pnpm 时报「找不到匹配的版本」(ERR_PNPM_NO_MATCHING_VERSION):要装的这个版本在实际执行安装的 npm 源里查不到。常见原因是你设置的 npmRegistry 与本机 ~/.npmrc 用的不是同一个源——官方源刚发布、镜像还没同步上来(快则几分钟、慢则几小时),也可能只是版本号写错了。这不是插件本身的问题。可在插件市场设置里把 npmRegistry 改成与本机 ~/.npmrc 一致(或反过来),或那一次显式走官方源(https://registry.npmjs.org)、改用离线包后重试;若该版本刚发布不久,等镜像同步后重试即可。',
361
364
  failPnpmLinkedDirMissingHint: '安装器调用 pnpm 时报「要安装的目录不存在」(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND):你的 profile 里留着一条指向本机不存在目录的本地依赖(link: / file: 形式,例如用 dsh plugin add <本地路径> 装过开发中的插件,之后那个目录被删除或移动了)。pnpm 每次解析 profile 依赖都会先卡在它上面,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 package.json 里删除该本地依赖条目(同时删掉对应的 pnpm-lock.yaml 条目或整个锁文件),或把该目录恢复回原路径后重试。',
362
365
  failFileLockedHint: '安装过程中 pnpm 无法替换 profile 里的文件:有程序正占用着它(Windows 常见报错 os error 32「另一个程序正在使用此文件」)。通常是 DeepSeek Harness 自身或杀毒软件的实时扫描持有文件句柄。这不是插件本身的问题——请完全退出 DeepSeek Harness(Windows 可在任务管理器确认进程已结束,必要时临时关闭杀毒软件实时防护)后重试。',
@@ -770,6 +773,9 @@ export const en = {
770
773
  // then give the removal instructions
771
774
  failPnpmMissingDepTarget: '{pkg} was not found on the npm registry (404)',
772
775
  failPnpmMissingDepHint: 'pnpm reported that a package does not exist on the npm registry (ERR_PNPM_FETCH_404): your profile keeps a dependency on a package that was never published, and pnpm hits it every time it resolves dependencies — no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that dependency from package.json inside the profile directory (~/.dsh/profiles/<profile>) — along with its entry in pnpm-lock.yaml, or the lockfile itself — then retry.',
776
+ failRegistryMirrorLagHint: 'pnpm reported that a package does not exist on the npm registry (ERR_PNPM_FETCH_404): that package does exist on the official npm registry, but the npm/pnpm registry (or mirror) configured on your machine has not synced it. Point the npm/pnpm registry back at the official one (https://registry.npmjs.org), or switch to a mirror that has the package, then retry. This is not a problem with the plugin itself.',
777
+ failPluginMissingDepTarget: '{pkg} does not exist on the official npm registry',
778
+ failPluginMissingDepHint: 'A package this plugin depends on does not exist on the official npm registry (removed, or never published). This is a dependency problem on the plugin author side, not a problem with your environment. Please report it to the plugin repository so the author can fix that dependency or ship it together with the plugin.',
773
779
  failRegistryVersionStaleHint: 'pnpm reported that no matching version was found (ERR_PNPM_NO_MATCHING_VERSION): the version being installed does not exist in the npm registry that actually performs the install. This usually means the npmRegistry configured in the hub differs from the source in your local ~/.npmrc — the official registry has published it but the mirror has not synced yet (it can take minutes to hours) — or the version number was simply mistyped. This is not a problem with the plugin itself. Align the hub\'s npmRegistry with your local ~/.npmrc (or the other way round), or explicitly use the official registry (https://registry.npmjs.org) for that one install, or use an offline package, then retry; if the version was published only recently, waiting for the mirror to sync is enough.',
774
780
  failPnpmLinkedDirMissingHint: 'pnpm reported that a directory it needs to install from does not exist (ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND): your profile keeps a local dependency (a link: / file: entry, e.g. from `dsh plugin add <local path>`) whose target directory has since been deleted or moved. pnpm hits it every time it resolves profile dependencies, so no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that local dependency from package.json inside the profile directory (~/.dsh/profiles/<profile>) — along with its entry in pnpm-lock.yaml, or the lockfile itself — or restore the directory to its original path, then retry.',
775
781
  failFileLockedHint: 'During install, pnpm could not replace a file inside the profile because another program is holding it (on Windows this shows as os error 32, "the process cannot access the file because it is being used by another process"). Usually DeepSeek Harness itself or a real-time antivirus scan holds the handle. This is not a problem with the plugin itself — fully quit DeepSeek Harness (on Windows, confirm in Task Manager that the process has ended, and temporarily disable real-time antivirus protection if needed), then retry.',
@@ -151,7 +151,7 @@ export function removeNotification(id: number): NotificationRecord[] {
151
151
  return next
152
152
  }
153
153
 
154
- export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
154
+ export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
155
155
 
156
156
  /** Match the pnpm error code, not a generic mention of checksum or integrity. */
157
157
  function missingTarballIntegrity(message: string): boolean {
@@ -230,6 +230,11 @@ export function classifyFailure(message: string): FailureKind {
230
230
  // 装后校验发现入口文件缺失([packaging])= git 分发缺构建产物
231
231
  if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
232
232
  // registry 上不存在的依赖(404),profile 任何安装都会卡在这条。必须在 pluginPrepare 之前判
233
+ // 官方源复核结论:缺的包在官方 registry 上也不存在,且不是本机 profile 声明的残留依赖 →
234
+ // 该包来自插件的依赖树,属**作者侧**问题(依赖没发布/已被删除)。标记由服务端联网复查后注入。
235
+ // 必须排在 pnpmMissingDep 之前 —— 日志里同样含 ERR_PNPM_FETCH_404,靠标记区分「作者的锅」
236
+ // 与「本机 profile 残留依赖(#73)/ 镜像源滞后」。
237
+ if (/\[registry-missing\]/i.test(message)) return 'pluginMissingDep'
233
238
  if (/ERR_PNPM_FETCH_404|404 Not Found|Not Found - 404/i.test(message)) return 'pnpmMissingDep'
234
239
  // 要装的版本在实际安装源里没有(镜像滞后或版本写错)。必须在 pluginPrepare 之前判
235
240
  if (/\bERR_PNPM_NO_MATCHING_VERSION\b|\bETARGET\b|No matching version found/i.test(message)) return 'registryVersionStale'
@@ -252,6 +257,12 @@ export function classifyFailure(message: string): FailureKind {
252
257
  // dsh 进程或安装任务在写,锁一直没释放。既不是插件问题、也不是用户环境坏了 —— 把并发收掉重试即可。
253
258
  // 摆放位置同样在一切错误码分支之后:日志里真有 pnpm 报错时仍以真实报错为准。
254
259
  if (/atomic-write: timed out waiting for the writer lock|timed out waiting for the writer lock/i.test(message)) return 'profileLock'
260
+ // 宿主 DSH 自己的 allowBuilds 摘要行(#210):新版宿主把 pnpm 报错只写进 diagnostics 日志文件,
261
+ // 终端里只留这一句「git-hosted plugins build … add the exact key … then re-run」,日志里因此
262
+ // 一条原始 ERR_PNPM_* 码都没有 —— 只能靠这句话识别为「构建脚本被白名单拦截」。
263
+ // 摆放位置在一切错误码分支之后:日志里真有 ERR_PNPM_PREPARE_PACKAGE / ERR_PNPM_IGNORED_BUILDS
264
+ // 时(构建确实跑挂、或不带摘要的旧宿主),仍以原始报错为准,不会被这句摘要改写。
265
+ if (/git-hosted plugins build on install via their prepare script|add the exact key pnpm printed above under allowBuilds/i.test(message)) return 'pnpmIgnoredBuild'
255
266
  // 其余未知失败:引导到插件中心仓库排查
256
267
  return 'hub'
257
268
  }
@@ -318,13 +329,21 @@ export function coreErrorCode(message: string): string | null {
318
329
  return m ? m[0].replace(/^\[|\]$/g, '') : null
319
330
  }
320
331
 
332
+ /** 「包在 registry 不存在」失败的提示变体:服务端用官方源复核过(会注入 `[registry-mirror-lag]`)
333
+ * 时,说明包在官方源上存在、是本机镜像源未同步 → 提示换源;否则沿用既有的「profile 残留依赖」
334
+ * 提示(#73:指向从未发布包的残留条目,任何插件都装不进来)。 */
335
+ export function registryMissingHintOf(message: string): 'failPnpmMissingDepHint' | 'failRegistryMirrorLagHint' {
336
+ return /\[registry-mirror-lag\]/i.test(message) ? 'failRegistryMirrorLagHint' : 'failPnpmMissingDepHint'
337
+ }
338
+
321
339
  /** 提取「registry 上不存在的包名」:从 404 行(`ERR_PNPM_FETCH_404 GET <registry>/<pkg>: Not Found - 404`)
322
340
  * 里取请求路径中的包名(含 scope,如 `@scope/name`);非 404 或提取不到返回 null。 */
323
341
  export function missingRegistryPackageOf(message: string): string | null {
324
342
  for (const line of message.split(/\r?\n/)) {
325
343
  if (!/ERR_PNPM_FETCH_404|404/.test(line)) continue
326
344
  const m = line.match(/GET\s+https?:\/\/[^\s/]+\/((?:@[^/\s:]+\/)?[^/\s:]+)/)
327
- if (m) return m[1]
345
+ // pnpm 输出里 scope 包是 URL 编码形态(`@scope%2Fname`):还原成 `@scope/name` 再展示
346
+ if (m) return m[1].replace(/%40/gi, '@').replace(/%2f/gi, '/')
328
347
  }
329
348
  return null
330
349
  }
@@ -66,6 +66,7 @@ function reasonTitleOf(kind: FailureKind): string {
66
66
  case 'hubState': return 'stale install state in the plugin hub'
67
67
  case 'timeout': return 'install timed out'
68
68
  case 'profileLock': return 'DSH profile is locked by another process'
69
+ case 'pluginMissingDep': return 'a dependency of the plugin is not published on npm'
69
70
  case 'hub': return 'install failed (cause unclassified)'
70
71
  default: return 'plugin install failed'
71
72
  }
@@ -90,6 +91,9 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
90
91
  const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
91
92
  const reason = /\[packaging\]/i.test(message)
92
93
  ? 'plugin distribution is incomplete — a file the package needs (its entry from main / exports, an entry a bundled patch mounts, or the patch declared by dsh.bundle.patch) is missing from the published package (github tarball or npm package); please commit build output or publish a complete package'
94
+ : /\[registry-missing\]/i.test(message)
95
+ // 服务端用官方 registry 复核过:缺的包在官方源上也不存在,且不是用户 profile 的残留依赖
96
+ ? 'a dependency declared by the plugin is not published on the public npm registry (missing, removed, or never released); please publish or correct that dependency, or ship it with the plugin'
93
97
  : kind === 'network'
94
98
  ? 'network connectivity issue on the user side (DNS, proxy, firewall, or a blocked connection)'
95
99
  : kind === 'pluginPrepare'
@@ -0,0 +1,94 @@
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
+ * 官方 registry 存在性复核:把「包在 registry 上不存在」这类失败的责任方分开。
7
+ *
8
+ * 背景:安装失败日志出现 `ERR_PNPM_FETCH_404` / `Not Found - 404` 时,光看日志分不清三种情形:
9
+ * ① 缺的包是 **profile 自己声明的依赖**(历史残留条目,指向一个从未发布的包)—— 用户环境问题:
10
+ * pnpm 每次解析依赖都会先卡在它上面,任何插件都装不进来(issue #73),提示删掉该条目即可;
11
+ * ② 缺的包来自**插件的依赖树**,且该包在官方 registry 上也不存在 —— 作者侧问题(漏声明依赖、
12
+ * 依赖被删除或从未发布),应引导用户投插件作者仓库;
13
+ * ③ 缺的包来自插件依赖树,但官方 registry 上有 —— 用户本机 registry 指向的镜像/私有源未同步
14
+ * 该包,属用户环境问题,提示换源即可。
15
+ * 故本模块先排除 ①(读 profile 的 package.json),再用**官方** registry(registry.npmjs.org,
16
+ * 绕开本机可能配置的镜像)复查存在性,产出 ② / ③ 的标记行。
17
+ *
18
+ * 与 npm-check.ts 同构:本模块只产出「标记行」,由 task-queue 追加到任务输出,前端按标记分类。
19
+ * fetch 与 profile 目录都可注入,便于单测覆盖全部结论而不依赖网络。
20
+ */
21
+ import { readFileSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+
24
+ /** pnpm 输出里 scope 包是 URL 编码形态(`@scope%2Fname`):比较依赖名、拼查询 URL 前先还原成
25
+ * `@scope/name`。 */
26
+ function decodePackageName(raw: string): string {
27
+ return raw.replace(/%40/gi, '@').replace(/%2f/gi, '/')
28
+ }
29
+
30
+ /** 从失败输出里提取「registry 上不存在的包名」(含 scope,如 `@scope/name`):读 404 行的请求路径。
31
+ * 提取不到返回 null。 */
32
+ export function missingPackageOf(output: string): string | null {
33
+ for (const line of output.split(/\r?\n/)) {
34
+ if (!/ERR_PNPM_FETCH_404|404/.test(line)) continue
35
+ const m = line.match(/GET\s+https?:\/\/[^\s/]+\/((?:@[^/\s:]+\/)?[^/\s:]+)/)
36
+ if (m) return decodePackageName(m[1])
37
+ }
38
+ return null
39
+ }
40
+
41
+ /** 读 profile 自己声明的依赖名(profile 目录下 package.json 的 dependencies 键)。读不到返回空数组。 */
42
+ export function profileDependencyNames(profileDir: string): string[] {
43
+ try {
44
+ const raw = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as { dependencies?: Record<string, unknown> }
45
+ return Object.keys(raw.dependencies ?? {})
46
+ } catch {
47
+ return []
48
+ }
49
+ }
50
+
51
+ /** 官方 registry 上是否存在该包:明确 404 → false(不存在),2xx → true,其余(限流/网络异常)→ null
52
+ * —— 判不了就不判,绝不妄下结论。 */
53
+ async function existsOnOfficialRegistry(
54
+ name: string,
55
+ fetchImpl: typeof fetch,
56
+ timeoutMs: number,
57
+ ): Promise<boolean | null> {
58
+ // scope 包的路径要编码:`@scope/name` → `%40scope%2Fname`
59
+ const url = `https://registry.npmjs.org/${name.replace('@', '%40').replace('/', '%2F')}`
60
+ try {
61
+ const res = await fetchImpl(url, {
62
+ headers: { accept: 'application/vnd.npm.install-v1+json' },
63
+ signal: AbortSignal.timeout(timeoutMs),
64
+ })
65
+ if (res.status === 404) return false
66
+ return res.ok ? true : null
67
+ } catch {
68
+ return null
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 产出「包不存在」失败的责任方标记行;判不了返回 null(前端维持既有提示)。
74
+ * - `[registry-missing]` 缺的包在官方源上也不存在 → 作者侧问题(前端引导投作者)
75
+ * - `[registry-mirror-lag]` 官方源上有 → 本机镜像源未同步(前端提示换源,不投任何一方)
76
+ */
77
+ export async function registryMissingMarker(
78
+ output: string,
79
+ opts: { profileDir: string; fetchImpl?: typeof fetch; timeoutMs?: number },
80
+ ): Promise<string | null> {
81
+ const name = missingPackageOf(output)
82
+ if (name === null) return null
83
+ // ① profile 自己声明的残留依赖(#73):用户环境问题,交给既有的 pnpmMissingDep 提示(该提示就是
84
+ // 教用户删 profile 里那条依赖),这里不再产出标记、不改归属
85
+ if (profileDependencyNames(opts.profileDir).some((dep) => dep.toLowerCase() === name.toLowerCase())) return null
86
+ const exists = await existsOnOfficialRegistry(name, opts.fetchImpl ?? fetch, opts.timeoutMs ?? 8000)
87
+ if (exists === false) {
88
+ return `[registry-missing] ${name} is not published on the official npm registry (registry.npmjs.org) — a dependency of this plugin is missing or was removed; author-side issue`
89
+ }
90
+ if (exists === true) {
91
+ return `[registry-mirror-lag] ${name} exists on the official npm registry — the configured registry or mirror has not synced it; local environment issue`
92
+ }
93
+ return null
94
+ }
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from 'node:path'
14
14
  import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
15
15
  import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.ts'
16
16
  import { npmTooLowMarker } from './npm-check.ts'
17
+ import { registryMissingMarker } from './registry-check.ts'
17
18
  import { backfillOperationLog } from './operation-log.ts'
18
19
  import { cleanLine, estimateProgress } from '../profile/progress.ts'
19
20
  import type { LoaderHandle } from '../loader.ts'
@@ -711,6 +712,24 @@ function annotateNpmTooLow(result: InstallResult, options: {
711
712
  return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
712
713
  }
713
714
 
715
+ /** 包在 registry 不存在(404)→ 用官方源复查一次,把「作者依赖缺失/包被删」与「本机镜像源滞后」
716
+ * 分开(两者的提示与去向不同:前者引导投作者、后者只提示换源)。检测逻辑在独立的
717
+ * registry-check.ts;这里之所以是异步,是因为要联网查官方 registry —— 只在安装失败且日志含 404
718
+ * 时发生,代价可忽略。 */
719
+ async function annotateRegistryMissing(result: InstallResult, options: {
720
+ action: 'add' | 'remove' | 'update'
721
+ profile: string
722
+ task?: InstallTask
723
+ }): Promise<InstallResult> {
724
+ if (options.action === 'remove' || result.exitCode === 0 || result.timedOut) return result
725
+ const line = await registryMissingMarker(`${result.stderr}\n${result.stdout}`, {
726
+ profileDir: profileDirectory(options.profile),
727
+ })
728
+ if (line === null) return result
729
+ if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
730
+ return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
731
+ }
732
+
714
733
  /**
715
734
  * Run a plugin mutation with one recovery path: when `add` fails because
716
735
  * pnpm's allowBuilds gate blocks a build script — either the git-hosted
@@ -804,6 +823,8 @@ export async function runPluginMutation(options: {
804
823
  result = { ...result, stderr: `${result.stderr}\n${backfilled}` }
805
824
  }
806
825
  }
826
+ // 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
827
+ result = await annotateRegistryMissing(result, options)
807
828
  // npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
808
829
  return annotateNpmTooLow(result, options)
809
830
  }