dsh-plugin 2.3.2 → 2.3.3

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 (84) hide show
  1. package/README.en.md +3 -9
  2. package/README.md +3 -9
  3. package/client/client.js +367 -362
  4. package/client/client.js.map +1 -1
  5. package/lib/http/catalog.js +4 -4
  6. package/lib/http/diagnostics.js +4 -4
  7. package/lib/http/install.js +13 -13
  8. package/lib/http/npm-release-age.js +3 -3
  9. package/lib/http/shared.js +1 -1
  10. package/lib/services/host-version.js +2 -2
  11. package/lib/services/install/dsh-compat.js +1 -1
  12. package/lib/services/install/npm-resolve.js +1 -1
  13. package/lib/services/install/operation-log.js +3 -3
  14. package/lib/services/install/preflight.js +1 -1
  15. package/lib/services/install/registry-fallback.js +3 -3
  16. package/lib/services/install/release-target.js +2 -2
  17. package/lib/services/install/task-queue.js +35 -20
  18. package/lib/services/loader.js +8 -8
  19. package/lib/services/npm-release-age.js +8 -8
  20. package/lib/services/probe.js +12 -12
  21. package/lib/services/profile/installed-versions.js +2 -2
  22. package/lib/services/profile/profile.js +2 -2
  23. package/lib/types/services/host-version.d.ts +1 -1
  24. package/lib/types/services/install/install-types.d.ts +5 -5
  25. package/lib/types/services/install/operation-log.d.ts +1 -1
  26. package/lib/types/services/install/preflight.d.ts +1 -1
  27. package/lib/types/services/install/registry-fallback.d.ts +1 -1
  28. package/lib/types/services/install/release-target.d.ts +1 -1
  29. package/lib/types/services/install/task-queue.d.ts +10 -7
  30. package/lib/types/services/loader.d.ts +5 -5
  31. package/lib/types/services/npm-release-age.d.ts +5 -5
  32. package/lib/types/services/probe.d.ts +4 -4
  33. package/package.json +1 -1
  34. package/src/client/components/PluginHubSection.tsx +29 -29
  35. package/src/client/components/catalog/CatalogList.tsx +1 -1
  36. package/src/client/components/catalog/PluginCard.tsx +3 -3
  37. package/src/client/components/modals/AboutModal.tsx +2 -2
  38. package/src/client/components/modals/ConfirmDialog.tsx +2 -2
  39. package/src/client/components/modals/HubUpdateModal.tsx +3 -3
  40. package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
  41. package/src/client/components/modals/LogsPathModal.tsx +1 -1
  42. package/src/client/components/modals/NotificationsModal.tsx +2 -2
  43. package/src/client/components/modals/modals.tsx +10 -10
  44. package/src/client/components/views/CustomInstallView.tsx +2 -2
  45. package/src/client/components/views/InstalledView.tsx +2 -2
  46. package/src/client/components/views/SettingsView.tsx +3 -3
  47. package/src/client/data/catalog.ts +1 -1
  48. package/src/client/data/host.ts +2 -2
  49. package/src/client/data/hub.ts +3 -3
  50. package/src/client/hooks/useCatalog.ts +14 -14
  51. package/src/client/hooks/useSettings.ts +2 -2
  52. package/src/client/hooks/useTaskQueue.ts +30 -23
  53. package/src/client/locales.ts +15 -15
  54. package/src/client/logic/constants.ts +1 -1
  55. package/src/client/logic/ensure-plugin-css.ts +2 -2
  56. package/src/client/logic/failures.ts +9 -9
  57. package/src/client/logic/install-command.ts +1 -2
  58. package/src/client/logic/installed.ts +1 -1
  59. package/src/client/logic/release-age.ts +10 -10
  60. package/src/client/logic/urls.ts +3 -3
  61. package/src/client/styles/Header.module.css +1 -1
  62. package/src/client/styles/Modal.module.css +2 -2
  63. package/src/client/styles/SectionTabs.module.css +1 -1
  64. package/src/client/styles/tokens.module.css +1 -1
  65. package/src/client/types.ts +4 -4
  66. package/src/server/http/catalog.ts +4 -4
  67. package/src/server/http/diagnostics.ts +4 -4
  68. package/src/server/http/install.ts +13 -13
  69. package/src/server/http/npm-release-age.ts +3 -3
  70. package/src/server/http/shared.ts +1 -1
  71. package/src/server/services/host-version.ts +2 -2
  72. package/src/server/services/install/dsh-compat.ts +1 -1
  73. package/src/server/services/install/install-types.ts +5 -5
  74. package/src/server/services/install/npm-resolve.ts +1 -1
  75. package/src/server/services/install/operation-log.ts +3 -3
  76. package/src/server/services/install/preflight.ts +1 -1
  77. package/src/server/services/install/registry-fallback.ts +3 -3
  78. package/src/server/services/install/release-target.ts +2 -2
  79. package/src/server/services/install/task-queue.ts +40 -26
  80. package/src/server/services/loader.ts +10 -10
  81. package/src/server/services/npm-release-age.ts +10 -10
  82. package/src/server/services/probe.ts +12 -12
  83. package/src/server/services/profile/installed-versions.ts +2 -2
  84. package/src/server/services/profile/profile.ts +2 -2
@@ -32,7 +32,7 @@ function fetchCatalogOnce(target, proxy) {
32
32
  return task;
33
33
  }
34
34
  /** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
35
- * maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时兜底使用。 */
35
+ * maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时使用。 */
36
36
  function readCatalogCache(file, maxAgeMs) {
37
37
  try {
38
38
  const entry = JSON.parse(readFileSync(file, 'utf8'));
@@ -46,7 +46,7 @@ function readCatalogCache(file, maxAgeMs) {
46
46
  }
47
47
  return null;
48
48
  }
49
- /** 写缓存:目录不存在则创建;写失败静默忽略(缓存只是加速,不影响功能)。 */
49
+ /** 写缓存:目录不存在则创建;写失败直接忽略(缓存只是加速,不影响功能)。 */
50
50
  function writeCatalogCache(file, body) {
51
51
  try {
52
52
  mkdirSync(dirname(file), { recursive: true });
@@ -92,7 +92,7 @@ export function registerCatalogRoutes(webServer, profile) {
92
92
  if (!requireMethod(request, response, 'GET'))
93
93
  return;
94
94
  // 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
95
- // 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理口径,
95
+ // 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理规则,
96
96
  // 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
97
97
  const url = new URL(request.url ?? '/', 'http://localhost');
98
98
  const settings = loadSettings(profile);
@@ -131,7 +131,7 @@ export function registerCatalogRoutes(webServer, profile) {
131
131
  return;
132
132
  }
133
133
  catch {
134
- // 兜底缓存也损坏:落到下面的 502
134
+ // 备用缓存也损坏:落到下面的 502
135
135
  }
136
136
  }
137
137
  // 带上 curl 退出码与 stderr 末行:只回 error 字段时,用户贴出来的响应里
@@ -13,7 +13,7 @@ import { hostDshVersion, toolVersion } from '../services/host-version.js';
13
13
  import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
14
14
  import { appendLog } from '../services/log.js';
15
15
  import { readJsonBody, requireTrustedPost, sendJson } from './shared.js';
16
- /** 本机实际生效的 npm registry:诊断与安装同口径 —— 设置里留空时读用户 npm 配置
16
+ /** 本机实际生效的 npm registry:诊断与安装同一套规则 —— 设置里留空时读用户 npm 配置
17
17
  * (含 profile 目录的 .npmrc),而不是硬探官方源,避免「能装但诊断说源不通」。
18
18
  * 读不到(npm 缺失/超时/输出异常)时回退官方源。 */
19
19
  function configuredNpmRegistry(profile) {
@@ -76,7 +76,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
76
76
  }
77
77
  const settings = loadSettings(profile);
78
78
  // npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
79
- // 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同口径。
79
+ // 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同一套规则。
80
80
  const registry = settings.npmRegistry.replace(/\/+$/, '') || configuredNpmRegistry(profile);
81
81
  // 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub 行探小仓库、
82
82
  // 目录站按 Range 取 1KB(探市场真实下载的 plugins.zh.json,但不整份下载)。
@@ -92,7 +92,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
92
92
  { key: 'catalog', url: 'https://api.dsh-plugin.org/plugins.zh.json', display: '', cmd: 'curl -r 0-1023 https://api.dsh-plugin.org/plugins.zh.json', range: '0-1023' },
93
93
  ];
94
94
  // 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
95
- // 验证「代理能不能把请求带出去」。与安装同口径:curl 子进程注入该代理 env。
95
+ // 验证「代理能不能把请求带出去」。与安装同一套规则:curl 子进程注入该代理 env。
96
96
  // 设置里未配置代理时不显示该行(跟随系统代理/直连,无「代理本身」可验证)。
97
97
  if (settings.proxy !== '') {
98
98
  allChecks.push({ key: 'proxy', url: 'https://github.com', display: settings.proxy, cmd: `curl -s -o /dev/null -x ${settings.proxy} https://github.com`, proxy: settings.proxy });
@@ -115,7 +115,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
115
115
  const results = [];
116
116
  for (const c of checks) {
117
117
  send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display });
118
- // git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时兜底
118
+ // git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时保护
119
119
  const r = c.git
120
120
  ? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
121
121
  : await probe(c.url, c.proxy, c.range);
@@ -16,7 +16,7 @@ import { lookupReleaseAge } from '../services/npm-release-age.js';
16
16
  import { profileDirectory } from '../services/profile/profile.js';
17
17
  import { readInstalled } from './catalog.js';
18
18
  import { COMMAND_TIMEOUT_MS, readJsonBody, requireTrustedPost, sendJson } from './shared.js';
19
- /** 安装/卸载 CLI 的子进程环境:透传宿主环境并注入 HTTP(S) 代理,
19
+ /** 安装/卸载 CLI 的子进程环境:原样传递宿主环境并注入 HTTP(S) 代理,
20
20
  * 代理来源与诊断一致(设置代理 → 系统代理 → 宿主 env);registry 由任务队列按候选源逐次注入。 */
21
21
  function mutationEnv(settings) {
22
22
  const env = { ...process.env, CI: 'true' };
@@ -66,15 +66,15 @@ export function registerInstallRoutes(webServer, profile, loader) {
66
66
  const lang = typeof body === 'object' && body !== null && body.lang === 'en'
67
67
  ? 'en'
68
68
  : 'zh';
69
- // 代理口径与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
69
+ // 代理规则与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
70
70
  const effectiveProxy = settings.proxy !== ''
71
71
  ? settings.proxy
72
72
  : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
73
- // 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm 配置(与诊断同口径)。
73
+ // 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm 配置(与诊断同一套规则)。
74
74
  // 只有公共源允许失败后换源;自定义/内网源保持单一候选,避免私有包名泄露给公共源。
75
75
  const registries = registryCandidates(settings.npmRegistry, settings.npmRegistry === '' ? userNpmRegistry(profileDirectory(profile)) : '', settings.registryFallback);
76
76
  // 全局 npm 安装(`npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
77
- // 包列表优先取 body.globalNpm,兜底从 rawRepo 解析,保证直接调 API 传完整命令也能识别。
77
+ // 包列表优先取 body.globalNpm,取不到时再从 rawRepo 解析,保证直接调 API 传完整命令也能识别。
78
78
  const bodyGlobalNpm = Array.isArray(body.globalNpm)
79
79
  ? body.globalNpm.filter((x) => typeof x === 'string' && x !== '')
80
80
  : [];
@@ -85,7 +85,7 @@ export function registerInstallRoutes(webServer, profile, loader) {
85
85
  sendJson(response, 400, { error: 'unsupported install target' });
86
86
  return;
87
87
  }
88
- // 通道门禁与安装通道同构:全局安装本质是 npm 操作,吃「启用 NPM 安装」开关
88
+ // 通道校验与安装通道同构:全局安装本质是 npm 操作,吃「启用 NPM 安装」开关
89
89
  if (source !== 'catalog' && !settings.enableNpmInstall) {
90
90
  sendJson(response, 403, { error: 'npm installs are disabled by the security settings' });
91
91
  return;
@@ -118,8 +118,8 @@ export function registerInstallRoutes(webServer, profile, loader) {
118
118
  return;
119
119
  }
120
120
  const target = rawRepo;
121
- // 钉住精确版本(Hub 自我更新):最新版可能仍在 pnpm 供应链门禁内,
122
- // 客户端提示的是「已过门禁」的版本,服务端必须按该版本安装而不是 @latest。
121
+ // 锁定精确版本(Hub 自我更新):最新版可能仍在 pnpm 发布冷却期内,
122
+ // 客户端提示的是「已过冷却期」的版本,服务端必须按该版本安装而不是 @latest。
123
123
  const pinVersion = typeof body.version === 'string'
124
124
  ? body.version.trim()
125
125
  : '';
@@ -179,12 +179,12 @@ export function registerInstallRoutes(webServer, profile, loader) {
179
179
  }
180
180
  ordered = [chosen, ...registries.filter((r) => r !== chosen)];
181
181
  }
182
- // 新安装钉住「已过供应链门禁」的版本:pnpm 对发布未满 24h 的版本会静默回退到很旧的版本,
183
- // 用户装完才发现功能缺失且没有任何提示。更新已有目标时客户端已按门禁筛选,继续走 @latest。
182
+ // 新安装锁定「已过发布冷却期」的最高稳定版:pnpm 对发布还不满 24 小时的版本会不提示地回退,
183
+ // 锁定后装到的就是预期版本。更新已有目标时客户端已按冷却期筛选,继续走 @latest。
184
184
  let installSpec = pinVersion !== '' ? `${target}@${pinVersion}` : undefined;
185
185
  if (installSpec === undefined && already === null) {
186
- // 解析源与安装首选源一致,钉住的版本一定存在于真正会用的那个源;解析失败则不钉,不阻断安装。
187
- // 超时压到 5s:网络刚探通,慢也只在极端情况,不能为查版本把安装请求拖住 8s
186
+ // 解析源与安装首选源一致,锁定的版本一定存在于真正会用的那个源;解析失败则不锁定,不阻断安装。
187
+ // 超时 5s:网络刚探测通过,卡住时也不能把安装请求拖到默认的 8s
188
188
  const release = await lookupReleaseAge(target, ordered[0] ?? '', effectiveProxy, 5000);
189
189
  if (release !== null && release.installable !== null)
190
190
  installSpec = `${target}@${release.installable.version}`;
@@ -198,7 +198,7 @@ export function registerInstallRoutes(webServer, profile, loader) {
198
198
  // 后台启动 CLI;客户端轮询 /status 获取进度
199
199
  const task = startPluginMutation({
200
200
  // mode=update(已安装目标的覆盖更新)以 update 动作入队:
201
- // 日志按 update 类别落盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
201
+ // 日志按 update 类别写入磁盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
202
202
  action: mode === 'update' ? 'update' : 'add',
203
203
  profile,
204
204
  target,
@@ -206,7 +206,7 @@ export function registerInstallRoutes(webServer, profile, loader) {
206
206
  displayTarget: githubRepoOf(displayRepo) ?? undefined,
207
207
  // 覆盖更新:npm 命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
208
208
  updateNpm: mode === 'update',
209
- // 运行中 loader:安装成功后把新插件热挂进 loader,非桌面端 + 纯服务端插件无需重启即生效
209
+ // 运行中 loader:安装成功后把新插件热加载进 loader,非桌面端 + 纯服务端插件无需重启即生效
210
210
  loader,
211
211
  timeoutMs: COMMAND_TIMEOUT_MS,
212
212
  env: mutationEnv(settings),
@@ -4,7 +4,7 @@
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
6
  * npm 包发布时间查询路由:按已安装插件的 npm 包名按需查询最新版本的发布时间,
7
- * 供客户端判定 pnpm 24h 供应链门禁(发布未满门槛的版本不提示更新)。
7
+ * 供客户端判定 pnpm 24h 发布冷却期(发布还不满 24 小时的版本不提示更新)。
8
8
  */
9
9
  import { systemProxy } from '../services/probe.js';
10
10
  import { lookupReleaseAge, parseReleaseAgeRequest } from '../services/npm-release-age.js';
@@ -27,12 +27,12 @@ export function registerNpmReleaseAgeRoutes(webServer, profile) {
27
27
  return;
28
28
  }
29
29
  const settings = loadSettings(profile);
30
- // 代理口径与安装/目录一致:设置里的代理 → 系统代理 → 环境变量 → 直连
30
+ // 代理规则与安装/目录一致:设置里的代理 → 系统代理 → 环境变量 → 直连
31
31
  const proxy = settings.proxy !== ''
32
32
  ? settings.proxy
33
33
  : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
34
34
  // 并发查询:单包各自带超时且失败即 null,整体不因任一包失败而失败
35
- // 查询源与安装首选源同口径(设置 → 本机 npm 配置):安装钉住的版本必须来自真正要用的那个源
35
+ // 查询源与安装首选源同一套规则(设置 → 本机 npm 配置):安装锁定的版本必须来自真正要用的那个源
36
36
  const registry = settings.npmRegistry !== ''
37
37
  ? settings.npmRegistry
38
38
  : userNpmRegistry(profileDirectory(profile));
@@ -32,7 +32,7 @@ function findPids() {
32
32
 
33
33
  const targets = findPids()
34
34
  // 一个监听进程都没找到 = 端口探测本身就失败了(netstat 输出格式 / 本地化差异),
35
- // 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 静默退出,
35
+ // 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 无提示地退出,
36
36
  // 用户却看到「重启完成」——不制造这种假象,留一行可诊断的记录后退出。
37
37
  if (targets.length === 0) {
38
38
  logLine('restart aborted: no listener found on port ' + port)
@@ -4,7 +4,7 @@
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
6
  * 宿主与命令行工具的版本探测:dsh 版本同时用于诊断快照和「插件要求更高 DSH」判定,
7
- * pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms 超时兜底,拿不到一律 null,绝不挂起。
7
+ * pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms 超时保护,拿不到一律 null,绝不挂起。
8
8
  */
9
9
  import { readFileSync } from 'node:fs';
10
10
  import { dirname, join, resolve } from 'node:path';
@@ -38,7 +38,7 @@ export function toolVersion(cmd, args) {
38
38
  }
39
39
  let cachedDshVersion = null;
40
40
  /** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
41
- * (dsh 不在 PATH 时兜底)。探测成功即缓存,失败不缓存以便下次重试。 */
41
+ * (dsh 不在 PATH 时使用)。探测成功即缓存,失败不缓存以便下次重试。 */
42
42
  export function hostDshVersion() {
43
43
  if (cachedDshVersion !== null)
44
44
  return cachedDshVersion;
@@ -9,7 +9,7 @@
9
9
  * 不再把这类失败归成「未分类」丢给用户或插件作者。
10
10
  *
11
11
  * 只在失败路径调用:peer 不满足本身通常只是警告,成功安装时不该出现这个提示;
12
- * 任何网络/解析异常都静默返回 null,不改变原有失败处理。
12
+ * 任何网络/解析异常都悄悄返回 null,不改变原有失败处理。
13
13
  */
14
14
  import { hostDshVersion } from '../host-version.js';
15
15
  /** 从安装目标里取 npm 包名:`@scope/pkg@1.2.3` → `@scope/pkg`。
@@ -75,7 +75,7 @@ export function isDshNpmPackageForRepo(packageName, repo, registry = '', proxy =
75
75
  }
76
76
  /**
77
77
  * 解析仓库改名后的规范 `owner/repo`:GitHub 对已改名仓库返回 301,跟随跳转取最终地址。
78
- * 与其它 GitHub 访问同口径走 curl + 代理 env(Node 内置 https 读不到系统代理)。
78
+ * 与其它 GitHub 访问同一套规则走 curl + 代理 env(Node 内置 https 读不到系统代理)。
79
79
  * 未改名、仓库不存在、网络失败或 curl 不可用都返回 '',调用方按原名继续。
80
80
  */
81
81
  async function canonicalRepo(repo, proxy) {
@@ -7,7 +7,7 @@
7
7
  * 回显到控制台,真正的 pnpm 报错写进该路径的 operation 日志。这里从输出里解析出该路径
8
8
  * 并读取其尾部,补进失败信息 —— 否则前端拿不到任何错误特征,只能把失败归为「未知」。
9
9
  * 只允许读 profile 目录下的 pnpm.log(dsh 自己的 operation 日志),
10
- * 防止插件把任意路径打印进日志造成越权读取;任何异常都静默降级(返回 null),
10
+ * 防止插件把任意路径打印进日志造成越权读取;任何异常都降级为 null,
11
11
  * 不改变原有失败处理。
12
12
  */
13
13
  import { readFileSync, readdirSync, statSync } from 'node:fs';
@@ -55,7 +55,7 @@ export function operationLogPathOf(output) {
55
55
  /**
56
56
  * 只允许读 profile 目录下的 pnpm.log(dsh 自己的操作日志):
57
57
  * 绝对路径必须落在该目录内,相对路径按该目录解析。
58
- * 未提供 profileDir 时退回旧口径(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
58
+ * 未提供 profileDir 时退回旧规则(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
59
59
  */
60
60
  function allowedLogPath(raw, profileDir) {
61
61
  const path = resolve(profileDir ?? '', raw);
@@ -101,7 +101,7 @@ export function hasSubstantiveOutput(output) {
101
101
  /**
102
102
  * 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
103
103
  * 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
104
- * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容兜底。
104
+ * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容补全。
105
105
  * 只认本次运行产生的新文件(mtime 晚于启动基线),避免复述上一次安装的日志。
106
106
  */
107
107
  export function createOperationLogTap(profileDir) {
@@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process';
8
8
  /** 缺 git 的失败标记:前端按 `[git-missing]` 归类并给「安装 git」指引(乱码免疫)。 */
9
9
  export const GIT_MISSING_DETAIL = '[git-missing] the git CLI was not found on this machine (install git and add it to the PATH, then retry)';
10
10
  /** 命令存在性探测:which/where 找 PATH 里的命令。
11
- * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
11
+ * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验处理。 */
12
12
  export function commandOnPath(cmd, env) {
13
13
  try {
14
14
  const probe = process.platform === 'win32' ? 'where' : 'which';
@@ -1,6 +1,6 @@
1
1
  /**
2
- * npm 源降级:当前源是公共镜像时,传输层失败后按「当前源 → 官方 → npmmirror」
3
- * 自动换源重试;自定义/内网源不降级,避免私有包名泄露给公共源。
2
+ * npm 换源:当前源是公共镜像时,传输层失败后按「当前源 → 官方 → npmmirror」
3
+ * 自动换源重试;自定义/内网源不换源,避免私有包名泄露给公共源。
4
4
  */
5
5
  import { spawnSync } from 'node:child_process';
6
6
  /** 允许自动切换的公共源:只放官方与大厂/高校镜像。 */
@@ -34,7 +34,7 @@ export function isPublicRegistry(value) {
34
34
  }
35
35
  /**
36
36
  * 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
37
- * 只有公共源才追加后备(最多 3 个);关闭降级或源不在白名单时只返回它自己。
37
+ * 只有公共源才追加后备(最多 3 个);关闭换源或源不在白名单时只返回它自己。
38
38
  */
39
39
  export function registryCandidates(configured, userRegistry, enabled) {
40
40
  const base = normalizeRegistry(configured) || normalizeRegistry(userRegistry) || PUBLIC_REGISTRIES[0];
@@ -6,13 +6,13 @@
6
6
  * 预构建 release 直链的仓库身份解析。部分插件(如 loopx)当初以 GitHub release 的 .tgz
7
7
  * 安装过,仓库 HEAD 反而是 monorepo 根、走 git 直装会因缺入口文件失败;老安装记录里留的
8
8
  * 就是这种 release 直链。这里把「历史安装目标」还原成所属仓库身份,供已装态读回、队列
9
- * 去重与展示复用(单一事实来源,避免两端口径漂移)。
9
+ * 去重与展示复用(单一事实来源,避免两端规则漂移)。
10
10
  *
11
11
  * 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
12
12
  * 输入是记录/展示数据、绝非 shell 代码,绝不执行其文本;跨仓跳转、外部主机、凭据、
13
13
  * 移动的 latest 链接、query/fragment、多余 CLI 参数与 shell 片段一律拒绝。
14
14
  */
15
- /** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同口径)。 */
15
+ /** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同一套规则)。 */
16
16
  const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i;
17
17
  /** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */
18
18
  const RELEASE_URL_RE = /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/releases\/download\/([A-Za-z0-9._+-]+)\/([A-Za-z0-9._+-]+\.tgz)$/;
@@ -132,7 +132,7 @@ function pushLine(task, line) {
132
132
  task.progress = Math.max(task.progress, estimate);
133
133
  }
134
134
  else if (task.status === 'running' && task.progress < 85) {
135
- // 兜底:行无解析格式也随输出量推进,保证进度条始终在动(不会卡在 0)
135
+ // 补充:行无解析格式也随输出量推进,保证进度条始终在动(不会卡在 0)
136
136
  task.progress = Math.max(task.progress, 6 + Math.min(79, task.lines.length * 2));
137
137
  }
138
138
  }
@@ -202,7 +202,7 @@ export function desktopCliBootstrap(entry) {
202
202
  return null;
203
203
  }
204
204
  /**
205
- * 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
205
+ * 传给 CLI 的安装目标:锁定版本优先(Hub 自我更新提示的是已过发布冷却期的版本),
206
206
  * 其次 npm 的 add/update 都显式 `@latest`;git 源、全局安装、已带版本/tag 的 spec 保持原样。
207
207
  */
208
208
  export function npmInstallTargetOf(target, opts) {
@@ -215,6 +215,13 @@ export function npmInstallTargetOf(target, opts) {
215
215
  return target;
216
216
  return `${target}@latest`;
217
217
  }
218
+ /** 重复卸载:目标已不在 profile 依赖里,pnpm 报「没有这个依赖」。
219
+ * 结果与卸载成功等价 —— 按失败上报会弹出错误并自动提一条不存在的 Issue。 */
220
+ export function isAlreadyRemoved(action, output) {
221
+ if (action !== 'remove')
222
+ return false;
223
+ return /ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS|no such dependency found/i.test(output);
224
+ }
218
225
  function stopChild(child) {
219
226
  if (process.platform === 'win32' && child.pid !== undefined) {
220
227
  const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
@@ -380,10 +387,10 @@ function spawnMutation(options) {
380
387
  }
381
388
  // 安装/更新 npm 包都显式 @latest:pnpm add <pkg>(无版本)对已存在的精确 spec 是幂等的,
382
389
  // 会原样留用旧版本(更新不生效,普通安装也可能装回不支持当前宿主的旧包)。
383
- // update 动作同样执行 `add <target>@latest`,CLI 动词透传 add,不与官方 update 动词分叉。
390
+ // update 动作同样执行 `add <target>@latest`,CLI 动词原样传递 add,不与官方 update 动词分叉。
384
391
  const isUpdate = action === 'update' || updateNpm === true;
385
392
  const cliAction = action === 'update' ? 'add' : action;
386
- // 钉住版本优先于 @latest:提示的是「已过门禁」的版本,装 latest 会被 pnpm 拒绝
393
+ // 锁定版本优先于 @latest:提示的是「已过冷却期」的版本,装 latest 会被 pnpm 拒绝
387
394
  const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec, add: cliAction === 'add' });
388
395
  const args = isGlobalNpm
389
396
  ? ['install', '-g', ...(globalNpm ?? [])]
@@ -395,7 +402,7 @@ function spawnMutation(options) {
395
402
  if (task && !task.attempts.includes(executed)) {
396
403
  task.attempts.push(executed);
397
404
  }
398
- // 单次 spawn 并捕获结果:流式输出打日志 + 超时/取消兜底,close 后 resolve。
405
+ // 单次 spawn 并捕获结果:流式输出打日志 + 超时、取消时收尾,close 后 resolve。
399
406
  // 不做任何终态后处理(exitCode/status/needsRestart 由下方统一收尾),
400
407
  // 这样全局 npm 首次失败后可以干净地换参数重试一次。
401
408
  const spawnOnce = (attemptArgs, registry) => new Promise((resolveOnce) => {
@@ -438,7 +445,7 @@ function spawnMutation(options) {
438
445
  }
439
446
  }
440
447
  // invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):
441
- // 没有它时保持 env 原样透传,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
448
+ // 没有它时保持 env 原样传递,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
442
449
  // registry 最后写入:换源重试只改这一层,pnpm/npm 都认 npm_config_registry。
443
450
  const childEnv = { ...(env ?? process.env), ...(invocation.env ?? {}) };
444
451
  if (registry !== undefined) {
@@ -477,12 +484,12 @@ function spawnMutation(options) {
477
484
  }
478
485
  if (task)
479
486
  runningChildren.set(task.id, child);
480
- // 兜底定时器句柄:整段超时 / exit 后 close 宽限 / 强制收尾
487
+ // 最终超时定时器句柄:整段超时 / exit 后 close 宽限 / 强制收尾
481
488
  let timer;
482
489
  let progressTimer;
483
490
  let exitWatch;
484
491
  let forceSettle;
485
- // 唯一收尾入口:close / error / 兜底超时 三者只放行一次,确保任务持续推进
492
+ // 唯一收尾入口:close / error / 最终超时 三者只放行一次,确保任务持续推进
486
493
  const settleOnce = (result) => {
487
494
  if (settled)
488
495
  return;
@@ -503,7 +510,7 @@ function spawnMutation(options) {
503
510
  timedOut = true;
504
511
  stopChild(child);
505
512
  }, timeoutMs);
506
- // 时间兜底:即使 CLI 静默不吐进度行,进度条也单调推进,不会卡在 0
513
+ // 时间补充:即使 CLI 悄悄不吐进度行,进度条也单调推进,不会卡在 0
507
514
  progressTimer = setInterval(() => {
508
515
  if (task && task.status === 'running' && task.progress < 85)
509
516
  task.progress += 1;
@@ -515,7 +522,7 @@ function spawnMutation(options) {
515
522
  else
516
523
  stderr = (stderr + text).slice(-CAPTURE_LIMIT_BYTES);
517
524
  if (task) {
518
- // 原生透传 pnpm/npm 输出:stdout/stderr 原样入日志,不做格式化,保证报错信息完整可见
525
+ // 原生原样传递 pnpm/npm 输出:stdout/stderr 原样入日志,不做格式化,保证报错信息完整可见
519
526
  for (const line of text.split(/\r?\n|\r/)) {
520
527
  if (line.trim() !== '')
521
528
  pushLine(task, line.trimEnd());
@@ -534,7 +541,7 @@ function spawnMutation(options) {
534
541
  if (task && task.status !== 'cancelled')
535
542
  pushLine(task, '[stale pipe] force closing child');
536
543
  stopChild(child);
537
- // 清理后极端情况下 close 仍未触发(kill 失败):再兜底强制收尾
544
+ // 清理后极端情况下 close 仍未触发(kill 失败):再强制收尾一次
538
545
  forceSettle = setTimeout(() => {
539
546
  if (settled)
540
547
  return;
@@ -597,7 +604,7 @@ function spawnMutation(options) {
597
604
  pushLine(task, '[npm] peer-set resolution crashed npm (known npm bug) — retrying with --legacy-peer-deps');
598
605
  result = await spawnOnce(retryArgs, activeRegistry);
599
606
  }
600
- // 超时被强杀 ≠ 没装上:命中的安装可能已经落盘(日志里 pnpm 已打印 Done,只是 dsh 进程迟迟不退出
607
+ // 超时被强杀 ≠ 没装上:命中的安装可能已经写入磁盘(日志里 pnpm 已打印 Done,只是 dsh 进程迟迟不退出
601
608
  // 而被我们超时掐掉)。判失败之前先复核 profile —— 目标已在依赖里、且声明分发的入口文件都在,
602
609
  // 就按成功收尾,避免「其实装好了却报安装失败」并被自动归档成未知失败。
603
610
  if (result.timedOut && !wasInstalled && action !== 'remove' && !isGlobalNpm && task?.status !== 'cancelled') {
@@ -608,6 +615,14 @@ function spawnMutation(options) {
608
615
  pushLine(task, '[timeout] the install had already completed — treating it as a success');
609
616
  }
610
617
  }
618
+ // 重复卸载:依赖已经不在 profile 里,pnpm 拒绝移除并报「没有这个依赖」。
619
+ // 与卸载成功同义(用户要的结果已达成),按成功收尾,避免误报失败、误提 Issue。
620
+ if (!result.timedOut && task?.status !== 'cancelled'
621
+ && isAlreadyRemoved(action, `${result.stderr}\n${result.stdout}`)) {
622
+ result = { ...result, exitCode: 0, error: null };
623
+ if (task)
624
+ pushLine(task, '[already removed] the package is no longer in the profile — treating it as uninstalled');
625
+ }
611
626
  // 统一终态后处理:exitCode/status/needsRestart 以最后一次尝试为准
612
627
  if (task) {
613
628
  task.exitCode = result.exitCode;
@@ -634,7 +649,7 @@ function spawnMutation(options) {
634
649
  else if (action === 'remove' && loader) {
635
650
  // 卸载成功 → 主动从运行中 loader 停用该包条目:
636
651
  // - 停用成功:服务端即时卸载;带客户端 UI 的插件面板要页面刷新才摘除(无需重启宿主)
637
- // - 停用失败 / 超时:回退「待重启」兜底清理(刷新解决不了,登记常驻提醒)
652
+ // - 停用失败 / 超时:回退「待重启」提醒(刷新解决不了,登记常驻提醒)
638
653
  const removal = await removeLoadedEntry(loader, target);
639
654
  task.needsRestart = !removal.ok;
640
655
  task.needsReload = removal.ok && removal.live && task.hadClientUi === true;
@@ -644,10 +659,10 @@ function spawnMutation(options) {
644
659
  clearPendingRestart(displayTarget ?? target);
645
660
  }
646
661
  else if (action !== 'remove' && !isUpdate && loader) {
647
- // 安装成功 → 尝试把新插件热挂进运行中 loader,让插件无需重启宿主即生效。
662
+ // 安装成功 → 尝试把新插件热加载进运行中 loader,让插件无需重启宿主即生效。
648
663
  // 挂上了:服务端即时生效;带客户端 UI 的插件还需页面重新加载(宿主按请求现场
649
664
  // 拼装 __DSH_BOOT__ 注入清单,网页端与桌面端一致,刷新即可)。
650
- // 挂不上 / 非 dsh 插件 / 无法定位包名:回退「待重启」提醒(保证不静默失效)。
665
+ // 挂不上 / 非 dsh 插件 / 无法定位包名:回退「待重启」提醒(保证不会无声失效)。
651
666
  const { name } = verifyInstalledEntry(profile, target);
652
667
  if (name !== null && isDshPlugin(profile, name) && await mountLoadedEntry(loader, name)) {
653
668
  task.needsReload = hasClientUi(profile, name);
@@ -659,7 +674,7 @@ function spawnMutation(options) {
659
674
  }
660
675
  }
661
676
  else {
662
- // 更新(ESM 缓存下热挂取不到新代码)/ 无 loader 的安装或卸载 → 登记待重启:
677
+ // 更新(ESM 缓存下热加载取不到新代码)/ 无 loader 的安装或卸载 → 登记待重启:
663
678
  // 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
664
679
  task.needsRestart = true;
665
680
  addPendingRestart(displayTarget ?? target, action === 'remove' ? 'uninstall' : 'install');
@@ -671,7 +686,7 @@ function spawnMutation(options) {
671
686
  }
672
687
  }
673
688
  // 带上最终生效的源:后续注解(如 [dsh-too-old])读元数据必须与安装同源,
674
- // 否则换过源之后会去另一个源查,查不到就静默漏报
689
+ // 否则换过源之后会去另一个源查,查不到就漏报
675
690
  return activeRegistry === undefined ? result : { ...result, registry: activeRegistry };
676
691
  })();
677
692
  }
@@ -815,7 +830,7 @@ async function annotateDshTooOld(result, options) {
815
830
  if ((options.globalNpm ?? []).length > 0)
816
831
  return result;
817
832
  // 用安装**实际**使用的源读元数据:换源重试后候选表首位可能并不是真正生效的那个,
818
- // 用错源会读不到包而静默漏报;私有源/镜像里同样能读到包声明的 engines
833
+ // 用错源会读不到包而漏报;私有源/镜像里同样能读到包声明的 engines
819
834
  const line = await dshTooOldMarker(options.target, { registry: result.registry ?? options.registries?.[0] });
820
835
  if (line === null)
821
836
  return result;
@@ -836,14 +851,14 @@ export function hostSpawnMarker(invocation, error) {
836
851
  return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`;
837
852
  }
838
853
  /**
839
- * 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 门禁拦下构建脚本而失败 ——
854
+ * 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 限制拦下构建脚本而失败 ——
840
855
  * 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
841
856
  * key@url 提示),或是某依赖的原生构建(`ERR_PNPM_IGNORED_BUILDS`,key = 列出的 name@version)——
842
857
  * 就把确切的 key 写进 profile 的 pnpm-workspace.yaml 并重试一次。
843
858
  * 其它失败一律落到单次 spawn 的结果。
844
859
  */
845
860
  export async function runPluginMutation(options) {
846
- // operation 日志只在运行期存在(宿主结束后可能清掉),先开抓取,退出后按路径读作兜底
861
+ // operation 日志只在运行期存在(宿主结束后可能清掉),先开抓取,退出后按路径读取作为后备
847
862
  const profileDir = profileDirectory(options.profile);
848
863
  const logTap = createOperationLogTap(profileDir);
849
864
  let result = await spawnMutation(options);
@@ -3,8 +3,8 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * 运行中 loader 的读取、停用与热挂载:卸载成功后对匹配条目做 live-disable、
7
- * 安装成功后把新插件条目热挂进运行中的 loader,让两类操作都尽量立即生效
6
+ * 运行中 loader 的读取、停用与热加载:卸载成功后对匹配条目做 live-disable、
7
+ * 安装成功后把新插件条目热加载进运行中的 loader,让两类操作都尽量立即生效
8
8
  * (刷新页面即可,无需重启宿主)。
9
9
  */
10
10
  import { readFileSync } from 'node:fs';
@@ -61,7 +61,7 @@ function nameMatches(entryName, target) {
61
61
  if (e === tShort)
62
62
  return true;
63
63
  // 3) 反向变体:目标短名可能就是条目的完整形式(条目 `scope-widget`,
64
- // 目标短名 `widget` 是它的尾段)——用「条目名以短名结尾」收口,避免误伤。
64
+ // 目标短名 `widget` 是它的尾段)——以「条目名以短名结尾」作为匹配条件,避免误伤。
65
65
  if (tShort.length >= 2 && e.endsWith(`-${tShort}`))
66
66
  return true;
67
67
  // 4) 条目名是目标包内的模块路径(`aegis/extensions/dsh/index.js` 属于包 `aegis`,
@@ -82,13 +82,13 @@ function nameMatches(entryName, target) {
82
82
  * - `{ ok: true, live: false }`:宿主从未加载过它(磁盘已干净),无需刷新/重启;
83
83
  * - `{ ok: true, live: true }`:曾在 loader 中存活且已 live-disable —— 服务端即时卸载;
84
84
  * 带客户端 UI 的插件面板要页面刷新才摘除(由调用方按 `hadClientUi` 决定 needsReload);
85
- * - `{ ok: false, live: true }`:disable 失败 / 超时,需宿主重启兜底清理。
85
+ * - `{ ok: false, live: true }`:disable 失败 / 超时,需宿主重启后清理。
86
86
  * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
87
87
  */
88
88
  export async function removeLoadedEntry(loader, name) {
89
89
  if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/'))
90
90
  return { ok: false, live: true };
91
- // 5s 超时兜底:fiber dispose 卡住时不能让卸载任务(乃至整个队列)被拖死
91
+ // 5s 超时保护:fiber dispose 卡住时不能让卸载任务(乃至整个队列)被拖死
92
92
  const removal = (async () => {
93
93
  const entries = dumpLoaderEntries(loader);
94
94
  const match = entries.find((e) => e.name === name);
@@ -134,7 +134,7 @@ export async function removeLoadedEntry(loader, name) {
134
134
  return result;
135
135
  }
136
136
  /**
137
- * 安装成功后把新插件热挂进运行中 loader(等价 profile bundle patch 的
137
+ * 安装成功后把新插件热加载进运行中 loader(等价 profile bundle patch 的
138
138
  * `- insert: - name: <pkg>` 行),让插件无需重启宿主即可生效。
139
139
  * 做法:在 Loader 根组新建条目并启动 —— Loader.write() 为空实现,不写盘;
140
140
  * 重启后由 profile 的 bundles 清单重新挂载,不会重复。
@@ -155,7 +155,7 @@ export async function mountLoadedEntry(loader, name) {
155
155
  console.log(`[hub-install] hot mount ${name}: entry ${id} created but not running (import failed?)`);
156
156
  return false;
157
157
  })();
158
- // 10s 超时兜底:import 卡住时不能让装完收尾(乃至整个队列)被拖死
158
+ // 10s 超时保护:import 卡住时不能让装完收尾(乃至整个队列)被拖死
159
159
  return Promise.race([
160
160
  mounting.catch((error) => {
161
161
  console.log(`[hub-install] hot mount ${name} failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -166,7 +166,7 @@ export async function mountLoadedEntry(loader, name) {
166
166
  }
167
167
  /**
168
168
  * 已安装包是否带客户端 UI(package.json 的 `dsh.client` 声明):client-modules 据它把
169
- * 客户端 bundle 注入宿主页面。带 UI 的插件热挂载后需要页面重新加载才会渲染,
169
+ * 客户端 bundle 注入宿主页面。带 UI 的插件热加载后需要页面重新加载才会渲染,
170
170
  * 网页端与桌面端一致 —— `__DSH_BOOT__` 注入清单由宿主服务端按请求现场拼装,
171
171
  * 壳不缓存,所以桌面端刷新页面同样能拿到新清单,无需重开 App。
172
172
  */
@@ -3,15 +3,15 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * npm 包最新版本的发布时间查询:为「已安装插件是否有更新」提供供应链门禁依据。
7
- * pnpm 对发布不满 24h(dsh profile minimumReleaseAge=1440)的版本会静默回退到旧版本,
8
- * 因此发布未满门槛的版本不该提示更新(否则用户点了也装不上)。
6
+ * npm 包最新版本的发布时间查询:为「已安装插件是否有更新」提供发布冷却期依据。
7
+ * pnpm 对发布不满 24h(dsh profile minimumReleaseAge=1440)的版本会不提示地回退到旧版本,
8
+ * 因此发布还不满 24 小时的版本不该提示更新(否则用户点了也装不上)。
9
9
  * 结果按包名内存缓存,同一次会话内不重复请求;失败/超时/响应过大一律返回 null,绝不抛错、绝不阻塞。
10
10
  */
11
11
  import { get as httpsGet } from 'node:https';
12
12
  import { validPackageName } from './profile/profile.js';
13
13
  import { fetchViaCurl } from './probe.js';
14
- /** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同口径。 */
14
+ /** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同一套规则。 */
15
15
  export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000;
16
16
  /** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
17
17
  export const MAX_RELEASE_AGE_PACKAGES = 50;
@@ -22,7 +22,7 @@ const MAX_PACKUMENT_BYTES = 10 * 1024 * 1024;
22
22
  export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
23
23
  /** 会话内缓存(源 + 包名 → 结果):只存成功结果,一次网络抖动不会把判断永久粘住。
24
24
  * 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
25
- * 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错门禁。 */
25
+ * 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错冷却期。 */
26
26
  const CACHE_TTL_MS = 10 * 60 * 1000;
27
27
  const releaseCache = new Map();
28
28
  /** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
@@ -41,7 +41,7 @@ export function latestReleaseOf(packument) {
41
41
  return { version, publishedAt };
42
42
  }
43
43
  /**
44
- * 已过供应链门禁的最高稳定版本:安装时钉住它,pnpm 就不会把最新版静默回退成很旧的版本。
44
+ * 已过发布冷却期的最高稳定版本:安装锁定它,pnpm 不会回退到旧版本。
45
45
  * 只看纯 x.y.z(预发布版不参与,避免把用户带到不兼容预览版);时间表缺失或无满足项返回 null。
46
46
  */
47
47
  export function installableReleaseOf(packument, now = Date.now()) {
@@ -126,7 +126,7 @@ function getJson(url, timeoutMs) {
126
126
  req.on('error', () => finish(null));
127
127
  });
128
128
  }
129
- /** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理口径(curl 子进程注入 proxy env)。 */
129
+ /** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理规则(curl 子进程注入 proxy env)。 */
130
130
  async function fetchPackument(url, proxy, timeoutMs) {
131
131
  if (proxy !== '') {
132
132
  const r = await fetchViaCurl(url, proxy, timeoutMs);
@@ -134,7 +134,7 @@ async function fetchPackument(url, proxy, timeoutMs) {
134
134
  }
135
135
  return getJson(url, timeoutMs);
136
136
  }
137
- /** 查询一个包的最新版本、发布时间与可钉住的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
137
+ /** 查询一个包的最新版本、发布时间与可锁定的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
138
138
  export async function lookupReleaseAge(packageName, registry, proxy, timeoutMs = RELEASE_AGE_TIMEOUT_MS) {
139
139
  const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY;
140
140
  const key = `${base}\n${packageName}`;