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,14 +3,16 @@
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 判定同一套规则。 */
15
+ export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000;
14
16
  /** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
15
17
  export const MAX_RELEASE_AGE_PACKAGES = 50;
16
18
  /** 单包查询超时(ms):慢网络下失败即返回 null,不拖住整个响应。 */
@@ -20,7 +22,7 @@ const MAX_PACKUMENT_BYTES = 10 * 1024 * 1024;
20
22
  export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
21
23
  /** 会话内缓存(源 + 包名 → 结果):只存成功结果,一次网络抖动不会把判断永久粘住。
22
24
  * 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
23
- * 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错门禁。 */
25
+ * 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错冷却期。 */
24
26
  const CACHE_TTL_MS = 10 * 60 * 1000;
25
27
  const releaseCache = new Map();
26
28
  /** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
@@ -38,6 +40,39 @@ export function latestReleaseOf(packument) {
38
40
  return null;
39
41
  return { version, publishedAt };
40
42
  }
43
+ /**
44
+ * 已过发布冷却期的最高稳定版本:安装锁定它,pnpm 不会回退到旧版本。
45
+ * 只看纯 x.y.z(预发布版不参与,避免把用户带到不兼容预览版);时间表缺失或无满足项返回 null。
46
+ */
47
+ export function installableReleaseOf(packument, now = Date.now()) {
48
+ if (packument === null || typeof packument !== 'object')
49
+ return null;
50
+ const time = packument.time;
51
+ if (time === null || typeof time !== 'object')
52
+ return null;
53
+ let best = null;
54
+ for (const [version, published] of Object.entries(time)) {
55
+ if (!/^\d+\.\d+\.\d+$/.test(version) || typeof published !== 'string')
56
+ continue;
57
+ const at = Date.parse(published);
58
+ if (Number.isNaN(at) || now - at < MIN_RELEASE_AGE_MS)
59
+ continue;
60
+ if (best === null || compareVersion(version, best.version) > 0)
61
+ best = { version, publishedAt: published };
62
+ }
63
+ return best;
64
+ }
65
+ /** 纯数字段版本比较(installableReleaseOf 只在 x.y.z 里用)。 */
66
+ function compareVersion(a, b) {
67
+ const pa = a.split('.');
68
+ const pb = b.split('.');
69
+ for (let i = 0; i < 3; i++) {
70
+ const d = Number.parseInt(pa[i] ?? '0', 10) - Number.parseInt(pb[i] ?? '0', 10);
71
+ if (d !== 0)
72
+ return d;
73
+ }
74
+ return 0;
75
+ }
41
76
  /** 请求体 → 去重后的合法包名列表;非数组 / 超量 / 含非法包名一律 null(拒绝非法输入)。 */
42
77
  export function parseReleaseAgeRequest(body) {
43
78
  if (body === null || typeof body !== 'object')
@@ -91,7 +126,7 @@ function getJson(url, timeoutMs) {
91
126
  req.on('error', () => finish(null));
92
127
  });
93
128
  }
