dsh-plugin 2.3.1 → 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 +15 -21
  2. package/README.md +15 -21
  3. package/client/client.js +412 -382
  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 +21 -10
  8. package/lib/http/npm-release-age.js +9 -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 +46 -9
  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 +14 -2
  32. package/lib/types/services/probe.d.ts +4 -4
  33. package/package.json +1 -1
  34. package/src/client/components/PluginHubSection.tsx +59 -28
  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 +13 -11
  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 +3 -1
  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 +31 -24
  53. package/src/client/locales.ts +21 -21
  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 +5 -3
  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 +20 -10
  69. package/src/server/http/npm-release-age.ts +9 -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 +50 -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
@@ -3,15 +3,15 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * 供应链门禁的时间判定(纯函数):pnpm 会静默回退发布未满门槛的版本,
7
- * 故发布太新的版本不该提示更新。Hub 自身更新与插件更新共用同一门槛。
6
+ * 发布冷却期的时间判定(纯函数):pnpm 会不提示地回退发布还不满 24 小时的版本,
7
+ * 故发布太新的版本不该提示更新。Hub 自身更新与插件更新共用同一规则。
8
8
  */
9
9
  import type { HubUpdateInfo, HubUpdateState, HubUpdateVersion } from '../types.ts'
10
10
 
11
- /** pnpm 供应链门禁:dsh profile 默认 minimumReleaseAge=1440(分钟);+30 分钟缓冲抵消网络/CDN 边缘时间偏差。 */
11
+ /** pnpm 发布冷却期:dsh profile 默认 minimumReleaseAge=1440(分钟);+30 分钟缓冲抵消网络/CDN 边缘时间偏差。 */
12
12
  export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
13
13
 
