dsh-plugin 1.5.2 → 1.6.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
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, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
21
+ import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
22
22
  import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
23
23
  import { pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
24
24
  import { CloseIcon } from '../ui/icons.tsx'
@@ -300,7 +300,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
300
300
  if (kind === 'pnpmPolicy') {
301
301
  // pnpm 11 供应链安全策略拦截(minimumReleaseAge 拒收发布太新的包 / untrusted origin):
302
302
  // 本机 pnpm 策略不放行,装任何「刚发布/新来源」插件都撞墙 —— 给豁免指引,不引导提 Issue
303
- return h('div', { className: styles.failPrepareHint }, t(pnpmPolicyHintOf(r.message)))
303
+ return h('div', { className: styles.failPrepareHint }, t(pnpmPolicyHintOf(r.message), pnpmPolicyHintParams(r.message)))
304
304
  }
305
305
  if (kind === 'pnpmUnusedPatch') {
306
306
  // profile 里留着指向旧版本 dsh-plugin 的补丁声明(ERR_PNPM_UNUSED_PATCH):pnpm
@@ -335,7 +335,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
335
335
  if (kind === 'originRejected') {
336
336
  // 请求来源没通过本地 hub 服务的校验(服务端 403 untrusted origin):请求根本没进
337
337
  // 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
338
- return h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint'))
338
+ return h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
339
339
  }
340
340
  if (kind === 'network') {
341
341
  // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
@@ -16,7 +16,7 @@ import { CloseIcon, ConfirmIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
16
16
  import { ProgressView } from './ProgressView.tsx'
17
17
  import { installCommandOf } from '../../logic/install-command.ts'
18
18
  import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
19
- import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
19
+ import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
20
20
 
21
21
  /** 完成结果视图:绿色对勾 + 标题/描述;
22
22
  * needsRestart=true(插件需重启才生效)→ 「稍后重启 / 立即重启」按钮对,点稍后重启后
@@ -416,7 +416,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
416
416
  : failureKind === 'pnpmPolicy'
417
417
  // pnpm 11 供应链安全策略拦截(minimumReleaseAge 拒收发布太新的包 / untrusted origin):
418
418
  // 本机 pnpm 策略不放行,装任何「刚发布/新来源」插件都撞墙 —— 给豁免指引,不引导提 Issue
419
- ? h('div', { className: styles.failPrepareHint }, t(pnpmPolicyHintOf(message)))
419
+ ? h('div', { className: styles.failPrepareHint }, t(pnpmPolicyHintOf(message), pnpmPolicyHintParams(message)))
420
420
  : failureKind === 'pnpmUnusedPatch'
421
421
  // profile 里留着指向旧版本 dsh-plugin 的补丁声明(ERR_PNPM_UNUSED_PATCH):pnpm 发现
422
422
  // 补丁没被用上就中止整次安装,任何插件都装不进来 —— 给删除条目的指引,不引导提 Issue
@@ -447,7 +447,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
447
447
  : failureKind === 'originRejected'
448
448
  // 请求来源没通过本地 hub 服务的校验(服务端 403 untrusted origin):请求根本没进
449
449
  // 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
450
- ? h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint'))
450
+ ? h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
451
451
  : failureKind === 'network'
452
452
  // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
453
453
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
@@ -176,7 +176,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
176
176
  const raw = cmdQuery.trim()
177
177
  if (!raw) return
178
178
  const parsed = parseDshCommand(raw)
179
- if (parsed === null) { setCmdError(t('dshCmdInvalid')); return }
179
+ if (parsed === null) { setCmdError(t('dshCmdInvalid', { profile })); return }
180
180
  // add / update 统一走安装流程:目标已安装时弹窗自动转「更新」覆盖重装(服务端 mode=update 放行),
181
181
  // 未安装时即为普通安装 —— 粘贴 `dsh plugin --profile web update dsh-plugin` 这类命令即可更新到最新
182
182
  onInstallCustom(parsed.target, { installChannel: 'dsh' })
@@ -303,7 +303,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
303
303
  !enableDsh
304
304
  ? channelOffRow
305
305
  : [
306
- h('div', { className: styles.installExample }, t('dshCmdExample')),
306
+ h('div', { className: styles.installExample }, t('dshCmdExample', { profile })),
307
307
  h('div', { className: styles.installRow },
308
308
  h('input', {
309
309
  className: cmdError ? `${styles.installInput} ${styles.installInputError}` : styles.installInput,
@@ -346,7 +346,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
346
346
  h('div', { className: modal.modalBody },
347
347
  // 格式清单:一行一条;`命令\t说明` 的说明跟在命令后,纯说明行以 \t 开头
348
348
  h('div', { className: styles.helpBody },
349
- t(HELP_CARDS[helpFor].contentKey).split('\n').map((line, i) => {
349
+ t(HELP_CARDS[helpFor].contentKey, { profile }).split('\n').map((line, i) => {
350
350
  const [cmd, note] = line.split('\t')
351
351
  return h('div', { key: i, className: styles.helpLine },
352
352
  cmd !== '' ? h('span', { className: styles.helpCmd }, cmd) : null,
@@ -59,13 +59,15 @@ export const zh = {
59
59
  dshCmdLabel: 'DeepSeek Harness 命令',
60
60
  dshCmdPlaceholder: '粘贴官方 dsh plugin 命令',
61
61
  dshCmdInsertHubUpdate: '插入更新命令',
62
- dshCmdExample: '例如:dsh plugin --profile web add github:owner/repo,或 dsh plugin --profile web update dsh-plugin',
63
- dshCmdInvalid: '请输入官方 dsh plugin 命令(--profile 必填,支持 add / update)。例如:dsh plugin --profile web update dsh-plugin',
62
+ // 示例/校验文案里的 profile 用运行时当前宿主 profile 渲染({profile} 占位符),
63
+ // 不写死 web —— 桌面端当前 profile 是 desktop,照抄写死的 web 会装进宿主永不加载的档
64
+ dshCmdExample: '例如:dsh plugin --profile {profile} add github:owner/repo,或 dsh plugin --profile {profile} update dsh-plugin',
65
+ dshCmdInvalid: '请输入官方 dsh plugin 命令(--profile 必填,支持 add / update)。例如:dsh plugin --profile {profile} update dsh-plugin',
64
66
  installHelp: '帮助',
65
67
  helpModalTitle: '支持的命令格式',
66
68
  npmInstallHelp: 'lodash、@scope/pkg\t直接输入包名\nnpm install lodash\nnpm i lodash\npnpm add @scope/pkg\npnpm i @scope/pkg\nnpm install -g @scope/cli\t全局安装 CLI 工具,不进「已安装」列表',
67
69
  gitInstallHelp: 'owner/repo\nhttps://github.com/owner/repo\ngithub:owner/repo\ngit+https://github.com/owner/repo.git\ngit@github.com:owner/repo.git',
68
- dshCmdHelp: 'dsh plugin --profile web add <目标>\t安装\ndsh plugin --profile web update <目标>\t更新已安装目标到最新版本\ndsh plugin --profile=web add <目标>\ndsh plugin --profile=web update <目标>\n\t--profile 必填,官方 CLI 无 -p 简写;<目标> 可为 npm 包名或 github:owner/repo;卸载请用「已安装」列表',
70
+ dshCmdHelp: 'dsh plugin --profile {profile} add <目标>\t安装\ndsh plugin --profile {profile} update <目标>\t更新已安装目标到最新版本\ndsh plugin --profile={profile} add <目标>\ndsh plugin --profile={profile} update <目标>\n\t--profile 必填(此处已填当前 profile),官方 CLI 无 -p 简写;<目标> 可为 npm 包名或 github:owner/repo;卸载请用「已安装」列表',
69
71
  globalNpmHint: '将全局安装 CLI 工具(npm install -g),不会出现在「已安装」列表,无需重启宿主',
70
72
  installCliBtn: '安装',
71
73
  // 安装通道关闭(安全信任开关关掉后,对应卡片禁用并引导去设置打开)
@@ -319,7 +321,7 @@ export const zh = {
319
321
  failNetworkRunDiag: '去系统诊断检测网络',
320
322
  // 请求来源没通过本地 hub 服务校验(服务端 403 untrusted origin):请求根本没进安装流程,
321
323
  // 与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
322
- failOriginRejectedHint: '这次安装请求没有通过插件市场的来源校验,被本地服务直接拒绝,所以安装根本没有开始 —— 与插件本身无关。通常是因为市场页面不是从本机地址打开的(比如用了局域网 IP),或宿主页面的来源未被识别。请从本机的插件市场地址(localhost)重新打开页面后重试;桌面端用户重启一次宿主即可。',
324
+ failOriginRejectedHint: '这次安装请求没有通过插件市场的来源校验,被本地服务直接拒绝,所以安装根本没有开始 —— 与插件本身无关。通常是因为市场页面不是从本机地址打开的(比如用了局域网 IP),或宿主页面的来源未被识别。请从本机的插件市场地址(localhost)重新打开页面后重试;桌面端用户重启一次宿主即可。若重启宿主后仍然失败,说明当前 dsh-plugin 版本与宿主不兼容,请用以下命令把市场升级到最新版,再重启一次宿主:dsh plugin --profile {profile} add dsh-plugin@latest。',
323
325
  failDshMissingHint: '系统找不到 dsh 命令(dsh 未加入系统 PATH),无法调用安装器执行安装。请确认 DeepSeek Harness 已正确安装、dsh 已加入 PATH(或重新安装),然后重试。这不是插件本身的问题。',
324
326
  failGitMissingHint: '系统找不到 git 命令(Git 未安装或未加入系统 PATH),无法从 GitHub 源安装插件。请先安装 Git(如 git-scm.com/downloads)或把 git 加入 PATH,重启 DSH 后重试。这不是插件本身的问题。',
325
327
  failPnpmMissingHint: '安装器找到了 dsh,但系统里没有 pnpm 命令(dsh 用它来管理 profile 插件)。请先安装 pnpm(如 npm install -g pnpm),确认 pnpm 已加入 PATH 后重试。这不是插件本身的问题。',
@@ -335,6 +337,9 @@ export const zh = {
335
337
  failPnpmWorkspaceHint: '安装器调用 pnpm 时报「不能把依赖装到 workspace 根目录」(ERR_PNPM_ADDING_TO_ROOT):你的 profile 目录被 pnpm 视为 workspace 根目录,而本次安装命令没有声明在根目录操作,pnpm 直接拒绝执行,因此任何插件装进来都会失败。这不是插件本身的问题——可在 profile 目录(~/.dsh/profiles/<profile>)的 .npmrc 里加一行 ignore-workspace-root-check=true 后重试,或把 DeepSeek Harness 升级到最新版再试。',
336
338
  failPnpmTarballIntegrityHint: 'pnpm 报 ERR_PNPM_MISSING_TARBALL_INTEGRITY:锁文件中的 tarball 条目没有校验值,无法验证下载的包。先查看完整日志,完全退出 DeepSeek Harness,并检查、备份当前 profile 的 package.json、pnpm-lock.yaml 和 pnpm-workspace.yaml。确认依赖来源后,将 pnpm-lock.yaml 改名保留,再通过同一 DSH 安装器安装适配当前宿主版本的插件,重新生成锁文件。重建会重新解析 profile 依赖,请核对新旧版本;保持完整性校验开启。',
337
339
  failPnpmPolicyHint: '安装器调用 pnpm 时被其供应链安全策略拦截——装任何插件都会撞墙,不是插件本身的问题。分两种情况处理:① 提示「Minimum release age」(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION):包发布还不满 24 小时,pnpm 11 出于安全会拒收这种刚发布的新包。可在 pnpm 配置(pnpm-workspace.yaml)里加 minimumReleaseAge: 0 豁免,或等满 24 小时后重试。② 提示「untrusted origin」(ERR_PNPM_UNTRUSTED_ORIGIN):依赖来源不被本机 pnpm 信任,通常是该 profile 里之前用 github: 或非官方 npm 源装过插件,锁文件记录了不受信任的来源。请删除该 profile 目录下的 node_modules 与 pnpm-lock.yaml 后重新安装(可先到「系统日志」或 ~/.dsh/profiles/<profile>/hub.log 查看完整报错)。',
340
+ // minimumReleaseAge 专属:从报错里算出确切的可重试时刻({time}),让用户知道「何时能再装」,
341
+ // 而不是笼统地让等 24 小时(见 issue #121)
342
+ failPnpmReleaseAgeHint: '安装器调用 pnpm 时被其供应链安全策略拦住了,任何插件装进这个 profile 都会先撞上它——与插件本身无关。原因是这个包发布还不满 24 小时,pnpm 11 出于安全会拒收刚发布的新包,这段时间内该 profile 的安装与卸载都会被拦下。可重试时间:{time}(即该包发布满 24 小时后)。如确需立刻安装,可在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml 里加 minimumReleaseAge: 0 豁免,但这会放宽整个 profile 的供应链保护。',
338
343
  // 自定义安装三卡片入口的渠道名(报错/日志溯源:本次安装从哪个入口发起)
339
344
  installChannelNpm: 'NPM 包',
340
345
  installChannelGit: 'GitHub 源码',
@@ -435,13 +440,13 @@ export const en = {
435
440
  dshCmdLabel: 'DeepSeek Harness command',
436
441
  dshCmdPlaceholder: 'Paste an official dsh plugin command',
437
442
  dshCmdInsertHubUpdate: 'Insert update command',
438
- dshCmdExample: 'e.g. dsh plugin --profile web add github:owner/repo, or dsh plugin --profile web update dsh-plugin',
439
- dshCmdInvalid: 'Paste an official dsh plugin command (--profile required, add / update), e.g. dsh plugin --profile web update dsh-plugin',
443
+ dshCmdExample: 'e.g. dsh plugin --profile {profile} add github:owner/repo, or dsh plugin --profile {profile} update dsh-plugin',
444
+ dshCmdInvalid: 'Paste an official dsh plugin command (--profile required, add / update), e.g. dsh plugin --profile {profile} update dsh-plugin',
440
445
  installHelp: 'Help',
441
446
  helpModalTitle: 'Supported command formats',
442
447
  npmInstallHelp: 'lodash or @scope/pkg\tjust type the package name\nnpm install lodash\nnpm i lodash\npnpm add @scope/pkg\npnpm i @scope/pkg\nnpm install -g @scope/cli\tinstalls the CLI globally, not shown in Installed',
443
448
  gitInstallHelp: 'owner/repo\nhttps://github.com/owner/repo\ngithub:owner/repo\ngit+https://github.com/owner/repo.git\ngit@github.com:owner/repo.git',
444
- dshCmdHelp: 'dsh plugin --profile web add <target>\tinstall\ndsh plugin --profile web update <target>\tupdate an installed target to the latest version\ndsh plugin --profile=web add <target>\ndsh plugin --profile=web update <target>\n\t--profile is required and there is no -p shorthand; <target> can be an npm package or github:owner/repo; uninstall from the Installed list',
449
+ dshCmdHelp: 'dsh plugin --profile {profile} add <target>\tinstall\ndsh plugin --profile {profile} update <target>\tupdate an installed target to the latest version\ndsh plugin --profile={profile} add <target>\ndsh plugin --profile={profile} update <target>\n\t--profile is required (prefilled with the current profile here) and there is no -p shorthand; <target> can be an npm package or github:owner/repo; uninstall from the Installed list',
445
450
  globalNpmHint: 'This installs CLI tools globally (npm install -g). They won\'t appear in Installed and no host restart is needed.',
446
451
  installCliBtn: 'Install',
447
452
  // Install channel disabled (security-trust toggle off → card disabled, guides to settings)
@@ -697,7 +702,7 @@ export const en = {
697
702
  // The request origin did not pass the local hub service check (403 untrusted origin): the request
698
703
  // never reached the install flow — nothing to do with the plugin, so point at the local address
699
704
  // and do not offer an issue link
700
- failOriginRejectedHint: 'This install request did not pass the marketplace origin check and was rejected by the local service, so the install never started — this is not about the plugin itself. It usually means the marketplace page was not opened from a local address (for example a LAN IP), or the host page origin was not recognized. Reopen the marketplace from localhost and retry; on the desktop app, restart the host once.',
705
+ failOriginRejectedHint: 'This install request did not pass the marketplace origin check and was rejected by the local service, so the install never started — this is not about the plugin itself. It usually means the marketplace page was not opened from a local address (for example a LAN IP), or the host page origin was not recognized. Reopen the marketplace from localhost and retry; on the desktop app, restart the host once. If it still fails after restarting the host, your dsh-plugin version is incompatible with the host — upgrade the marketplace with dsh plugin --profile {profile} add dsh-plugin@latest, then restart the host again.',
701
706
  failDshMissingHint: 'The dsh command could not be found (it is not on the system PATH), so the installer could not run. Please make sure DeepSeek Harness is installed correctly and dsh is on your PATH (or reinstall it), then retry. This is not a problem with the plugin itself.',
702
707
  failGitMissingHint: 'The git command could not be found (Git is not installed or not on the system PATH), so GitHub-source plugins cannot be installed. Please install Git (e.g. from git-scm.com/downloads) or add git to your PATH, restart DSH, then retry. This is not a problem with the plugin itself.',
703
708
  failPnpmMissingHint: 'The installer found dsh, but pnpm is missing on this machine (dsh uses it to manage profile plugins). Please install pnpm first (e.g. npm install -g pnpm), make sure it is on your PATH, then retry. This is not a problem with the plugin itself.',
@@ -714,6 +719,9 @@ export const en = {
714
719
  failPnpmWorkspaceHint: 'pnpm refused to add the dependency to the workspace root (ERR_PNPM_ADDING_TO_ROOT): the profile directory is treated as a pnpm workspace root, and this install command did not declare that it operates at the root, so pnpm rejected it outright — no plugin can be installed into that profile. This is not a problem with the plugin itself. Add `ignore-workspace-root-check=true` to the `.npmrc` inside the profile directory (~/.dsh/profiles/<profile>) and retry, or upgrade DeepSeek Harness and try again.',
715
720
  failPnpmTarballIntegrityHint: 'pnpm reported ERR_PNPM_MISSING_TARBALL_INTEGRITY: a tarball entry in the lockfile has no checksum to verify the downloaded package. Read the full log, fully quit DeepSeek Harness, and inspect and back up package.json, pnpm-lock.yaml and pnpm-workspace.yaml in the affected profile. Once the dependency sources are verified, rename pnpm-lock.yaml to preserve it, then use the same DSH installer to install a plugin version compatible with the host and regenerate the lockfile. This resolves profile dependencies again: compare the old and new versions, and keep integrity checks enabled.',
716
721
  failPnpmPolicyHint: 'The installer was blocked by pnpm supply-chain security policy — every plugin install would hit the same wall, so this is not a problem with the plugin itself. Two cases: ① "Minimum release age" (ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION): the package was published less than 24 hours ago, and pnpm 11 refuses freshly-published packages for safety. Add minimumReleaseAge: 0 to your pnpm config (pnpm-workspace.yaml) to opt out, or wait until the 24 hours have passed and retry. ② "untrusted origin" (ERR_PNPM_UNTRUSTED_ORIGIN): a dependency source is not trusted by this machine, usually because plugins were previously installed via github: or a non-official npm registry, so the lockfile records an untrusted source. Delete node_modules and pnpm-lock.yaml under the profile directory (see System Logs or ~/.dsh/profiles/<profile>/hub.log for the full output), then reinstall.',
722
+ // minimumReleaseAge only: compute the exact retry time ({time}) from the error so the user knows
723
+ // when installs will work again, instead of the vague "wait 24 hours" (see issue #121)
724
+ failPnpmReleaseAgeHint: 'The installer was blocked by pnpm supply-chain security policy, and every plugin installed into this profile would hit the same wall — this is not a problem with the plugin itself. The package was published less than 24 hours ago, and pnpm 11 refuses freshly-published packages for safety, so both installs and removals are blocked for this profile in the meantime. Retry after: {time} (i.e. 24 hours after the package was published). If you must install right now, add minimumReleaseAge: 0 to pnpm-workspace.yaml inside the profile directory (~/.dsh/profiles/<profile>) to opt out, but that loosens supply-chain protection for the whole profile.',
717
725
  // Custom install entry channel names (the three cards) — used in error/log traceability
718
726
  installChannelNpm: 'NPM package',
719
727
  installChannelGit: 'GitHub source',
@@ -158,9 +158,32 @@ function missingTarballIntegrity(message: string): boolean {
158
158
  return /\bERR_PNPM_MISSING_TARBALL_INTEGRITY\b/.test(message)
159
159
  }
160
160
 
161
- /** The same policy subtype selects recovery copy in live and saved failures. */
162
- export function pnpmPolicyHintOf(message: string): 'failPnpmTarballIntegrityHint' | 'failPnpmPolicyHint' {
163
- return missingTarballIntegrity(message) ? 'failPnpmTarballIntegrityHint' : 'failPnpmPolicyHint'
161
+ /** pnpm 拒收「刚发布」包时,报错里带着发布时间;这里从报错文本算出「发布满 24h」的可重试时刻(UTC,分钟精度)。 */
162
+ export function releaseAgeRetryAfter(message: string): string | null {
163
+ const match = /published at (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/.exec(message)
164
+ if (match === null) return null
165
+ const published = Date.parse(match[1])
166
+ if (Number.isNaN(published)) return null
167
+ return `${new Date(published + 24 * 60 * 60 * 1000).toISOString().slice(0, 16)}Z`
168
+ }
169
+
170
+ /**
171
+ * The same policy subtype selects recovery copy in live and saved failures.
172
+ * minimumReleaseAge 只有在能算出可重试时刻时才走专属文案(给出确切的「何时可再试」),
173
+ * 算不出时退回通用文案,避免出现「预计可重试时间:」后接空值的残缺句子。
174
+ */
175
+ export function pnpmPolicyHintOf(message: string): 'failPnpmTarballIntegrityHint' | 'failPnpmReleaseAgeHint' | 'failPnpmPolicyHint' {
176
+ if (missingTarballIntegrity(message)) return 'failPnpmTarballIntegrityHint'
177
+ if (/ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|Minimum release age/i.test(message)
178
+ && releaseAgeRetryAfter(message) !== null) {
179
+ return 'failPnpmReleaseAgeHint'
180
+ }
181
+ return 'failPnpmPolicyHint'
182
+ }
183
+
184
+ /** 与 pnpmPolicyHintOf 配套的文案参数:release-age 场景给出可重试时刻({time})。 */
185
+ export function pnpmPolicyHintParams(message: string): Record<string, string> {
186
+ return { time: releaseAgeRetryAfter(message) ?? '' }
164
187
  }
165
188
 
166
189
  /**