94
- /** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理口径(curl 子进程注入 proxy env)。 */
129
+ /** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理规则(curl 子进程注入 proxy env)。 */
95
130
  async function fetchPackument(url, proxy, timeoutMs) {
96
131
  if (proxy !== '') {
97
132
  const r = await fetchViaCurl(url, proxy, timeoutMs);
@@ -99,7 +134,7 @@ async function fetchPackument(url, proxy, timeoutMs) {
99
134
  }
100
135
  return getJson(url, timeoutMs);
101
136
  }
102
- /** 查询一个包的最新版本与发布时间(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
137
+ /** 查询一个包的最新版本、发布时间与可锁定的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
103
138
  export async function lookupReleaseAge(packageName, registry, proxy, timeoutMs = RELEASE_AGE_TIMEOUT_MS) {
104
139
  const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY;
105
140
  const key = `${base}\n${packageName}`;
@@ -116,8 +151,10 @@ export async function lookupReleaseAge(packageName, registry, proxy, timeoutMs =
116
151
  catch {
117
152
  return null;
118
153
  }
119
- const release = latestReleaseOf(parsed);
120
- if (release !== null)
121
- releaseCache.set(key, { at: Date.now(), release });
154
+ const latest = latestReleaseOf(parsed);
155
+ if (latest === null)
156
+ return null;
157
+ const release = { ...latest, installable: installableReleaseOf(parsed) };
158
+ releaseCache.set(key, { at: Date.now(), release });
122
159
  return release;
123
160
  }
@@ -30,13 +30,13 @@ export function curlTlsArgs() {
30
30
  export function curlRevokeFlagUnsupported(code, stderr) {
31
31
  return code === 2 && /ssl-revoke-best-effort/i.test(stderr) && /unknown|unrecognized/i.test(stderr);
32
32
  }
33
- /** 彻底关闭证书吊销检查的兜底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
33
+ /** 彻底关闭证书吊销检查的保底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
34
34
  const NO_REVOKE_ARG = '--ssl-no-revoke';
35
35
  /** stderr 是否指向 Schannel 取不到证书吊销信息(据此归因为环境 TLS 问题,而非网络不通)。 */
36
36
  export function tlsRevocationBlocked(stderr) {
37
37
  return /CRYPT_E_NO_REVOCATION_CHECK|CRYPT_E_REVOCATION_OFFLINE|0x80092012/i.test(stderr);
38
38
  }
39
- /** 探测/安装同一套代理口径:curl 与 git 子进程都读 HTTP(S)_PROXY 环境变量。 */
39
+ /** 探测/安装同一套代理规则:curl 与 git 子进程都读 HTTP(S)_PROXY 环境变量。 */
40
40
  function proxyEnv(proxy) {
41
41
  if (proxy === '')
42
42
  return { ...process.env };
@@ -132,11 +132,11 @@ function runCurl(args, env, timeoutMs) {
132
132
  child.stderr.on('data', (c) => { stderr += c.toString(); });
133
133
  child.on('error', () => finish(null));
134
134
  child.on('close', (code) => finish(code));
135
- // 超时兜底:curl --max-time 到点自行退出,此处再设一个进程级超时防止悬挂
135
+ // 超时保护:curl --max-time 到点自行退出,此处再设一个进程级超时防止悬挂
136
136
  const timer = setTimeout(() => { child.kill(); finish(null); }, timeoutMs + 1000);
137
137
  });
138
138
  }
139
- /** 带 TLS 兜底的 curl 调用:先按平台参数跑(Windows 带吊销检查降级),
139
+ /** 带 TLS 重试的 curl 调用:先按平台参数跑(Windows 带吊销检查降级),
140
140
  * 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败;
141
141
  * 降级后仍被吊销检查拦死(CRYPT_E_NO_REVOCATION_CHECK)时,再用 --ssl-no-revoke
142
142
  * 彻底关掉吊销检查跑一次 —— 该参数 curl ≥ 7.19 即存在,覆盖面比降级参数更广,
@@ -150,7 +150,7 @@ async function curlWithTlsFallback(args, env, timeoutMs) {
150
150
  }
151
151
  if (tlsArgs.length > 0 && tlsRevocationBlocked(run.stderr)) {
152
152
  const retry = await runCurl([NO_REVOKE_ARG, ...args], env, timeoutMs);
153
- // 只在兜底本身跑起来(非「不认识该参数」)时采用其结果,否则保留原始失败线索
153
+ // 只在重试本身跑起来(非「不认识该参数」)时采用其结果,否则保留原始失败线索
154
154
  if (retry.code !== null && !curlRevokeFlagUnsupported(retry.code, retry.stderr))
155
155
  run = retry;
156
156
  }
@@ -158,7 +158,7 @@ async function curlWithTlsFallback(args, env, timeoutMs) {
158
158
  }
159
159
  /**
160
160
  * curl 子进程探测:spawn curl -s -w '%{http_code}' 并注入代理 env。
161
- * curl 读 HTTP_PROXY/HTTPS_PROXY 是原生行为,与 pnpm/git 安装通道同一套代理口径;
161
+ * curl 读 HTTP_PROXY/HTTPS_PROXY 是原生行为,与 pnpm/git 安装通道同一套代理规则;
162
162
  * 失败(curl 不存在 / 连接失败 / 超时)都归为不可达,全程不抛错。
163
163
  * 证书吊销检查受阻(Windows Schannel)另记 reason,供诊断给出对症提示。
164
164
  */
@@ -219,7 +219,7 @@ export function gitLsRemote(url, proxy, timeoutMs) {
219
219
  const child = spawn('git', ['ls-remote', '--exit-code', url, 'HEAD'], { env });
220
220
  let out = '';
221
221
  child.stdout.on('data', (c) => { out += c.toString(); });
222
- // git 的报错不透传给终端:结果以退出码 + HEAD ref 是否返回为准
222
+ // git 的报错不原样传递给终端:结果以退出码 + HEAD ref 是否返回为准
223
223
  child.stderr.on('data', () => { });
224
224
  let done = false;
225
225
  const finish = (r) => {
@@ -235,14 +235,14 @@ export function gitLsRemote(url, proxy, timeoutMs) {
235
235
  // status 复用为 git 退出码(0 = 克隆握手成功);客户端 git 行不把它显示成 HTTP 码
236
236
  finish({ ok, ms: Date.now() - started, status: code });
237
237
  });
238
- // 超时兜底:与 curlProbe 同口径,防止 git 挂起
238
+ // 超时保护:与 curlProbe 同一套规则,防止 git 挂起
239
239
  const timer = setTimeout(() => { child.kill(); finish(probeFail()); }, timeoutMs + 1000);
240
240
  });
241
241
  }
242
242
  /**
243
243
  * 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
244
244
  * 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
245
- * 即规范地址。与其它 GitHub 访问同口径 —— curl 子进程注入代理 env(Node 内置
245
+ * 即规范地址。与其它 GitHub 访问同一套规则 —— curl 子进程注入代理 env(Node 内置
246
246
  * http(s) 读不到系统代理,直连在部分网络下不可达)。
247
247
  * 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
248
248
  */
@@ -257,11 +257,11 @@ export async function finalUrl(url, proxy, timeoutMs) {
257
257
  return r.code === 0 && final !== '' ? final : null;
258
258
  }
259
259
  /**
260
- * curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数兜底)。
260
+ * curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数重试)。
261
261
  * 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
262
- * 使「目录数据请求走设置里的代理」与 npm / git 安装通道口径一致。
262
+ * 使「目录数据请求走设置里的代理」与 npm / git 安装通道规则一致。
263
263
  * 失败(curl 不存在 / 连接失败 / 超时 / 非零退出)时 ok=false,全程不抛错;
264
- * reason 区分「证书吊销受阻」这类环境 TLS 问题,供调用方决定兜底策略。
264
+ * reason 区分「证书吊销受阻」这类环境 TLS 问题,供调用方决定备用策略。
265
265
  */
266
266
  export function fetchViaCurl(url, proxy, timeoutMs) {
267
267
  // 同样用 -sS:正文之外还要 stderr 里的 TLS 失败线索(被 stderr 收集,不外泄给用户)。
@@ -65,7 +65,7 @@ export function recordInstalledVersion(profile, repo, version, updatedAt) {
65
65
  export function recordResolvedNpmPackage(profile, repo, npmPackage) {
66
66
  const table = readInstalledVersions(profile);
67
67
  const key = repo.toLowerCase();
68
- // 显式写全字段:这一步发生在安装开始时,版本还不知道 —— 但落盘记录必须是完整形状,
68
+ // 显式写全字段:这一步发生在安装开始时,版本还不知道 —— 但写入磁盘记录必须是完整形状,
69
69
  // 否则只带 npmPackage 的残缺记录会在客户端版本比较时崩掉整块界面
70
70
  table[key] = {
71
71
  version: table[key]?.version ?? '',
@@ -85,7 +85,7 @@ function writeVersions(profile, table) {
85
85
  const file = versionsFilePath(profile);
86
86
  mkdirSync(dirname(file), { recursive: true });
87
87
  // 先写临时文件再原子替换:直接覆盖时进程被杀/磁盘满会留下截断的 JSON,
88
- // 下次读取整份账本都会失效(更新检测静默停摆),而用户看不到任何报错
88
+ // 下次读取整份账本都会失效(更新检测随之失效),而用户看不到任何报错
89
89
  const tmp = `${file}.tmp`;
90
90
  writeFileSync(tmp, JSON.stringify(table, null, 2) + '\n', 'utf8');
91
91
  renameSync(tmp, file);
@@ -77,7 +77,7 @@ export function githubTarget(repo, ref) {
77
77
  * 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 必填且只有长选项
78
78
  * (官方 CLI 无 -p 简写,省略即报错),支持 `--profile=<name>` 等号形式,命令大小写不敏感。
79
79
  * update = 更新已安装目标到最新;remove 不在此列(卸载走 /uninstall 路由,按已装包名直卸)。
80
- * 与客户端 CustomInstallView 口径一致。
80
+ * 与客户端 CustomInstallView 规则一致。
81
81
  */
82
82
  const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+(add|update)\s+(.+)$/i;
83
83
  export function installTargetOf(value) {
@@ -88,7 +88,7 @@ export function installTargetOf(value) {
88
88
  /**
89
89
  * npm 全局安装命令(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令)——
90
90
  * 提取 -g 之后的包列表。支持 `npm install|i` 与 `-g|--global`;任一包名非法(或格式
91
- * 不匹配)返回 null,由调用方按「不支持的目标」处理。与客户端 CustomInstallView 同口径。
91
+ * 不匹配)返回 null,由调用方按「不支持的目标」处理。与客户端 CustomInstallView 同一套规则。
92
92
  */
93
93
  const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i;
94
94
  export function globalNpmPackagesOf(value) {
@@ -6,5 +6,5 @@ export declare function versionOf(output: string): string | null;
6
6
  * Windows 上 pnpm/npm 是 .cmd shim,不走 shell 起不来;参数是固定的 `--version`,没有注入面。 */
7
7
  export declare function toolVersion(cmd: string, args: string[]): string | null;
8
8
  /** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
9
- * (dsh 不在 PATH 时兜底)。探测成功即缓存,失败不缓存以便下次重试。 */
9
+ * (dsh 不在 PATH 时使用)。探测成功即缓存,失败不缓存以便下次重试。 */
10
10
  export declare function hostDshVersion(): string | null;
@@ -34,14 +34,14 @@ export interface InstallTask {
34
34
  status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
35
35
  timedOut: boolean;
36
36
  exitCode: number | null;
37
- /** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出,阶段行兜底 */
37
+ /** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出;没有进度行时按阶段行估算 */
38
38
  progress: number;
39
39
  /** 输出行最新在前(消费方展示尾部)。 */
40
40
  lines: string[];
41
- /** 完成后是否仍需宿主重启才生效:仅「热挂载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
42
- * 无 loader」等刷新解决不了的兜底场景为 true(弹窗给「立即重启」);其余一律 false。 */
41
+ /** 完成后是否仍需宿主重启才生效:仅「热加载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
42
+ * 无 loader」等刷新解决不了的例外场景为 true(弹窗给「立即重启」);其余一律 false。 */
43
43
  needsRestart: boolean;
44
- /** 完成结果是否只需刷新页面即生效(插件已热挂进运行中 loader,仅剩客户端 UI 待页面重新加载,
44
+ /** 完成结果是否只需刷新页面即生效(插件已热加载进运行中 loader,仅剩客户端 UI 待页面重新加载,
45
45
  * 网页端与桌面端一致):true 时关闭结果弹窗即自动刷新页面,不给手动刷新按钮。 */
46
46
  needsReload?: boolean;
47
47
  /** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
@@ -75,7 +75,7 @@ export interface QueueItem {
75
75
  installChannel?: 'npm';
76
76
  /** npm 源候选(首选在前):传输层失败时按顺序换源重试 */
77
77
  registries?: string[];
78
- /** 精确安装 spec(Hub 自我更新钉版本) */
78
+ /** 精确安装 spec(Hub 自我更新时锁定版本) */
79
79
  installSpec?: string;
80
80
  };
81
81
  }
@@ -18,7 +18,7 @@ export declare function hasSubstantiveOutput(output: string): boolean;
18
18
  /**
19
19
  * 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
20
20
  * 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
21
- * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容兜底。
21
+ * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容补全。
22
22
  * 只认本次运行产生的新文件(mtime 晚于启动基线),避免复述上一次安装的日志。
23
23
  */
24
24
  export declare function createOperationLogTap(profileDir: string): {
@@ -1,7 +1,7 @@
1
1
  /** 缺 git 的失败标记:前端按 `[git-missing]` 归类并给「安装 git」指引(乱码免疫)。 */
2
2
  export declare 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)";
3
3
  /** 命令存在性探测:which/where 找 PATH 里的命令。
4
- * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
4
+ * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验处理。 */
5
5
  export declare function commandOnPath(cmd: string, env?: NodeJS.ProcessEnv): boolean;
6
6
  /** 该安装目标是否需要 git:github: / git+ / git@ 与 owner/repo 简写都要先 clone;
7
7
  * release 直链(https://…releases/download/…tgz)由 pnpm 直接下载,不需要 git。
@@ -4,7 +4,7 @@ export declare function normalizeRegistry(value: string): string;
4
4
  export declare function isPublicRegistry(value: string): boolean;
5
5
  /**
6
6
  * 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
7
- * 只有公共源才追加后备(最多 3 个);关闭降级或源不在白名单时只返回它自己。
7
+ * 只有公共源才追加后备(最多 3 个);关闭换源或源不在白名单时只返回它自己。
8
8
  */
9
9
  export declare function registryCandidates(configured: string, userRegistry: string, enabled: boolean): string[];
10
10
  /** 本机 npm 实际生效的源(含 profile 目录的 .npmrc);读不到返回空串。 */
@@ -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 代码,绝不执行其文本;跨仓跳转、外部主机、凭据、
@@ -39,7 +39,7 @@ export declare function cancelTask(id: number): boolean;
39
39
  */
40
40
  export declare function desktopCliBootstrap(entry: string): string | null;
41
41
  /**
42
- * 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
42
+ * 传给 CLI 的安装目标:锁定版本优先(Hub 自我更新提示的是已过发布冷却期的版本),
43
43
  * 其次 npm 的 add/update 都显式 `@latest`;git 源、全局安装、已带版本/tag 的 spec 保持原样。
44
44
  */
45
45
  export declare function npmInstallTargetOf(target: string, opts: {
@@ -48,6 +48,9 @@ export declare function npmInstallTargetOf(target: string, opts: {
48
48
  pinned?: string;
49
49
  add?: boolean;
50
50
  }): string;
51
+ /** 重复卸载:目标已不在 profile 依赖里,pnpm 报「没有这个依赖」。
52
+ * 结果与卸载成功等价 —— 按失败上报会弹出错误并自动提一条不存在的 Issue。 */
53
+ export declare function isAlreadyRemoved(action: 'add' | 'remove' | 'update', output: string): boolean;
51
54
  /**
52
55
  * 插件变更入队并返回其任务。任务严格串行:队列 worker 只在上一个结束后才启动下一个。
53
56
  * 完成前可通过 `getTask(id)` / `activeTask()` 查看进度。
@@ -62,7 +65,7 @@ export declare function startPluginMutation(options: {
62
65
  globalNpm?: string[];
63
66
  /** 待重启行的展示目标(owner/repo):卸载时用于把 npm 包名映射回仓库名 */
64
67
  displayTarget?: string;
65
- /** 运行中 loader:安装成功后热挂新条目、卸载成功后停用旧条目,两类操作都尽量立即生效
68
+ /** 运行中 loader:安装成功后热加载新条目、卸载成功后停用旧条目,两类操作都尽量立即生效
66
69
  * (缺失时安装/卸载仍需重启) */
67
70
  loader?: LoaderHandle;
68
71
  /** 入队前已尝试的安装方式:失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
@@ -72,10 +75,10 @@ export declare function startPluginMutation(options: {
72
75
  /** 自定义安装入口渠道(NPM 包卡片):任务输出行与系统日志据此溯源「从哪个入口发起」;
73
76
  * 目录插件安装不传 */
74
77
  installChannel?: 'npm';
75
- /** npm 源候选(首选在前):传输层失败时按顺序换源重试,空/单个表示不降级 */
78
+ /** npm 源候选(首选在前):传输层失败时按顺序换源重试,空/单个表示不换源 */
76
79
  registries?: string[];
77
- /** 精确安装 spec(如 `dsh-plugin@2.0.5`):Hub 自我更新提示的是已过供应链门禁的版本,
78
- * 必须钉住安装,不能再用 @latest 被带回门禁内的最新版 */
80
+ /** 精确安装 spec(如 `dsh-plugin@2.0.5`):Hub 自我更新提示的是已过发布冷却期的版本,
81
+ * 必须锁定安装,不能再用 @latest 被带回冷却期内的最新版 */
79
82
  installSpec?: string;
80
83
  }): InstallTask;
81
84
  /**
@@ -102,7 +105,7 @@ export declare function verifyInstalledEntry(profile: string, target: string): {
102
105
  */
103
106
  export declare function hostSpawnMarker(invocation: Invocation, error: unknown): string | null;
104
107
  /**
105
- * 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 门禁拦下构建脚本而失败 ——
108
+ * 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 限制拦下构建脚本而失败 ——
106
109
  * 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
107
110
  * key@url 提示),或是某依赖的原生构建(`ERR_PNPM_IGNORED_BUILDS`,key = 列出的 name@version)——
108
111
  * 就把确切的 key 写进 profile 的 pnpm-workspace.yaml 并重试一次。
@@ -117,7 +120,7 @@ export declare function runPluginMutation(options: {
117
120
  task?: InstallTask;
118
121
  /** 待重启行的展示目标(owner/repo):卸载时用于把 npm 包名映射回仓库名 */
119
122
  displayTarget?: string;
120
- /** 运行中 loader:安装成功后热挂新条目、卸载成功后停用旧条目(缺失时仍需重启) */
123
+ /** 运行中 loader:安装成功后热加载新条目、卸载成功后停用旧条目(缺失时仍需重启) */
121
124
  loader?: LoaderHandle;
122
125
  /** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
123
126
  globalNpm?: string[];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 运行中 loader 的最小接口:卸载即时停用 / 安装即时热挂载用。
2
+ * 运行中 loader 的最小接口:卸载即时停用 / 安装即时热加载用。
3
3
  * 对应官方 `ctx.loader`(cordis-plugin-loader 的 Loader 服务)的读取、新建与移除面。
4
4
  * 移除方式:对匹配条目 live-disable(`entry.update({ disabled: true })`),
5
5
  * 而不是 `loader.remove(id)` —— disable 走 Entry.update 的 disabled 分支,不触发
@@ -28,7 +28,7 @@ export interface LoaderHandle {
28
28
  id?: string;
29
29
  name: string;
30
30
  }): Promise<string>;
31
- /** 按 id 解析条目:热挂载后确认 fiber 真的起来了(import 失败时条目在、但 fiber 为空)。 */
31
+ /** 按 id 解析条目:热加载后确认 fiber 真的起来了(import 失败时条目在、但 fiber 为空)。 */
32
32
  resolve(id: string): {
33
33
  id: string;
34
34
  options?: {
@@ -59,7 +59,7 @@ export declare function isEntryLoaded(loader: LoaderHandle | undefined, name: st
59
59
  * - `{ ok: true, live: false }`:宿主从未加载过它(磁盘已干净),无需刷新/重启;
60
60
  * - `{ ok: true, live: true }`:曾在 loader 中存活且已 live-disable —— 服务端即时卸载;
61
61
  * 带客户端 UI 的插件面板要页面刷新才摘除(由调用方按 `hadClientUi` 决定 needsReload);
62
- * - `{ ok: false, live: true }`:disable 失败 / 超时,需宿主重启兜底清理。
62
+ * - `{ ok: false, live: true }`:disable 失败 / 超时,需宿主重启后清理。
63
63
  * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
64
64
  */
65
65
  export declare function removeLoadedEntry(loader: LoaderHandle, name: string): Promise<{
@@ -67,7 +67,7 @@ export declare function removeLoadedEntry(loader: LoaderHandle, name: string): P
67
67
  live: boolean;
68
68
  }>;
69
69
  /**
70
- * 安装成功后把新插件热挂进运行中 loader(等价 profile bundle patch 的
70
+ * 安装成功后把新插件热加载进运行中 loader(等价 profile bundle patch 的
71
71
  * `- insert: - name: <pkg>` 行),让插件无需重启宿主即可生效。
72
72
  * 做法:在 Loader 根组新建条目并启动 —— Loader.write() 为空实现,不写盘;
73
73
  * 重启后由 profile 的 bundles 清单重新挂载,不会重复。
@@ -78,7 +78,7 @@ export declare function removeLoadedEntry(loader: LoaderHandle, name: string): P
78
78
  export declare function mountLoadedEntry(loader: LoaderHandle, name: string): Promise<boolean>;
79
79
  /**
80
80
  * 已安装包是否带客户端 UI(package.json 的 `dsh.client` 声明):client-modules 据它把
81
- * 客户端 bundle 注入宿主页面。带 UI 的插件热挂载后需要页面重新加载才会渲染,
81
+ * 客户端 bundle 注入宿主页面。带 UI 的插件热加载后需要页面重新加载才会渲染,
82
82
  * 网页端与桌面端一致 —— `__DSH_BOOT__` 注入清单由宿主服务端按请求现场拼装,
83
83
  * 壳不缓存,所以桌面端刷新页面同样能拿到新清单,无需重开 App。
84
84
  */
@@ -2,6 +2,13 @@ export interface NpmRelease {
2
2
  version: string;
3
3
  publishedAt: string;
4
4
  }
5
+ /** 最新版 + 可锁定的安装版本:安装锁定后者,pnpm 才不会回退到旧版本。 */
6
+ export interface NpmReleaseWithInstallable extends NpmRelease {
7
+ /** 已过发布冷却期的最高稳定版;时间表缺失或没有满足项时为 null(调用方退回不锁定版本)。 */
8
+ installable: NpmRelease | null;
9
+ }
10
+ /** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同一套规则。 */
11
+ export declare const MIN_RELEASE_AGE_MS: number;
5
12
  /** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
6
13
  export declare const MAX_RELEASE_AGE_PACKAGES = 50;
7
14
  /** 单包查询超时(ms):慢网络下失败即返回 null,不拖住整个响应。 */
@@ -9,7 +16,12 @@ export declare const RELEASE_AGE_TIMEOUT_MS = 8000;
9
16
  export declare const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
10
17
  /** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
11
18
  export declare function latestReleaseOf(packument: unknown): NpmRelease | null;
19
+ /**
20
+ * 已过发布冷却期的最高稳定版本:安装锁定它,pnpm 不会回退到旧版本。
21
+ * 只看纯 x.y.z(预发布版不参与,避免把用户带到不兼容预览版);时间表缺失或无满足项返回 null。
22
+ */
23
+ export declare function installableReleaseOf(packument: unknown, now?: number): NpmRelease | null;
12
24
  /** 请求体 → 去重后的合法包名列表;非数组 / 超量 / 含非法包名一律 null(拒绝非法输入)。 */
13
25
  export declare function parseReleaseAgeRequest(body: unknown): string[] | null;
14
- /** 查询一个包的最新版本与发布时间(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
15
- export declare function lookupReleaseAge(packageName: string, registry: string, proxy: string, timeoutMs?: number): Promise<NpmRelease | null>;
26
+ /** 查询一个包的最新版本、发布时间与可锁定的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
27
+ export declare function lookupReleaseAge(packageName: string, registry: string, proxy: string, timeoutMs?: number): Promise<NpmReleaseWithInstallable | null>;
@@ -63,17 +63,17 @@ export declare function gitLsRemote(url: string, proxy: string, timeoutMs: numbe
63
63
  /**
64
64
  * 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
65
65
  * 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
66
- * 即规范地址。与其它 GitHub 访问同口径 —— curl 子进程注入代理 env(Node 内置
66
+ * 即规范地址。与其它 GitHub 访问同一套规则 —— curl 子进程注入代理 env(Node 内置
67
67
  * http(s) 读不到系统代理,直连在部分网络下不可达)。
68
68
  * 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
69
69
  */
70
70
  export declare function finalUrl(url: string, proxy: string, timeoutMs: number): Promise<string | null>;
71
71
  /**
72
- * curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数兜底)。
72
+ * curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数重试)。
73
73
  * 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
74
- * 使「目录数据请求走设置里的代理」与 npm / git 安装通道口径一致。
74
+ * 使「目录数据请求走设置里的代理」与 npm / git 安装通道规则一致。
75
75
  * 失败(curl 不存在 / 连接失败 / 超时 / 非零退出)时 ok=false,全程不抛错;
76
- * reason 区分「证书吊销受阻」这类环境 TLS 问题,供调用方决定兜底策略。
76
+ * reason 区分「证书吊销受阻」这类环境 TLS 问题,供调用方决定备用策略。
77
77
  */
78
78
  export declare function fetchViaCurl(url: string, proxy: string, timeoutMs: number): Promise<{
79
79
  ok: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
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",