14
- /** 发布时间是否仍在门禁窗口内(太新 → 暂缓提示);无值/解析不出返回 false(不拦真实更新)。 */
14
+ /** 发布时间是否仍在冷却期内(太新 → 暂缓提示);无值/解析不出返回 false(不拦真实更新)。 */
15
15
  export function isReleaseTooFresh(publishedAt: string | null | undefined, now: number): boolean {
16
16
  if (typeof publishedAt !== 'string' || publishedAt === '') return false
17
17
  const ms = Date.parse(publishedAt)
@@ -61,20 +61,20 @@ export function compareVersions(a: string | null | undefined, b: string | null |
61
61
  return 0
62
62
  }
63
63
 
64
- /** 「可更新」判定结果:是否提示,以及被供应链门禁拦下时的到期时刻。 */
64
+ /** 「可更新」判定结果:是否提示,以及还没过冷却期时的可重试时刻。 */
65
65
  export interface UpdateDecision {
66
66
  offered: boolean
67
- /** 发布未满门槛 → 可重试时刻(毫秒);非门禁原因(npm 上没有更高版本)→ null */
67
+ /** 发布还不满冷却期 → 可重试时刻(毫秒);非冷却期原因(npm 上没有更高版本)→ null */
68
68
  pendingUntil: number | null
69
69
  }
70
70
 
71
71
  /**
72
- * 是否值得提示「可更新」:npm 上最新可安装版本确实比已装版本新,且已过供应链门禁。
72
+ * 是否值得提示「可更新」:npm 上最新可安装版本确实比已装版本新,且已过发布冷却期。
73
73
  *
74
74
  * 更新命令执行的是 `pkg@latest`,所以「目录里标的版本」不是最终能装上的版本:
75
75
  * 作者可能只打了 GitHub release 没发 npm —— 此时提示会变成永远点不掉的循环。
76
76
  * 任一信息缺失(查不到发布信息 / 没有已装版本记录)返回 offered,交回调用方原有判定:
77
- * 宁可提示,也不因为一次查询失败把真实更新吃掉。
77
+ * 宁可提示,也不因为一次查询失败漏掉真实更新。
78
78
  */
79
79
  export function updateDecision(
80
80
  release: { version: string; publishedAt: string } | null,
@@ -104,8 +104,8 @@ export function isUpdateOffered(
104
104
  /**
105
105
  * 选出本次真正该提示的更新版本。
106
106
  *
107
- * 只看最新版会漏更新:连续发版时间隔小于供应链门禁时,最新版永远「太新」,
108
- * 提示被整体跳过。这里把最新版与 recentVersions 一起当候选,取「高于当前版本且已过门禁」
107
+ * 只看最新版会漏更新:连续发版时间隔小于发布冷却期时,最新版永远「太新」,
108
+ * 提示被整体跳过。这里把最新版与 recentVersions 一起当候选,取「高于当前版本且已过冷却期」
109
109
  * 的最高版本;一个都没有但仍存在更高版本 → pending(暂缓,不谎称已是最新)。
110
110
  */
111
111
  export function pickHubUpdate(info: HubUpdateInfo, currentVersion: string, now: number): { state: HubUpdateState; target: HubUpdateVersion | null } {
@@ -228,7 +228,7 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
228
228
  * 错误摘要,供插件中心维护者分诊 —— 不再把环境问题/插件中心自身问题误投给插件作者。
229
229
  */
230
230
  export type HubIssueVariant =
231
- /** 缺省:安装器主动把失败送到插件中心分诊(未分类兜底) */
231
+ /** 缺省:安装器主动把失败送到插件中心分诊(未分类时的默认去向) */
232
232
  | 'triage'
233
233
  /** 从判定由插件中心自己产出的分支(插件打包问题 / allowBuilds 拦截)的次要入口提交:用户怀疑是误报 */
234
234
  | 'misjudge'
@@ -249,7 +249,7 @@ export function hubIssueUrl(
249
249
  const zhWord = actionWordZh(action)
250
250
  const repo = pluginRepo && pluginRepo.trim() !== '' ? pluginRepo.trim() : 'unknown'
251
251
  const title = variant === 'misjudge'
252
- // 标题就点明「可能是误判」,维护者一眼能区分它和未分类兜底
252
+ // 标题就点明「可能是误判」,维护者一眼能区分它和未分类时的默认去向
253
253
  ? `[dsh-plugin-hub] possible misjudgment: ${repo}`
254
254
  : `[dsh-plugin-hub] ${reasonTitleOf(kind, action)}: ${repo}`
255
255
  const code = coreErrorCode(message)
@@ -264,7 +264,7 @@ export function hubIssueUrl(
264
264
  ...(variant === 'misjudge'
265
265
  // 这些分支的判据由插件中心自己产出、或掺了我们的动作(自动放行 allowBuilds)——
266
266
  // 一旦我们自己误报,用户就被送去作者仓库、我们永远收不到反馈。
267
- // 正文写清它来自哪条分支、判据是谁给的,维护者才不会把它当成未分类兜底。
267
+ // 正文写清它来自哪条分支、判据是谁给的,维护者才不会把它当成未分类时的默认去向。
268
268
  ? [
269
269
  '- Filed from: a branch that normally routes to the plugin repo — filed here because that verdict may be our own misjudgment.',
270
270
  '- Source of that verdict: produced by dsh-plugin-hub\'s own check or its auto-allow retry, not by plain pnpm output.',
@@ -216,7 +216,7 @@
216
216
  line-height: 18px;
217
217
  color: var(--hub-text-tertiary);
218
218
  margin: 0;
219
- /* 品牌副标题强制单行:中文/英文都不换行,超宽时省略号截断兜底 */
219
+ /* 品牌副标题强制单行:中文/英文都不换行,超宽时用省略号截断 */
220
220
  white-space: nowrap;
221
221
  overflow: hidden;
222
222
  text-overflow: ellipsis;
@@ -876,7 +876,7 @@
876
876
 
877
877
  /* —— 安装失败记录 —— */
878
878
 
879
- /* 失败记录列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height 兜底滚动 */
879
+ /* 失败记录列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height 限制滚动 */
880
880
  .failList {
881
881
  display: flex;
882
882
  flex-direction: column;
@@ -904,7 +904,7 @@
904
904
 
905
905
  /* —— 任务通知中心(成功 + 失败) —— */
906
906
 
907
- /* 通知列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height 兜底滚动 */
907
+ /* 通知列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height 限制滚动 */
908
908
  .noticeList {
909
909
  display: flex;
910
910
  flex-direction: column;
@@ -105,7 +105,7 @@
105
105
 
106
106
  /* —— 通知中心入口 —— */
107
107
 
108
- /* 通知铃铛按钮:位于「设置」tab 后边、靠右对齐(margin-left:auto 吃掉 tab 剩余空间),
108
+ /* 通知铃铛按钮:位于「设置」tab 后边、靠右对齐(margin-left:auto 占满 tab 剩余空间),
109
109
  在 38px tab 栏里垂直居中;常态透明底只显铃铛轮廓,hover 浅灰,不抢 tab 视觉层级 */
110
110
  .noticeBtn {
111
111
  position: relative;
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * 插件中心设计变量。
6
6
  *
7
- * 对宿主 --dsw-alias-* 主题变量的语义别名,各带原始浅色字面量,供未定义底层变量的宿主兜底。
7
+ * 对宿主 --dsw-alias-* 主题变量的语义别名,各带原始浅色字面量,供未定义底层变量的宿主使用。
8
8
  * 声明在 :root 与暗色 body 上,让每种主题都按自己的变量集重新解析别名,再由所有板块元素继承。
9
9
  *
10
10
  * 各区域样式表不要写颜色字面量:在此引用 --hub-*,新样式即可自动跟随主题。 */
@@ -110,8 +110,8 @@ export interface HubUpdateInfo {
110
110
  notes?: string | Record<string, string> | null
111
111
  /**
112
112
  * 最近若干发版(含最新版),按版本号降序无关、由客户端自行排序。
113
- * 用途:连续快速发版时,最新版往往还在 pnpm 的 24h 供应链门禁内;
114
- * 客户端据此回退到「更早、但已过门禁」的版本提示更新,而不是整体跳过。
113
+ * 用途:连续快速发版时,最新版往往还在 pnpm 的 24h 发布冷却期内;
114
+ * 客户端据此回退到「更早、但已过冷却期」的版本提示更新,而不是整体跳过。
115
115
  */
116
116
  recentVersions?: HubUpdateVersion[]
117
117
  }
@@ -126,8 +126,8 @@ export interface HubUpdateVersion {
126
126
  /**
127
127
  * Hub 自我更新状态 —— 供版本弹窗选文案与按钮,四态互斥:
128
128
  * - `none` 未取到接口中心信息(拉取失败)→ 只说当前版本,不提示更新
129
- * - `available` 远端有更新且已过供应链安全门槛 → 提示更新
130
- * - `pending` 远端有更新但发布未满门槛(见 useCatalog)→ 暂缓提示并说明原因
129
+ * - `available` 远端有更新且已过发布冷却期 → 提示更新
130
+ * - `pending` 远端有更新但发布还不满冷却期(见 useCatalog)→ 暂缓提示并说明原因
131
131
  * - `latest` 与远端最新版一致 → 确已最新
132
132
  * 「有新版但暂缓提示」必须与「确已最新」分开:两者若共用文案,弹窗会在本机版本落后时
133
133
  * 谎称「你正在使用 v{远端最新版},已是最新」。
@@ -42,7 +42,7 @@ function fetchCatalogOnce(target: string, proxy: string): ReturnType<typeof fetc
42
42
  }
43
43
 
44
44
  /** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
45
- * maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时兜底使用。 */
45
+ * maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时使用。 */
46
46
  function readCatalogCache(file: string, maxAgeMs: number): string | null {
47
47
  try {
48
48
  const entry = JSON.parse(readFileSync(file, 'utf8')) as CatalogCacheEntry
@@ -56,7 +56,7 @@ function readCatalogCache(file: string, maxAgeMs: number): string | null {
56
56
  return null
57
57
  }
58
58
 
59
- /** 写缓存:目录不存在则创建;写失败静默忽略(缓存只是加速,不影响功能)。 */
59
+ /** 写缓存:目录不存在则创建;写失败直接忽略(缓存只是加速,不影响功能)。 */
60
60
  function writeCatalogCache(file: string, body: string): void {
61
61
  try {
62
62
  mkdirSync(dirname(file), { recursive: true })
@@ -108,7 +108,7 @@ export function registerCatalogRoutes(webServer: WebServerService, profile: stri
108
108
  handler: async (request, response) => {
109
109
  if (!requireMethod(request, response, 'GET')) return
110
110
  // 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
111
- // 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理口径,
111
+ // 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理规则,
112
112
  // 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
113
113
  const url = new URL(request.url ?? '/', 'http://localhost')
114
114
  const settings = loadSettings(profile)
@@ -145,7 +145,7 @@ export function registerCatalogRoutes(webServer: WebServerService, profile: stri
145
145
  sendJson(response, 200, JSON.parse(stale))
146
146
  return
147
147
  } catch {
148
- // 兜底缓存也损坏:落到下面的 502
148
+ // 备用缓存也损坏:落到下面的 502
149
149
  }
150
150
  }
151
151
  // 带上 curl 退出码与 stderr 末行:只回 error 字段时,用户贴出来的响应里
@@ -14,7 +14,7 @@ import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.ts'
14
14
  import { appendLog } from '../services/log.ts'
15
15
  import { readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
16
16
 
17
- /** 本机实际生效的 npm registry:诊断与安装同口径 —— 设置里留空时读用户 npm 配置
17
+ /** 本机实际生效的 npm registry:诊断与安装同一套规则 —— 设置里留空时读用户 npm 配置
18
18
  * (含 profile 目录的 .npmrc),而不是硬探官方源,避免「能装但诊断说源不通」。
19
19
  * 读不到(npm 缺失/超时/输出异常)时回退官方源。 */
20
20
  function configuredNpmRegistry(profile: string): string {
@@ -75,7 +75,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
75
75
  }
76
76
  const settings = loadSettings(profile)
77
77
  // npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
78
- // 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同口径。
78
+ // 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同一套规则。
79
79
  const registry = settings.npmRegistry.replace(/\/+$/, '') || configuredNpmRegistry(profile)
80
80
  // 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub 行探小仓库、
81
81
  // 目录站按 Range 取 1KB(探市场真实下载的 plugins.zh.json,但不整份下载)。
@@ -91,7 +91,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
91
91
  { 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' },
92
92
  ]
93
93
  // 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
94
- // 验证「代理能不能把请求带出去」。与安装同口径:curl 子进程注入该代理 env。
94
+ // 验证「代理能不能把请求带出去」。与安装同一套规则:curl 子进程注入该代理 env。
95
95
  // 设置里未配置代理时不显示该行(跟随系统代理/直连,无「代理本身」可验证)。
96
96
  if (settings.proxy !== '') {
97
97
  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 })
@@ -114,7 +114,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
114
114
  const results: Array<{ key: string; ok: boolean }> = []
115
115
  for (const c of checks) {
116
116
  send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display })
117
- // git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时兜底
117
+ // git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时保护
118
118
  const r = c.git
119
119
  ? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
120
120
  : await probe(c.url, c.proxy, c.range)
@@ -12,11 +12,12 @@ import { isDshNpmPackageForRepo } from '../services/install/npm-resolve.ts'
12
12
  import { probeUrl, systemProxy } from '../services/probe.ts'
13
13
  import { loadSettings, type HubSettings } from '../services/settings.ts'
14
14
  import { registryCandidates, registryHost, userNpmRegistry } from '../services/install/registry-fallback.ts'
15
+ import { lookupReleaseAge } from '../services/npm-release-age.ts'
15
16
  import { profileDirectory } from '../services/profile/profile.ts'
16
17
  import { readInstalled } from './catalog.ts'
17
18
  import { COMMAND_TIMEOUT_MS, readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
18
19
 
19
- /** 安装/卸载 CLI 的子进程环境:透传宿主环境并注入 HTTP(S) 代理,
20
+ /** 安装/卸载 CLI 的子进程环境:原样传递宿主环境并注入 HTTP(S) 代理,
20
21
  * 代理来源与诊断一致(设置代理 → 系统代理 → 宿主 env);registry 由任务队列按候选源逐次注入。 */
21
22
  function mutationEnv(settings: HubSettings): NodeJS.ProcessEnv {
22
23
  const env: NodeJS.ProcessEnv = { ...process.env, CI: 'true' }
@@ -66,11 +67,11 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
66
67
  const lang = typeof body === 'object' && body !== null && (body as { lang?: unknown }).lang === 'en'
67
68
  ? 'en'
68
69
  : 'zh'
69
- // 代理口径与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
70
+ // 代理规则与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
70
71
  const effectiveProxy = settings.proxy !== ''
71
72
  ? settings.proxy
72
73
  : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
73
- // 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm 配置(与诊断同口径)。
74
+ // 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm 配置(与诊断同一套规则)。
74
75
  // 只有公共源允许失败后换源;自定义/内网源保持单一候选,避免私有包名泄露给公共源。
75
76
  const registries = registryCandidates(
76
77
  settings.npmRegistry,
@@ -78,7 +79,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
78
79
  settings.registryFallback,
79
80
  )
80
81
  // 全局 npm 安装(`npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
81
- // 包列表优先取 body.globalNpm,兜底从 rawRepo 解析,保证直接调 API 传完整命令也能识别。
82
+ // 包列表优先取 body.globalNpm,取不到时再从 rawRepo 解析,保证直接调 API 传完整命令也能识别。
82
83
  const bodyGlobalNpm = Array.isArray((body as { globalNpm?: unknown }).globalNpm)
83
84
  ? (body as { globalNpm: unknown[] }).globalNpm.filter((x): x is string => typeof x === 'string' && x !== '')
84
85
  : []
@@ -89,7 +90,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
89
90
  sendJson(response, 400, { error: 'unsupported install target' })
90
91
  return
91
92
  }
92
- // 通道门禁与安装通道同构:全局安装本质是 npm 操作,吃「启用 NPM 安装」开关
93
+ // 通道校验与安装通道同构:全局安装本质是 npm 操作,吃「启用 NPM 安装」开关
93
94
  if (source !== 'catalog' && !settings.enableNpmInstall) {
94
95
  sendJson(response, 403, { error: 'npm installs are disabled by the security settings' })
95
96
  return
@@ -122,8 +123,8 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
122
123
  return
123
124
  }
124
125
  const target = rawRepo
125
- // 钉住精确版本(Hub 自我更新):最新版可能仍在 pnpm 供应链门禁内,
126
- // 客户端提示的是「已过门禁」的版本,服务端必须按该版本安装而不是 @latest。
126
+ // 锁定精确版本(Hub 自我更新):最新版可能仍在 pnpm 发布冷却期内,
127
+ // 客户端提示的是「已过冷却期」的版本,服务端必须按该版本安装而不是 @latest。
127
128
  const pinVersion = typeof (body as { version?: unknown }).version === 'string'
128
129
  ? (body as { version: string }).version.trim()
129
130
  : ''
@@ -181,6 +182,15 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
181
182
  }
182
183
  ordered = [chosen, ...registries.filter((r) => r !== chosen)]
183
184
  }
185
+ // 新安装锁定「已过发布冷却期」的最高稳定版:pnpm 对发布还不满 24 小时的版本会不提示地回退,
186
+ // 锁定后装到的就是预期版本。更新已有目标时客户端已按冷却期筛选,继续走 @latest。
187
+ let installSpec = pinVersion !== '' ? `${target}@${pinVersion}` : undefined
188
+ if (installSpec === undefined && already === null) {
189
+ // 解析源与安装首选源一致,锁定的版本一定存在于真正会用的那个源;解析失败则不锁定,不阻断安装。
190
+ // 超时 5s:网络刚探测通过,卡住时也不能把安装请求拖到默认的 8s
191
+ const release = await lookupReleaseAge(target, ordered[0] ?? '', effectiveProxy, 5000)
192
+ if (release !== null && release.installable !== null) installSpec = `${target}@${release.installable.version}`
193
+ }
184
194
  // 入队前复检:网络探测的 await 期间并发请求可能已通过上方检查,而此处到 startPluginMutation
185
195
  // 之间无 await,JS 单线程保证同一目标至多一个任务,杜绝双击/并发双任务。
186
196
  if (hasQueuedTarget(target)) {
@@ -190,7 +200,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
190
200
  // 后台启动 CLI;客户端轮询 /status 获取进度
191
201
  const task = startPluginMutation({
192
202
  // mode=update(已安装目标的覆盖更新)以 update 动作入队:
193
- // 日志按 update 类别落盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
203
+ // 日志按 update 类别写入磁盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
194
204
  action: mode === 'update' ? 'update' : 'add',
195
205
  profile,
196
206
  target,
@@ -198,13 +208,13 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
198
208
  displayTarget: githubRepoOf(displayRepo) ?? undefined,
199
209
  // 覆盖更新:npm 命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
200
210
  updateNpm: mode === 'update',
201
- // 运行中 loader:安装成功后把新插件热挂进 loader,非桌面端 + 纯服务端插件无需重启即生效
211
+ // 运行中 loader:安装成功后把新插件热加载进 loader,非桌面端 + 纯服务端插件无需重启即生效
202
212
  loader,
203
213
  timeoutMs: COMMAND_TIMEOUT_MS,
204
214
  env: mutationEnv(settings),
205
215
  // 候选源(首选在前):任务内遇到传输层失败时按顺序换源重试
206
216
  registries: ordered,
207
- installSpec: pinVersion !== '' ? `${target}@${pinVersion}` : undefined,
217
+ installSpec,
208
218
  // 自定义安装入口渠道:日志/报错据此溯源
209
219
  installChannel,
210
220
  })
@@ -4,10 +4,12 @@
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.ts'
10
10
  import { lookupReleaseAge, parseReleaseAgeRequest } from '../services/npm-release-age.ts'
11
+ import { userNpmRegistry } from '../services/install/registry-fallback.ts'
12
+ import { profileDirectory } from '../services/profile/profile.ts'
11
13
  import { loadSettings } from '../services/settings.ts'
12
14
  import { readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
13
15
 
@@ -25,13 +27,17 @@ export function registerNpmReleaseAgeRoutes(webServer: WebServerService, profile
25
27
  return
26
28
  }
27
29
  const settings = loadSettings(profile)
28
- // 代理口径与安装/目录一致:设置里的代理 → 系统代理 → 环境变量 → 直连
30
+ // 代理规则与安装/目录一致:设置里的代理 → 系统代理 → 环境变量 → 直连
29
31
  const proxy = settings.proxy !== ''
30
32
  ? settings.proxy
31
33
  : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
32
34
  // 并发查询:单包各自带超时且失败即 null,整体不因任一包失败而失败
35
+ // 查询源与安装首选源同一套规则(设置 → 本机 npm 配置):安装锁定的版本必须来自真正要用的那个源
36
+ const registry = settings.npmRegistry !== ''
37
+ ? settings.npmRegistry
38
+ : userNpmRegistry(profileDirectory(profile))
33
39
  const results = await Promise.all(packages.map(async (name) =>
34
- [name, await lookupReleaseAge(name, settings.npmRegistry, proxy)] as const))
40
+ [name, await lookupReleaseAge(name, registry, proxy)] as const))
35
41
  sendJson(response, 200, { packages: Object.fromEntries(results) })
36
42
  } catch (error) {
37
43
  sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
@@ -42,7 +42,7 @@ function findPids() {
42
42
 
43
43
  const targets = findPids()
44
44
  // 一个监听进程都没找到 = 端口探测本身就失败了(netstat 输出格式 / 本地化差异),
45
- // 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 静默退出,
45
+ // 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 无提示地退出,
46
46
  // 用户却看到「重启完成」——不制造这种假象,留一行可诊断的记录后退出。
47
47
  if (targets.length === 0) {
48
48
  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'
@@ -39,7 +39,7 @@ export function toolVersion(cmd: string, args: string[]): string | null {
39
39
  let cachedDshVersion: string | null = null
40
40
 
41
41
  /** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
42
- * (dsh 不在 PATH 时兜底)。探测成功即缓存,失败不缓存以便下次重试。 */
42
+ * (dsh 不在 PATH 时使用)。探测成功即缓存,失败不缓存以便下次重试。 */
43
43
  export function hostDshVersion(): string | null {
44
44
  if (cachedDshVersion !== null) return cachedDshVersion
45
45
  const viaCli = toolVersion('dsh', ['--version'])
@@ -9,7 +9,7 @@
9
9
  * 不再把这类失败归成「未分类」丢给用户或插件作者。
10
10
  *
11
11
  * 只在失败路径调用:peer 不满足本身通常只是警告,成功安装时不该出现这个提示;
12
- * 任何网络/解析异常都静默返回 null,不改变原有失败处理。
12
+ * 任何网络/解析异常都悄悄返回 null,不改变原有失败处理。
13
13
  */
14
14
  import { hostDshVersion } from '../host-version.ts'
15
15
 
@@ -36,14 +36,14 @@ export interface InstallTask {
36
36
  status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled'
37
37
  timedOut: boolean
38
38
  exitCode: number | null
39
- /** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出,阶段行兜底 */
39
+ /** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出;没有进度行时按阶段行估算 */
40
40
  progress: number
41
41
  /** 输出行最新在前(消费方展示尾部)。 */
42
42
  lines: string[]
43
- /** 完成后是否仍需宿主重启才生效:仅「热挂载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
44
- * 无 loader」等刷新解决不了的兜底场景为 true(弹窗给「立即重启」);其余一律 false。 */
43
+ /** 完成后是否仍需宿主重启才生效:仅「热加载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
44
+ * 无 loader」等刷新解决不了的例外场景为 true(弹窗给「立即重启」);其余一律 false。 */
45
45
  needsRestart: boolean
46
- /** 完成结果是否只需刷新页面即生效(插件已热挂进运行中 loader,仅剩客户端 UI 待页面重新加载,
46
+ /** 完成结果是否只需刷新页面即生效(插件已热加载进运行中 loader,仅剩客户端 UI 待页面重新加载,
47
47
  * 网页端与桌面端一致):true 时关闭结果弹窗即自动刷新页面,不给手动刷新按钮。 */
48
48
  needsReload?: boolean
49
49
  /** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
@@ -79,7 +79,7 @@ export interface QueueItem {
79
79
  installChannel?: 'npm'
80
80
  /** npm 源候选(首选在前):传输层失败时按顺序换源重试 */
81
81
  registries?: string[]
82
- /** 精确安装 spec(Hub 自我更新钉版本) */
82
+ /** 精确安装 spec(Hub 自我更新时锁定版本) */
83
83
  installSpec?: string
84
84
  }
85
85
  }
@@ -83,7 +83,7 @@ export function isDshNpmPackageForRepo(packageName: string, repo: string, regist
83
83
 
84
84
  /**
85
85
  * 解析仓库改名后的规范 `owner/repo`:GitHub 对已改名仓库返回 301,跟随跳转取最终地址。
86
- * 与其它 GitHub 访问同口径走 curl + 代理 env(Node 内置 https 读不到系统代理)。
86
+ * 与其它 GitHub 访问同一套规则走 curl + 代理 env(Node 内置 https 读不到系统代理)。
87
87
  * 未改名、仓库不存在、网络失败或 curl 不可用都返回 '',调用方按原名继续。
88
88
  */
89
89
  async function canonicalRepo(repo: string, proxy: string): Promise<string> {
@@ -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'
@@ -53,7 +53,7 @@ export function operationLogPathOf(output: string): string | null {
53
53
  /**
54
54
  * 只允许读 profile 目录下的 pnpm.log(dsh 自己的操作日志):
55
55
  * 绝对路径必须落在该目录内,相对路径按该目录解析。
56
- * 未提供 profileDir 时退回旧口径(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
56
+ * 未提供 profileDir 时退回旧规则(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
57
57
  */
58
58
  function allowedLogPath(raw: string, profileDir?: string): string | null {
59
59
  const path = resolve(profileDir ?? '', raw)
@@ -96,7 +96,7 @@ export function hasSubstantiveOutput(output: string): boolean {
96
96
  /**
97
97
  * 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
98
98
  * 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
99
- * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容兜底。
99
+ * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容补全。
100
100
  * 只认本次运行产生的新文件(mtime 晚于启动基线),避免复述上一次安装的日志。
101
101
  */
102
102
  export function createOperationLogTap(profileDir: string): { stop: () => string } {
@@ -10,7 +10,7 @@ import { spawnSync } from 'node:child_process'
10
10
  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)'
11
11
 
12
12
  /** 命令存在性探测:which/where 找 PATH 里的命令。
13
- * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
13
+ * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验处理。 */
14
14
  export function commandOnPath(cmd: string, env?: NodeJS.ProcessEnv): boolean {
15
15
  try {
16
16
  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
 
@@ -37,7 +37,7 @@ export function isPublicRegistry(value: string): boolean {
37
37
 
38
38
  /**
39
39
  * 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
40
- * 只有公共源才追加后备(最多 3 个);关闭降级或源不在白名单时只返回它自己。
40
+ * 只有公共源才追加后备(最多 3 个);关闭换源或源不在白名单时只返回它自己。
41
41
  */
42
42
  export function registryCandidates(configured: string, userRegistry: string, enabled: boolean): string[] {
43
43
  const base = normalizeRegistry(configured) || normalizeRegistry(userRegistry) || PUBLIC_REGISTRIES[0]!
@@ -6,7 +6,7 @@
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 代码,绝不执行其文本;跨仓跳转、外部主机、凭据、
@@ -21,7 +21,7 @@ export interface GithubReleaseTarget {
21
21
  repo: string
22
22
  }
23
23
 
24
- /** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同口径)。 */
24
+ /** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同一套规则)。 */
25
25
  const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i
26
26
 
27
27
  /** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */