dsh-plugin 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.js +515 -362
- package/client/client.js.map +1 -1
- package/lib/services/install/operation-log.js +1 -1
- package/lib/services/install/registry-fallback.js +5 -0
- package/lib/services/install/task-queue.js +40 -3
- package/lib/services/npm-release-age.js +10 -6
- package/lib/services/profile/installed-versions.js +32 -4
- package/lib/types/services/install/install-types.d.ts +5 -0
- package/lib/types/services/install/registry-fallback.d.ts +2 -0
- package/lib/types/services/install/task-queue.d.ts +5 -0
- package/lib/types/services/npm-release-age.d.ts +1 -1
- package/lib/types/services/profile/installed-versions.d.ts +3 -0
- package/package.json +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +29 -4
- package/src/client/components/modals/modals.tsx +28 -4
- package/src/client/hooks/useCatalog.ts +45 -25
- package/src/client/hooks/useTaskQueue.ts +30 -8
- package/src/client/locales.ts +32 -8
- package/src/client/logic/failures.ts +34 -8
- package/src/client/logic/installed.ts +12 -5
- package/src/client/logic/release-age.ts +53 -5
- package/src/client/logic/urls.ts +11 -3
- package/src/server/services/install/install-types.ts +5 -0
- package/src/server/services/install/operation-log.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +5 -0
- package/src/server/services/install/task-queue.ts +37 -3
- package/src/server/services/npm-release-age.ts +10 -6
- package/src/server/services/profile/installed-versions.ts +30 -4
|
@@ -36,6 +36,8 @@ export interface QueueTask {
|
|
|
36
36
|
/** 安装时的目录信号快照(版本 + 仓库更新时间):任务成功后在本地记录,供「有更新」比对 */
|
|
37
37
|
version?: string
|
|
38
38
|
updatedAt?: string
|
|
39
|
+
/** 服务端在成功终态带出的**实际**安装版本(读 profile 的 node_modules):优先于目录版本写账本 */
|
|
40
|
+
installedVersion?: string
|
|
39
41
|
/** pending 排队中 / running 执行中 / cancelling 取消中(客户端乐观过渡态) */
|
|
40
42
|
status: 'pending' | 'running' | 'cancelling'
|
|
41
43
|
progress: number
|
|
@@ -186,20 +188,36 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
186
188
|
setPendingRestarts(next)
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
/** 向宿主记录某插件的安装时目录信号(version + updatedAt
|
|
191
|
+
/** 向宿主记录某插件的安装时目录信号(version + updatedAt)。
|
|
192
|
+
* 版本未知时**不写**:服务端把空版本当作「卸载清理」会删掉整条记录(连 npm 包名映射一起),
|
|
193
|
+
* 刷新页面后任务丢了快照就会走到这条路径 —— 宁可保留旧记录,也不能把账本清空。 */
|
|
190
194
|
const syncInstalledVersion = async (repo: string | null, version: string | undefined, updatedAt: string | undefined) => {
|
|
191
|
-
if (!repo) return
|
|
195
|
+
if (!repo || version === undefined || version === '') return
|
|
192
196
|
try {
|
|
193
197
|
await fetch('/dsh-plugin-hub/installed-version', {
|
|
194
198
|
method: 'POST',
|
|
195
199
|
headers: { 'content-type': 'application/json' },
|
|
196
|
-
body: JSON.stringify({ repo, version
|
|
200
|
+
body: JSON.stringify({ repo, version, updatedAt }),
|
|
197
201
|
})
|
|
198
202
|
} catch {
|
|
199
203
|
// 宿主未挂本地路由:静默忽略,仅本机展示受影响
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
/** 清除某仓库的版本记录(卸载成功后调用):空版本是服务端的「删除记录」语义,只在这里显式发送。 */
|
|
208
|
+
const clearInstalledVersion = async (repo: string | null) => {
|
|
209
|
+
if (!repo) return
|
|
210
|
+
try {
|
|
211
|
+
await fetch('/dsh-plugin-hub/installed-version', {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'content-type': 'application/json' },
|
|
214
|
+
body: JSON.stringify({ repo, version: null }),
|
|
215
|
+
})
|
|
216
|
+
} catch {
|
|
217
|
+
// 同上:清理失败不影响卸载结果
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
203
221
|
/** 后台任务结束(active 中消失后查终态确认):完成 → 刷新安装表 + 结果视图/toast;失败 → 完整错误弹窗 */
|
|
204
222
|
const finishQueueTask = (ok: boolean, q: QueueTask, lines: string[]) => {
|
|
205
223
|
applyQueue((prev) => prev.filter((x) => x.id !== q.id))
|
|
@@ -207,13 +225,15 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
207
225
|
refreshInstalled()
|
|
208
226
|
if (q.kind === 'uninstall') {
|
|
209
227
|
// 卸载成功:清掉本地版本记录
|
|
210
|
-
void
|
|
228
|
+
void clearInstalledVersion(q.repo)
|
|
211
229
|
onUninstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart, q.needsReload ?? false)
|
|
212
230
|
} else {
|
|
213
231
|
// 安装成功:记录安装时的目录信号(版本 + 仓库更新时间),供「有更新」比对;
|
|
214
232
|
// 同时缓存展示信息,待重启列表(服务端已登记)合并时直接补齐简介/版本
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
// 账本记**实际装上的版本**:目录版本可能滞后,抄目录字段会让「有更新」永久失真
|
|
234
|
+
const recordedVersion = q.installedVersion ?? q.version
|
|
235
|
+
void syncInstalledVersion(q.repo, recordedVersion, q.updatedAt)
|
|
236
|
+
pendingInfoRef.current.set(q.target, { desc: q.desc, version: recordedVersion })
|
|
217
237
|
onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart, q.action === 'update', q.needsReload ?? false, q.bundleDeselected === true)
|
|
218
238
|
}
|
|
219
239
|
} else {
|
|
@@ -260,21 +280,23 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
260
280
|
let lines: string[] = []
|
|
261
281
|
let attempts: string[] | undefined
|
|
262
282
|
let bundleDeselected: boolean | undefined
|
|
283
|
+
let installedVersion: string | undefined
|
|
263
284
|
try {
|
|
264
285
|
const res = await fetch(`/dsh-plugin-hub/status?task=${q.id}`, { cache: 'no-store' })
|
|
265
286
|
if (res.ok) {
|
|
266
|
-
const data = await res.json() as { task?: { status?: string; lines?: string[]; attempts?: string[]; needsRestart?: unknown; needsReload?: unknown; bundleDeselected?: unknown } }
|
|
287
|
+
const data = await res.json() as { task?: { status?: string; lines?: string[]; attempts?: string[]; needsRestart?: unknown; needsReload?: unknown; bundleDeselected?: unknown; installedVersion?: unknown } }
|
|
267
288
|
const task = data.task
|
|
268
289
|
if (task !== undefined) {
|
|
269
290
|
status = task.status ?? 'failed'
|
|
270
291
|
lines = task.lines ?? []
|
|
271
292
|
attempts = Array.isArray(task.attempts) ? task.attempts : undefined
|
|
272
293
|
bundleDeselected = task.bundleDeselected === true ? true : undefined
|
|
294
|
+
installedVersion = typeof task.installedVersion === 'string' && task.installedVersion !== '' ? task.installedVersion : undefined
|
|
273
295
|
// 终态带出的「是否需要重启 / 是否只需刷新」覆盖本地(服务端 done 时按 loader 热挂/即时移除情况设置)
|
|
274
296
|
const needsRestart = typeof task.needsRestart === 'boolean' ? task.needsRestart : q.needsRestart
|
|
275
297
|
const needsReload = typeof task.needsReload === 'boolean' ? task.needsReload : (q.needsReload ?? false)
|
|
276
298
|
if (status === 'done') {
|
|
277
|
-
const settled = { ...q, needsRestart, needsReload, bundleDeselected: bundleDeselected === true ? true : q.bundleDeselected }
|
|
299
|
+
const settled = { ...q, needsRestart, needsReload, bundleDeselected: bundleDeselected === true ? true : q.bundleDeselected, installedVersion }
|
|
278
300
|
// 卸载成功走进度过渡动画;安装成功直接收尾
|
|
279
301
|
if (q.kind === 'uninstall') settleDone(settled, lines)
|
|
280
302
|
else finishQueueTask(true, settled, lines)
|
package/src/client/locales.ts
CHANGED
|
@@ -22,6 +22,7 @@ export const zh = {
|
|
|
22
22
|
sortAria: '插件排序方式',
|
|
23
23
|
openHint: '打开 dsh-plugin.org',
|
|
24
24
|
toggleLangHint: '切换界面语言',
|
|
25
|
+
githubHint: '在 GitHub 查看源码',
|
|
25
26
|
fork: 'Fork',
|
|
26
27
|
loading: '正在加载插件数据…',
|
|
27
28
|
failed: '插件数据加载失败',
|
|
@@ -289,6 +290,7 @@ export const zh = {
|
|
|
289
290
|
queuedUninstallTitle: '已加入卸载排队',
|
|
290
291
|
queuedHint: '已加入排队,前序任务完成后将自动开始,可关闭本窗口继续浏览。',
|
|
291
292
|
cancelTask: '取消任务',
|
|
293
|
+
cancel: '取消',
|
|
292
294
|
cancelling: '正在取消…',
|
|
293
295
|
errorTitle: '操作失败',
|
|
294
296
|
errorTitleInstall: '安装失败',
|
|
@@ -350,6 +352,8 @@ export const zh = {
|
|
|
350
352
|
bundleDeselectedHint: '注意:插件市场在当前 profile 的 bundle 列表中被取消勾选,重启宿主后它不会出现。请到「设置 → 插件」重新勾选 dsh-plugin。',
|
|
351
353
|
failPnpmStoreHint: '安装器调用 pnpm 时报「store 位置不匹配」(ERR_PNPM_UNEXPECTED_STORE):你的 profile 目录里旧插件依赖是用另一个大版本的 pnpm 生成的,当前 pnpm 出于安全不认旧目录,导致任何插件装进该 profile 都会失败。请删除该 profile 目录下的 node_modules 与 pnpm-lock.yaml(可先到「系统日志」或 ~/.dsh/profiles/<profile>/hub.log 查看完整报错),再重新安装。这不是插件本身的问题。',
|
|
352
354
|
failPnpmUnusedPatchHint: '安装器调用 pnpm 时报「有些补丁没有被使用」(ERR_PNPM_UNUSED_PATCH):你的 profile 里留着一条指向旧版本 dsh-plugin 的补丁声明(例如 dsh-plugin@1.4.2),而本次安装解析到的版本已经不是它,pnpm 出于安全直接中止整次安装,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml(pnpm 11 及以上)或 package.json 的 pnpm.patchedDependencies(pnpm 10)里删除该 dsh-plugin 补丁条目后重试。',
|
|
355
|
+
failProfileStateHint: '安装器调用 pnpm 时报 profile 的元数据与当前环境对不上(如 ERR_PNPM_OUTDATED_LOCKFILE / ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND / ERR_PNPM_BAD_PM_VERSION / ERR_PNPM_PATCH_NOT_APPLIED):profile 里的 pnpm-lock.yaml、package.json 或补丁声明与当前 pnpm 版本不一致,pnpm 在真正装插件之前就中止了,所以这个 profile 里装任何插件都会失败。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)里删除 pnpm-lock.yaml 后重试(pnpm 会按当前依赖重新生成);若报的是 packageManager 版本不符,把 package.json 里的 packageManager 字段改成当前 pnpm 的大版本或直接删掉该字段;仍不行请重建该 profile 的依赖。',
|
|
356
|
+
failNodeEngineHint: '这个插件(或它的某个依赖)声明了更高的 Node / pnpm 版本要求(ERR_PNPM_UNSUPPORTED_ENGINE / EBADENGINE),当前版本不满足,pnpm 在安装阶段直接拒绝。这不是插件本身的问题——请把 Node 升级到要求的版本(nvm / fnm 切换,或安装新版 Node.js),确认 node -v 与 pnpm -v 都满足要求后重试。',
|
|
353
357
|
// 404 依赖:点名 registry 上不存在的那个包(取不到包名时跳过这行),再给删除条目的指引
|
|
354
358
|
failPnpmMissingDepTarget: '{pkg} 在 npm registry 上不存在(404)',
|
|
355
359
|
failPnpmMissingDepHint: '安装器调用 pnpm 时报「包不存在」(ERR_PNPM_FETCH_404):你的 profile 里留着一条指向 registry 上未发布包的依赖,pnpm 每次解析依赖都会先卡在它上面,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 package.json 里删除该依赖条目(同时删掉对应的 pnpm-lock.yaml 条目或整个锁文件)后重试。',
|
|
@@ -374,9 +378,9 @@ export const zh = {
|
|
|
374
378
|
failReason_buildToolchainMissing: '这个插件依赖需要在本机编译的原生模块,但找不到可用的编译器。',
|
|
375
379
|
failKind_pnpmStore: 'pnpm store 版本不匹配',
|
|
376
380
|
failReason_pnpmStore: 'profile 里的依赖由另一大版本的 pnpm 生成,当前 pnpm 不认。',
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
failKind_pnpmModulesMismatch: 'profile 的依赖目录与当前 pnpm 配置不一致',
|
|
382
|
+
failReason_pnpmModulesMismatch: 'node_modules 是用另一套 pnpm 配置(hoist / 虚拟目录长度等)创建的,pnpm 拒绝复用。',
|
|
383
|
+
failPnpmModulesMismatchHint: '安装器调用 pnpm 时报「modules 目录与当前配置不一致」(`ERR_PNPM_*_DIFF`,例如 virtual-store-dir-max-length、public-hoist-pattern、hoist 规则、node-linker):profile 里的 node_modules 是在另一套 pnpm 配置下生成的(常见于改过 .npmrc、换过 pnpm 大版本,或桌面端内置 pnpm 与 PATH 上的 pnpm 不是同一个),当前 pnpm 出于安全拒绝在它上面继续操作 —— 所以这个 profile 里的安装/卸载都会失败。修复(任选其一,在 profile 目录 ~/.dsh/profiles/<profile> 下操作):① 执行一次 pnpm install,让它按当前配置重建 modules 目录;② 删掉该目录下的 node_modules,下次安装/卸载会自动重建;③ 如果你在 .npmrc 里设过这些配置项,把它们统一成同一个值(或删掉自定义、回到默认)。这不是插件本身的问题。',
|
|
380
384
|
failKind_pnpmWorkspace: 'workspace 根校验阻止了安装',
|
|
381
385
|
failReason_pnpmWorkspace: 'profile 被当作 workspace 根,但没有声明根操作,pnpm 中止了安装。',
|
|
382
386
|
failKind_originRejected: '本地服务的来源校验未通过',
|
|
@@ -385,6 +389,10 @@ export const zh = {
|
|
|
385
389
|
failReason_pnpmPolicy: '版本未满最小发布年龄,或缺完整性校验 / 来源不可信。',
|
|
386
390
|
failKind_pnpmUnusedPatch: 'profile 残留旧补丁声明',
|
|
387
391
|
failReason_pnpmUnusedPatch: 'profile 里留着指向旧版本 dsh-plugin 的补丁,pnpm 出于安全中止了安装。',
|
|
392
|
+
failKind_profileState: 'profile 的 pnpm 元数据与本机不一致',
|
|
393
|
+
failReason_profileState: 'profile 的 lockfile / package.json / packageManager 版本与当前 pnpm 对不上,安装没开始就被中止。',
|
|
394
|
+
failKind_nodeEngine: '本机 Node / pnpm 版本不满足要求',
|
|
395
|
+
failReason_nodeEngine: '这个插件(或其依赖)要求更高的 Node / pnpm 版本,当前版本被 pnpm 拒绝。',
|
|
388
396
|
failKind_pnpmMissingDep: '依赖包在 registry 上不存在',
|
|
389
397
|
failReason_pnpmMissingDep: 'profile 里有一条指向未发布包的依赖,pnpm 每次解析都卡在它上面。',
|
|
390
398
|
failKind_pluginMissingDep: '插件依赖未发布',
|
|
@@ -411,10 +419,16 @@ export const zh = {
|
|
|
411
419
|
failReason_hostIncompatible: '插件声明的兼容清单里没有你正在运行的 DSH 版本。',
|
|
412
420
|
failKind_network: '网络连接失败',
|
|
413
421
|
failReason_network: '安装过程中连不上 npm 源或代理,请求没有完成。',
|
|
422
|
+
failKind_tlsTrust: 'HTTPS 证书校验失败',
|
|
423
|
+
failReason_tlsTrust: '本机有程序替换了 HTTPS 证书,Node / pnpm 不信任它的根证书。',
|
|
424
|
+
failTlsTrustHint: '安装器访问 npm 源时 TLS 证书校验失败(`ERR_PNPM_META_FETCH_FAIL` / unable to verify the first certificate):本机有程序在替换 HTTPS 证书 —— 公司网络的 SSL 审计、杀毒软件的网页防护、Fiddler / Charles 这类抓包工具,或本机代理的 HTTPS 解密 —— 而 Node / pnpm 不信任它的根证书。这不是插件本身的问题,任何插件都会一样失败。按顺序处理:① 如果公司网络有 SSL 审计,找 IT 要根证书(.cer / .pem),把环境变量 NODE_EXTRA_CA_CERTS 指向该文件后重试;② Node ≥ 22.15 可以设置 NODE_OPTIONS=--use-system-ca,让 Node 直接信任系统证书库;③ 如果只是本机代理开了 HTTPS 解密,把 registry.npmjs.org 加进直连(跳过 MITM);④ 也可以先临时切到公共镜像源 https://registry.npmmirror.com 重试。不建议直接关闭 strict-ssl:那等于放弃校验,会把你暴露在中间人攻击下。',
|
|
414
425
|
failKind_hostCliSpawn: '无法启动 DSH 宿主',
|
|
415
426
|
failReason_hostCliSpawn: '解析到的宿主入口起不来,安装命令没有真正执行。',
|
|
416
427
|
failKind_hubState: '插件中心状态不同步',
|
|
417
428
|
failReason_hubState: '本地记录的安装状态与实际不一致,这次操作没有生效。',
|
|
429
|
+
failKind_profileBundleMissing: 'profile 的依赖没装全',
|
|
430
|
+
failReason_profileBundleMissing: '宿主找不到 profile 里声明的某个 bundle,安装命令还没执行就失败了。',
|
|
431
|
+
failProfileBundleMissingHint: '宿主在启动插件管理时找不到 profile 依赖「{pkg}」—— 说明这个 profile 里声明的依赖没有装全(node_modules 被删过、上次安装中断,或桌面应用升级不完整)。这与你要装的插件无关,换任何插件都会一样失败。修复:① 让宿主把这个 profile 的依赖装回来(在桌面应用内重新打开插件中心,或执行 `dsh plugin --profile {profile} install`;桌面 profile 需在应用内执行,外部 CLI 无权管理);② 仍然报同样的错,说明是应用本体不完整,请重装或升级 DeepSeek Harness 桌面端。',
|
|
418
432
|
failKind_timeout: '操作超时',
|
|
419
433
|
failReason_timeout: '安装耗时超过上限仍未完成,已经中止。',
|
|
420
434
|
failKind_profileLock: 'profile 被其他进程占用',
|
|
@@ -592,8 +606,6 @@ export const en = {
|
|
|
592
606
|
settingsUpdateDesc: 'Update checks and the network channels used for installs',
|
|
593
607
|
settingsCheckOnStart: 'Check for updates on startup',
|
|
594
608
|
settingsCheckOnStartDesc: 'Check installed plugins for new versions when the hub opens; you will be notified when updates are found',
|
|
595
|
-
settingsAutoInstall: 'Auto-install updates',
|
|
596
|
-
settingsAutoInstallDesc: 'Install detected updates in the background without asking (a host restart applies them)',
|
|
597
609
|
settingsProxy: 'HTTP proxy',
|
|
598
610
|
settingsProxyDesc: 'Route npm, git and catalog requests through this proxy; leave empty to connect directly',
|
|
599
611
|
// 代理连通性实时探测:输入停笔后提示检测结果(不通仍可保存,先填地址后开代理也合法)
|
|
@@ -835,6 +847,8 @@ export const en = {
|
|
|
835
847
|
bundleDeselectedHint: 'Note: the marketplace is unchecked in this profile\'s bundle list, so it will not appear after the host restarts. Re-check dsh-plugin under Settings → Plugins.',
|
|
836
848
|
failPnpmStoreHint: 'pnpm reported an unexpected store location (ERR_PNPM_UNEXPECTED_STORE): the profile directory was previously set up with a different major version of pnpm, and the current pnpm refuses to reuse it — no plugin can be installed into that profile. Delete node_modules and pnpm-lock.yaml under the profile directory (see System Logs or ~/.dsh/profiles/<profile>/hub.log for the full output), then reinstall. This is not a problem with the plugin itself.',
|
|
837
849
|
failPnpmUnusedPatchHint: 'pnpm reported that some patches were not used (ERR_PNPM_UNUSED_PATCH): your profile still holds a patch entry for an older dsh-plugin version (for example dsh-plugin@1.4.2), while this install resolved a different version, so pnpm aborts the whole install for safety — no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that dsh-plugin patch entry from pnpm-workspace.yaml (pnpm 11+) or from pnpm.patchedDependencies in package.json (pnpm 10) inside the profile directory (~/.dsh/profiles/<profile>), then retry.',
|
|
850
|
+
failProfileStateHint: 'pnpm reported that the profile metadata does not match the current environment (e.g. ERR_PNPM_OUTDATED_LOCKFILE / ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND / ERR_PNPM_BAD_PM_VERSION / ERR_PNPM_PATCH_NOT_APPLIED): pnpm-lock.yaml, package.json or the patch declarations inside the profile disagree with the pnpm version in use, so pnpm aborts before installing anything — every plugin install into this profile will fail. This is not a problem with the plugin itself. Delete pnpm-lock.yaml inside the profile directory (~/.dsh/profiles/<profile>) and retry (pnpm regenerates it from the declared dependencies); if the error is about packageManager, set that field in package.json to the current pnpm major or remove it; if it still fails, rebuild the profile dependencies.',
|
|
851
|
+
failNodeEngineHint: 'This plugin (or one of its dependencies) declares a higher Node / pnpm version (ERR_PNPM_UNSUPPORTED_ENGINE / EBADENGINE), which the current one does not satisfy, so pnpm refuses at install time. This is not a problem with the plugin itself. Upgrade Node to the required version (switch with nvm / fnm, or install a newer Node.js), confirm node -v and pnpm -v both satisfy it, then retry.',
|
|
838
852
|
// 缺失依赖:点名 404 的包(解析不出时跳过该行),再给删除指引
|
|
839
853
|
failPnpmMissingDepTarget: '{pkg} was not found on the npm registry (404)',
|
|
840
854
|
failPnpmMissingDepHint: 'pnpm reported that a package does not exist on the npm registry (ERR_PNPM_FETCH_404): your profile keeps a dependency on a package that was never published, and pnpm hits it every time it resolves dependencies — no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that dependency from package.json inside the profile directory (~/.dsh/profiles/<profile>) — along with its entry in pnpm-lock.yaml, or the lockfile itself — then retry.',
|
|
@@ -859,9 +873,9 @@ export const en = {
|
|
|
859
873
|
failReason_buildToolchainMissing: 'A dependency of this plugin must be compiled locally, but no usable compiler was found.',
|
|
860
874
|
failKind_pnpmStore: 'pnpm store version mismatch',
|
|
861
875
|
failReason_pnpmStore: 'The profile dependencies were written by a different pnpm major, which the current pnpm does not accept.',
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
876
|
+
failKind_pnpmModulesMismatch: 'profile dependency directory does not match the current pnpm config',
|
|
877
|
+
failReason_pnpmModulesMismatch: 'node_modules was created with a different pnpm config (hoisting / virtual store length), so pnpm refuses to reuse it.',
|
|
878
|
+
failPnpmModulesMismatchHint: 'pnpm reported that the modules directory does not match the current configuration (`ERR_PNPM_*_DIFF`, e.g. virtual-store-dir-max-length, public-hoist-pattern, hoisting rules, node-linker): node_modules inside the profile was built under a different pnpm config (typically after editing .npmrc, switching pnpm majors, or when the pnpm bundled with the desktop app differs from the one on PATH), so the current pnpm refuses to operate on it — installs and removals in this profile will keep failing. Fix it (any one of these, run inside the profile directory ~/.dsh/profiles/<profile>): ① run pnpm install once so it recreates the modules directory with the current config; ② delete node_modules there — the next install/removal recreates it; ③ if you set these options in .npmrc, make the values consistent (or drop the overrides and go back to the defaults). This is not a problem with the plugin itself.',
|
|
865
879
|
failKind_pnpmWorkspace: 'workspace-root check blocked the install',
|
|
866
880
|
failReason_pnpmWorkspace: 'The profile is treated as a workspace root without a root operation declared, so pnpm aborted.',
|
|
867
881
|
failKind_originRejected: 'request origin rejected by the local service',
|
|
@@ -870,6 +884,10 @@ export const en = {
|
|
|
870
884
|
failReason_pnpmPolicy: 'The version is younger than the minimum release age, or an integrity / trust check failed.',
|
|
871
885
|
failKind_pnpmUnusedPatch: 'stale patch entry in the profile',
|
|
872
886
|
failReason_pnpmUnusedPatch: 'The profile keeps a patch for an older dsh-plugin version, so pnpm aborted for safety.',
|
|
887
|
+
failKind_profileState: 'profile pnpm metadata does not match this machine',
|
|
888
|
+
failReason_profileState: 'The profile lockfile / package.json / packageManager version disagrees with the current pnpm, so the install aborts before starting.',
|
|
889
|
+
failKind_nodeEngine: 'Node / pnpm on this machine is below the required version',
|
|
890
|
+
failReason_nodeEngine: 'This plugin (or a dependency) requires a newer Node / pnpm, so pnpm rejects the install.',
|
|
873
891
|
failKind_pnpmMissingDep: 'a dependency does not exist on the registry',
|
|
874
892
|
failReason_pnpmMissingDep: 'The profile keeps a dependency on an unpublished package, which pnpm hits on every resolve.',
|
|
875
893
|
failKind_pluginMissingDep: 'plugin dependency is not published',
|
|
@@ -896,10 +914,16 @@ export const en = {
|
|
|
896
914
|
failReason_hostIncompatible: 'The plugin\'s declared compatibility list does not include the DSH version you are running.',
|
|
897
915
|
failKind_network: 'network connection failed',
|
|
898
916
|
failReason_network: 'The install could not reach the npm registry or the proxy, so the request never completed.',
|
|
917
|
+
failKind_tlsTrust: 'HTTPS certificate verification failed',
|
|
918
|
+
failReason_tlsTrust: 'Something on this machine is replacing HTTPS certificates, and Node / pnpm does not trust its root CA.',
|
|
919
|
+
failTlsTrustHint: 'TLS certificate verification failed while reaching the npm registry (`ERR_PNPM_META_FETCH_FAIL` / unable to verify the first certificate): something on this machine is replacing HTTPS certificates — corporate SSL inspection, an antivirus web shield, a debugging proxy such as Fiddler / Charles, or HTTPS decryption in a local proxy — and Node / pnpm does not trust its root CA. This is not a problem with the plugin; every plugin would fail the same way. In order: ① if your network performs SSL inspection, ask IT for the root certificate (.cer / .pem) and point the NODE_EXTRA_CA_CERTS environment variable at it; ② on Node ≥ 22.15 you can set NODE_OPTIONS=--use-system-ca so Node trusts the system certificate store; ③ if only your local proxy decrypts HTTPS, add registry.npmjs.org to its direct (no-MITM) list; ④ as a temporary workaround, switch to a public mirror such as https://registry.npmmirror.com and retry. We do not recommend disabling strict-ssl: that gives up verification entirely and exposes you to man-in-the-middle attacks.',
|
|
899
920
|
failKind_hostCliSpawn: 'the DSH host could not be launched',
|
|
900
921
|
failReason_hostCliSpawn: 'The resolved host entry point would not start, so the install command never ran.',
|
|
901
922
|
failKind_hubState: 'plugin hub state is out of sync',
|
|
902
923
|
failReason_hubState: 'The locally recorded install state does not match reality, so this operation had no effect.',
|
|
924
|
+
failKind_profileBundleMissing: 'profile dependencies are incomplete',
|
|
925
|
+
failReason_profileBundleMissing: 'The host cannot find a bundle the profile declares, so the install failed before it even started.',
|
|
926
|
+
failProfileBundleMissingHint: 'The host could not resolve the profile dependency "{pkg}" while starting plugin management — the dependencies declared by this profile are not fully installed (node_modules was deleted, a previous install was interrupted, or the desktop app update was incomplete). This has nothing to do with the plugin you were installing; any plugin would fail the same way. Fix it: ① let the host install the profile dependencies again (reopen the Plugin Hub inside the desktop app, or run `dsh plugin --profile {profile} install`; a desktop-managed profile has to be handled from inside the app, an external CLI is not allowed); ② if the same error persists, the app installation itself is incomplete — reinstall or upgrade DeepSeek Harness Desktop.',
|
|
903
927
|
failKind_timeout: 'operation timed out',
|
|
904
928
|
failReason_timeout: 'The install exceeded the time limit and was aborted.',
|
|
905
929
|
failKind_profileLock: 'profile is locked by another process',
|
|
@@ -148,7 +148,7 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
148
148
|
return next
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | '
|
|
151
|
+
export type FailureKind = 'npmTooOld' | 'nodeEngine' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmModulesMismatch' | 'profileState' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'registryAuth' | 'profileExclusive' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'buildToolchainMissing' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'hostIncompatible' | 'tlsTrust' | 'network' | 'hostCliSpawn' | 'hubState' | 'profileBundleMissing' | 'timeout' | 'profileLock' | 'dshTooOld' | 'localEnv' | 'hub'
|
|
152
152
|
|
|
153
153
|
/** 失败动作口径:标题、上报正文与提示文案按安装/更新/卸载取词,不写死「安装」。 */
|
|
154
154
|
export type FailureAction = 'install' | 'update' | 'uninstall'
|
|
@@ -226,6 +226,9 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
226
226
|
// npm 自身崩溃(edgesOut)或服务端标记的本机 npm 版本过低。必须最先判,否则会被外层
|
|
227
227
|
// ERR_PNPM_PREPARE_PACKAGE 吞成「插件打包问题」
|
|
228
228
|
if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
|
|
229
|
+
// 本机 Node / pnpm 版本低于包声明的下限:装任何声明了同样 engine 的插件都会失败,属本机环境。
|
|
230
|
+
// 只认硬错误码 —— pnpm 的「WARN Unsupported Engine」只是告警,照着判会盖掉真实失败原因
|
|
231
|
+
if (/ERR_PNPM_UNSUPPORTED_ENGINE|ERR_PNPM_INVALID_NODE_VERSION|\bEBADENGINE\b/i.test(message)) return 'nodeEngine'
|
|
229
232
|
// 本机缺 pnpm。必须在 dshMissing 之前 —— 后者的裸「command not found」会吞掉 pnpm 缺失
|
|
230
233
|
if (/\[pnpm-missing\]|pnpm not found|pnpm: command not found|spawn pnpm ENOENT|'pnpm' 不是内部或外部命令|"pnpm" 不是内部或外部命令|pnpm['"]?\s*is not recognized/i.test(message)) return 'pnpmMissing'
|
|
231
234
|
// 本机缺 git。必须在 dshMissing 之前,原因同上(裸「is not recognized」会吞掉 git 缺失)
|
|
@@ -234,12 +237,13 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
234
237
|
if (/\[npm-missing\]|spawn npm ENOENT|'npm' 不是内部或外部命令|"npm" 不是内部或外部命令|npm['"]?\s*is not recognized|npm: command not found/i.test(message)) return 'npmMissing'
|
|
235
238
|
// 本机缺 dsh。服务端 [dsh-missing] 标记乱码免疫(Windows GBK 解码后无法匹配原文)
|
|
236
239
|
if (/\[dsh-missing\]|不是内部或外部命令|is not recognized as an internal or external command|command not found|spawn dsh ENOENT/i.test(message)) return 'dshMissing'
|
|
237
|
-
// profile 的 modules
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
+
// profile 的 modules 目录与当前 pnpm 配置不一致(pnpm 会把 virtual-store-dir-max-length /
|
|
241
|
+
// public-hoist-pattern / hoist 规则 / node-linker 等写进 node_modules/.modules.yaml,任一不一致就拒绝复用)。
|
|
242
|
+
// 必须在 pnpmStore 之前:两者处置方式不同(这里只需按当前配置重建 modules 目录)。
|
|
243
|
+
if (/ERR_PNPM_[A-Z0-9_]*DIFF\b|modules directory was created using a different/i.test(message)) return 'pnpmModulesMismatch'
|
|
240
244
|
// store 版本不匹配:profile 依赖由另一大版本 pnpm 生成,当前 pnpm 不认。
|
|
241
245
|
// 必须在 pnpmMissing 之后(pnpm 能跑起来报错,不是找不到命令)
|
|
242
|
-
if (/ERR_PNPM_UNEXPECTED_(VIRTUAL_)?STORE|Unexpected (virtual )?store location/i.test(message)) return 'pnpmStore'
|
|
246
|
+
if (/ERR_PNPM_UNEXPECTED_(VIRTUAL_)?STORE|ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE|Unexpected (virtual )?store location/i.test(message)) return 'pnpmStore'
|
|
243
247
|
// pnpm 拒绝往 workspace 根装依赖:profile 被视为 workspace 根但未声明根操作
|
|
244
248
|
if (/ERR_PNPM_ADDING_TO_ROOT|add the dependency to the workspace root/i.test(message)) return 'pnpmWorkspace'
|
|
245
249
|
// 本地服务来源校验拒绝(403 untrusted origin),请求没进安装流程。必须在 pnpmPolicy 之前
|
|
@@ -250,6 +254,10 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
250
254
|
if (missingTarballIntegrity(message) || mismatchedTarballIntegrity(message) || /ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|Minimum release age|minimumReleaseAge|untrusted origin/i.test(message)) return 'pnpmPolicy'
|
|
251
255
|
// profile 残留旧版本 patch 声明,pnpm 中止安装。必须在兜底之前
|
|
252
256
|
if (/ERR_PNPM_UNUSED_PATCH|patches were not used/i.test(message)) return 'pnpmUnusedPatch'
|
|
257
|
+
// profile 的 pnpm 元数据与本机不一致(lockfile 过期 / 找不到可安装的 manifest / packageManager
|
|
258
|
+
// 版本不符 / 补丁未生效 / workspace 依赖缺失):都是 profile 自身状态,这个 profile 里装任何
|
|
259
|
+
// 插件都会同样失败。必须在 pluginPrepare 之前 —— 这类错误常被 pnpm 包一层 Command failed
|
|
260
|
+
if (/ERR_PNPM_OUTDATED_LOCKFILE|ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND|ERR_PNPM_NO_PKG_MANIFEST|ERR_PNPM_BAD_PM_VERSION|ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY|ERR_PNPM_PATCH_NOT_APPLIED|ERR_PNPM_WORKSPACE_PKG_NOT_FOUND|ERR_PNPM_JSON_PARSE|ERR_PNPM_PKG_MANIFEST_IS_NOT_OBJECT|ERR_PNPM_MISSING_HOIST_PATTERN|Cannot install with "frozen-lockfile"/i.test(message)) return 'profileState'
|
|
253
261
|
// profile 文件被其他进程占用(os error 32 / EBUSY)。ASCII 特征免疫乱码
|
|
254
262
|
if (/os error 32|EBUSY|resource busy or locked|being used by another process/i.test(message)) return 'fileLocked'
|
|
255
263
|
// 系统拒绝写 profile(os error 5 / EPERM / EACCES)。ASCII 特征免疫乱码
|
|
@@ -265,9 +273,12 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
265
273
|
// profile 里指向已删除目录的本地依赖(file:/link:):pnpm 直接 ENOENT(lstat),任何安装都会失败。
|
|
266
274
|
// 必须在 network 之前判 —— 同一份日志常混着 pnpm 重试产生的 UND_ERR_* 警告,先判网络会把根因盖掉。
|
|
267
275
|
if (/ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND|Could not install from .+ as it does not exist|pnpm:\s*ENOENT: no such file or directory,\s*(?:lstat|scandir|readlink)/i.test(message)) return 'pnpmLinkedDirMissing'
|
|
276
|
+
// TLS 证书信任失败(公司 SSL 审计 / 杀软网页防护 / 本机代理替换证书):属于本机证书链问题,
|
|
277
|
+
// 必须在 network 之前 —— 否则会被当成「网络不通」,给出的排查方向完全不对。
|
|
278
|
+
if (/unable to verify the first certificate|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE|ERR_TLS_CERT_ALTNAME_INVALID|CERT_HAS_EXPIRED|certificate has expired|UNABLE_TO_GET_ISSUER_CERT|CERT_UNTRUSTED|ERR_TLS_CERT/i.test(message)) return 'tlsTrust'
|
|
268
279
|
// 网络/代理/源配置问题。必须在 prepare 之前判:git fetch 失败常被外层包成 "Command failed"
|
|
269
280
|
// tarball 下载失败的括号里可能是数字码(pnpm 自己的)或符号码(undici 的 UND_ERR_*),两种都要认
|
|
270
|
-
if (/\[network\]|ERR_PNPM_GIT_FETCH_FAILED|ETIMEDOUT|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH|UND_ERR_[A-Z0-9_]+|getaddrinfo|Could not connect|Could not resolve host|Network unreachable|Failed to connect|socket hang up|CERT_HAS_EXPIRED|SSL certificate problem
|
|
281
|
+
if (/\[network\]|ERR_PNPM_GIT_FETCH_FAILED|ERR_PNPM_FETCH_5\d\d|ERR_PNPM_META_FETCH_FAIL|ERR_SOCKET_TIMEOUT|ETIMEDOUT|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH|UND_ERR_[A-Z0-9_]+|getaddrinfo|Could not connect|Could not resolve host|Network unreachable|Failed to connect|socket hang up|CERT_HAS_EXPIRED|SSL certificate problem|SSL routines|TLS handshake|SSL handshake|ERR_SSL_|ssl_error|fetch failed|failed to fetch|\bGET https?:\/\/\S+\.tgz\s+error \([A-Za-z0-9_]+\)/i.test(message)) return 'network'
|
|
271
282
|
// 浏览器端请求超时(客户端 fetch 被 abort):请求没到宿主,属于连不上网络,与插件无关
|
|
272
283
|
if (/\[request-timeout\]/i.test(message)) return 'network'
|
|
273
284
|
// 装后校验发现入口文件缺失([packaging])= 分发物缺构建产物
|
|
@@ -300,6 +311,10 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
300
311
|
if (/atomic-write: timed out waiting for the writer lock|timed out waiting for the writer lock/i.test(message)) return 'profileLock'
|
|
301
312
|
// 宿主自己的 allowBuilds 摘要行(pnpm 原文只写进 diagnostics 日志文件,终端只剩这一句)
|
|
302
313
|
if (/git-hosted plugins build on install via their prepare script|add the exact key pnpm printed above under allowBuilds/i.test(message)) return 'pnpmIgnoredBuild'
|
|
314
|
+
// 宿主启动插件管理时找不到某个 profile bundle(`cannot resolve profile bundle "<pkg>"`):
|
|
315
|
+
// 该 profile 声明的依赖没装全(node_modules 被删过 / 上次安装中断 / 桌面端更新不完整),
|
|
316
|
+
// 与本次要装的插件无关 —— 必须在 hub 兜底之前,否则又会被当成未知错误丢给我们
|
|
317
|
+
if (/cannot resolve profile bundle/i.test(message)) return 'profileBundleMissing'
|
|
303
318
|
// 宿主自身入口起不来(服务端 [host-spawn] 标记):环境问题,给提示、不引导提 Issue。
|
|
304
319
|
// 不匹配 `spawn … ENOENT` 文本 —— 那个形态任何 spawn(curl/git/taskkill…)都会产出
|
|
305
320
|
if (/\[host-spawn\]/i.test(message)) return 'hostCliSpawn'
|
|
@@ -321,7 +336,7 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
321
336
|
/** 「特征指向本机环境但说不清具体哪一类」的兜底判据:系统调用错误码(权限/路径过长/设备异常)、
|
|
322
337
|
* 证书与代理校验失败、本地配置文件(~/.npmrc、profile 的 package.json)损坏。
|
|
323
338
|
* 只做排除用:命中它表示「不是插件侧问题」,不代表我们已经定位到原因。 */
|
|
324
|
-
const LOCAL_ENV_RE = /os error \d{1,3}\b|ERROR_[A-Z_]{4,}|\bE(?:ACCES|PERM|NOENT|NAMETOOLONG|INVAL|PROTO|ISDIR|NOTDIR|EXIST|BADF|NOTEMPTY|JSONPARSE)\b|Unexpected token .{0,60}in JSON|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE|ERR_TLS_CERT_ALTNAME_INVALID|unable to verify the first certificate|self[- ]signed certificate|ERR_INVALID_URL|invalid registry|proxy authentication|407 Proxy/i
|
|
339
|
+
const LOCAL_ENV_RE = /os error \d{1,3}\b|ERROR_[A-Z_]{4,}|\bE(?:ACCES|PERM|NOENT|NAMETOOLONG|INVAL|PROTO|ISDIR|NOTDIR|EXIST|BADF|NOTEMPTY|JSONPARSE|NOMEM)\b|JavaScript heap out of memory|Unexpected token .{0,60}in JSON|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE|ERR_TLS_CERT_ALTNAME_INVALID|unable to verify the first certificate|self[- ]signed certificate|ERR_INVALID_URL|invalid registry|proxy authentication|407 Proxy/i
|
|
325
340
|
|
|
326
341
|
/** 从服务端 `[npm-too-low]` 标记行提取本机 npm 版本(如 `[npm-too-low] npm@11.3.0` → "11.3.0");
|
|
327
342
|
* 历史记录无标记时返回 null,前端据此决定提示文案是否带具体版本。 */
|
|
@@ -341,7 +356,7 @@ const PEER_WARN_RE = /missing peer|issues with peer dependencies|peer dependenci
|
|
|
341
356
|
/** 安装器自己打的诊断标记行:它们本身就是「为什么没有日志 / 卡在哪一步」的证据,
|
|
342
357
|
* 不含 CORE_LINE_RE 的关键词,必须无条件保留 —— 否则上报正文会退回成只剩宿主那句摘要,
|
|
343
358
|
* 我们又拿不到任何可排查的信息(#382/#393 就是这样)。 */
|
|
344
|
-
const MARKER_LINE_RE = /^\[(?:pnpm log|no-output|desktop-cli-fallback|dsh-too-old|host-spawn|npm-too-low|git-missing|npm-missing|pnpm-missing|dsh-missing|network|packaging|registry-missing|registry-mirror-lag|internal|exit\b|timed out)/i
|
|
359
|
+
const MARKER_LINE_RE = /^\[(?:pnpm log|no-output|desktop-cli-fallback|dsh-too-old|host-spawn|npm-too-low|git-missing|npm-missing|pnpm-missing|dsh-missing|network|packaging|registry-missing|registry-mirror-lag|registry-fallback|allow build|stale pipe|install-channel|internal|exit\b|timed out)/i
|
|
345
360
|
/** 提交 issue 时正文里错误摘要的上限字符数。GitHub 请求行上限 8192 字节,
|
|
346
361
|
* 固定模板与 URL 编码开销约 1~2K,核心错误(以 ASCII 日志为主)可安全带到 ~5K;
|
|
347
362
|
* 仍超长时 pluginIssueUrl 会逐档缩小核心预算,最终 URL 不会超限。 */
|
|
@@ -503,6 +518,17 @@ export function dshTooOldInfoOf(message: string): { pkg: string; floor: string;
|
|
|
503
518
|
return m === null ? null : { pkg: m[1]!, floor: m[2]!, current: m[3]! }
|
|
504
519
|
}
|
|
505
520
|
|
|
521
|
+
/**
|
|
522
|
+
* `cannot resolve profile bundle "<pkg>" from the dsh installation or <profileDir>` 里的
|
|
523
|
+
* 缺失包名与 profile 目录;只取到包名也算成功(profile 目录可能被换行截断)。
|
|
524
|
+
*/
|
|
525
|
+
export function unresolvedBundleOf(message: string): { pkg: string; profileDir: string } | null {
|
|
526
|
+
const m = /cannot resolve profile bundle\s+"([^"]+)"\s+from the dsh installation or\s+(.+?)\s*(?:;|$)/im.exec(message)
|
|
527
|
+
if (m !== null) return { pkg: m[1]!, profileDir: m[2]!.trim() }
|
|
528
|
+
const only = /cannot resolve profile bundle\s+"([^"]+)"/i.exec(message)
|
|
529
|
+
return only === null ? null : { pkg: only[1]!, profileDir: '' }
|
|
530
|
+
}
|
|
531
|
+
|
|
506
532
|
/**
|
|
507
533
|
* 鉴权失败(401/403)时被拒绝的源主机名:从 `ERR_PNPM_FETCH_401 GET https://host/pkg: Unauthorized - 401`
|
|
508
534
|
* 里取,取不到返回 null(提示文案退回通用说法,不拼出残缺句子)。
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { HubPlugin } from '../types.ts'
|
|
13
13
|
import { HUB_REPO } from './constants.ts'
|
|
14
14
|
import { repoFromInstallTarget } from './install-command.ts'
|
|
15
|
+
import { compareVersions } from './release-age.ts'
|
|
15
16
|
|
|
16
17
|
/** 安装时记录的目录信号(versions 表项)。 */
|
|
17
18
|
export interface InstalledVersionSignal {
|
|
@@ -90,7 +91,7 @@ export function installedNameOf(
|
|
|
90
91
|
export function installedVersionOf(plugin: HubPlugin, versions: Record<string, InstalledVersionSignal>): string | null {
|
|
91
92
|
const repo = plugin.source?.repo
|
|
92
93
|
if (!repo) return null
|
|
93
|
-
return versions
|
|
94
|
+
return signalOf(repo, versions)?.version || null
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/** 该插件对应的 npm 包名(目录下发,或安装时持久化的映射);无则 null。决定更新是否走 npm registry。 */
|
|
@@ -104,6 +105,8 @@ export function npmPackageOf(plugin: HubPlugin, versions: Record<string, Install
|
|
|
104
105
|
* 是否有更新:仅对已安装插件有意义。
|
|
105
106
|
* 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
|
|
106
107
|
* (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
|
|
108
|
+
* 版本用「目录 > 已装」判断,不做 != 比较:目录可能滞后于 npm,若直接用不等,
|
|
109
|
+
* 会把「已装 1.1.10、目录还写 1.1.8」判成有更新,反而引导用户降级。
|
|
107
110
|
*/
|
|
108
111
|
export function hasUpdateOf(
|
|
109
112
|
plugin: HubPlugin,
|
|
@@ -115,9 +118,13 @@ export function hasUpdateOf(
|
|
|
115
118
|
if (!repo) return false
|
|
116
119
|
const rec = versions[repo.toLowerCase()]
|
|
117
120
|
if (!rec) return false
|
|
118
|
-
|
|
121
|
+
// 账本记录可能是残缺的:安装一开始就会先写入 npm 包名映射,版本要等任务结束才知道。
|
|
122
|
+
// 缺版本的记录按「无版本信号」处理、退回更新时间比较 —— 直接拿去比版本会让整个插件区块崩掉
|
|
123
|
+
const recorded = typeof rec.version === 'string' ? rec.version : ''
|
|
124
|
+
if (plugin.version && recorded !== '') return compareVersions(plugin.version, recorded) > 0
|
|
119
125
|
const current = plugin.dates?.repoUpdatedAt
|
|
120
|
-
|
|
126
|
+
const seen = typeof rec.updatedAt === 'string' ? rec.updatedAt : ''
|
|
127
|
+
return Boolean(current && seen && current > seen)
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
/** 依赖 spec 能否解析出仓库身份(owner/repo 形态才算)。 */
|
|
@@ -219,7 +226,7 @@ export function installedItemsOf(
|
|
|
219
226
|
plugin: p,
|
|
220
227
|
repo,
|
|
221
228
|
installPath: paths?.[name] ?? null,
|
|
222
|
-
installedVersion: rec?.version
|
|
229
|
+
installedVersion: rec?.version || null,
|
|
223
230
|
installedAt: rec?.installedAt ?? null,
|
|
224
231
|
catalogVersion: p.version ?? null,
|
|
225
232
|
hasUpdate: hasUpdateOf(p, installed, versions),
|
|
@@ -242,7 +249,7 @@ export function installedItemsOf(
|
|
|
242
249
|
plugin: null,
|
|
243
250
|
repo,
|
|
244
251
|
installPath: paths?.[name] ?? null,
|
|
245
|
-
installedVersion: rec?.version
|
|
252
|
+
installedVersion: rec?.version || null,
|
|
246
253
|
installedAt: rec?.installedAt ?? null,
|
|
247
254
|
catalogVersion: null,
|
|
248
255
|
// 目录外没有目录信号可比,无法判断是否有更新
|
|
@@ -19,19 +19,67 @@ export function isReleaseTooFresh(publishedAt: string | null | undefined, now: n
|
|
|
19
19
|
return now - ms < MIN_RELEASE_AGE_MS
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** 版本号比较(逐段数字;缺失段按 0)。返回负数/0/正数,非法输入按相等处理。
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
/** 版本号比较(逐段数字;缺失段按 0)。返回负数/0/正数,非法输入按相等处理。
|
|
23
|
+
* 兼容目录里的 `v1.1.8` 与 npm 的 `1.1.8`:每段先剥掉前导非数字,再取整。
|
|
24
|
+
* 预发布后缀单独处理:`1.0.0` 高于 `1.0.0-beta.1`,两个预发布版按标识符比较
|
|
25
|
+
* (纯数字段按数值,数字段低于字母段)。 */
|
|
26
|
+
export function compareVersions(a: string | null | undefined, b: string | null | undefined): number {
|
|
27
|
+
const core = (v: string | null | undefined): number[] =>
|
|
28
|
+
(v ?? '').split('+')[0]!.split('-')[0]!
|
|
29
|
+
.split('.')
|
|
30
|
+
.map((part) => Number.parseInt(part.replace(/^[^\d]+/, ''), 10))
|
|
31
|
+
.filter((n) => !Number.isNaN(n))
|
|
32
|
+
const pre = (v: string | null | undefined): string[] => {
|
|
33
|
+
const rest = (v ?? '').split('+')[0]!.split('-').slice(1)
|
|
34
|
+
return rest.length === 0 ? [] : rest.join('-').split('.')
|
|
35
|
+
}
|
|
36
|
+
const va = core(a)
|
|
37
|
+
const vb = core(b)
|
|
27
38
|
const len = Math.max(va.length, vb.length)
|
|
28
39
|
for (let i = 0; i < len; i++) {
|
|
29
40
|
const d = (va[i] ?? 0) - (vb[i] ?? 0)
|
|
30
41
|
if (d !== 0) return d
|
|
31
42
|
}
|
|
43
|
+
const pa = pre(a)
|
|
44
|
+
const pb = pre(b)
|
|
45
|
+
if (pa.length === 0 && pb.length === 0) return 0
|
|
46
|
+
if (pa.length === 0) return 1
|
|
47
|
+
if (pb.length === 0) return -1
|
|
48
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
49
|
+
const x = pa[i]
|
|
50
|
+
const y = pb[i]
|
|
51
|
+
if (x === undefined) return -1
|
|
52
|
+
if (y === undefined) return 1
|
|
53
|
+
const nx = /^\d+$/.test(x) ? Number(x) : null
|
|
54
|
+
const ny = /^\d+$/.test(y) ? Number(y) : null
|
|
55
|
+
if (nx !== null && ny !== null) {
|
|
56
|
+
if (nx !== ny) return nx - ny
|
|
57
|
+
} else if (x !== y) {
|
|
58
|
+
return x < y ? -1 : 1
|
|
59
|
+
}
|
|
60
|
+
}
|
|
32
61
|
return 0
|
|
33
62
|
}
|
|
34
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 是否值得提示「可更新」:npm 上最新可安装版本确实比已装版本新,且已过供应链门禁。
|
|
66
|
+
*
|
|
67
|
+
* 更新命令执行的是 `pkg@latest`,所以「目录里标的版本」不是最终能装上的版本:
|
|
68
|
+
* 作者可能只打了 GitHub release 没发 npm —— 此时提示会变成永远点不掉的循环。
|
|
69
|
+
* 任一信息缺失(查不到发布信息 / 没有已装版本记录)返回 true,交回调用方原有判定:
|
|
70
|
+
* 宁可提示,也不因为一次查询失败把真实更新吃掉。
|
|
71
|
+
*/
|
|
72
|
+
export function isUpdateOffered(
|
|
73
|
+
release: { version: string; publishedAt: string } | null,
|
|
74
|
+
installedVersion: string | null,
|
|
75
|
+
now: number,
|
|
76
|
+
): boolean {
|
|
77
|
+
if (release === null) return true
|
|
78
|
+
if (isReleaseTooFresh(release.publishedAt, now)) return false
|
|
79
|
+
if (installedVersion !== null && compareVersions(release.version, installedVersion) <= 0) return false
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
35
83
|
/**
|
|
36
84
|
* 选出本次真正该提示的更新版本。
|
|
37
85
|
*
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -46,10 +46,12 @@ function reasonTitleOf(kind: FailureKind, action: FailureAction): string {
|
|
|
46
46
|
const word = actionWordEn(action)
|
|
47
47
|
switch (kind) {
|
|
48
48
|
case 'npmTooOld': return `npm too old for this ${word}`
|
|
49
|
+
case 'nodeEngine': return `Node / pnpm on the user machine is below the version this ${word} requires`
|
|
50
|
+
case 'profileState': return `the user profile's pnpm metadata does not match the local pnpm for this ${word}`
|
|
49
51
|
case 'dshMissing': return 'dsh command not found on the user machine'
|
|
50
52
|
case 'pnpmMissing': return 'pnpm command not found on the user machine'
|
|
51
53
|
case 'pnpmStore': return 'pnpm store version mismatch on the user machine'
|
|
52
|
-
case '
|
|
54
|
+
case 'pnpmModulesMismatch': return 'the profile modules directory was built with a different pnpm config on the user machine'
|
|
53
55
|
case 'pnpmWorkspace': return `pnpm workspace-root check blocked the ${word} on the user machine`
|
|
54
56
|
case 'originRejected': return 'request origin rejected by the local service on the user machine'
|
|
55
57
|
case 'pnpmPolicy': return `pnpm supply-chain policy blocked the ${word} on the user machine`
|
|
@@ -66,8 +68,10 @@ function reasonTitleOf(kind: FailureKind, action: FailureAction): string {
|
|
|
66
68
|
case 'pluginPrepare': return 'plugin distribution incomplete'
|
|
67
69
|
case 'hostIncompatible': return 'plugin does not support the DSH version in use'
|
|
68
70
|
case 'network': return 'network failure on the user side'
|
|
71
|
+
case 'tlsTrust': return `TLS certificate verification failed on the user machine during the ${word}`
|
|
69
72
|
case 'hostCliSpawn': return 'the DSH host could not be launched on the user machine'
|
|
70
73
|
case 'hubState': return `stale ${word} state in the plugin hub`
|
|
74
|
+
case 'profileBundleMissing': return 'the DSH profile is missing a bundle it declares'
|
|
71
75
|
case 'timeout': return `${word} timed out`
|
|
72
76
|
case 'profileLock': return 'DSH profile is locked by another process'
|
|
73
77
|
case 'pluginMissingDep': return 'a dependency of the plugin is not published on npm'
|
|
@@ -106,12 +110,16 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
106
110
|
? 'a dependency declared by the plugin is not published on the public npm registry (missing, removed, or never released); please publish or correct that dependency, or ship it with the plugin'
|
|
107
111
|
: kind === 'network'
|
|
108
112
|
? 'network connectivity issue on the user side (DNS, proxy, firewall, or a blocked connection)'
|
|
113
|
+
: kind === 'tlsTrust'
|
|
114
|
+
? 'the local certificate chain is not trusted by Node/pnpm (corporate SSL inspection, antivirus web shield, or a local MITM proxy) — local environment issue'
|
|
109
115
|
: kind === 'localEnv'
|
|
110
116
|
? 'no evidence of a plugin-side cause; the log shows local-environment signals (system error codes, certificates/proxy, or a broken local config), so this is reported for triage of the classifier only'
|
|
111
117
|
: kind === 'dshTooOld'
|
|
112
118
|
? 'the plugin declares a minimum DSH version this host does not meet — upgrade DSH first (local environment issue)'
|
|
113
|
-
: kind === '
|
|
114
|
-
? 'the profile node_modules was created with a different
|
|
119
|
+
: kind === 'pnpmModulesMismatch'
|
|
120
|
+
? 'the profile node_modules was created with a different pnpm config (hoisting / virtual store settings), so pnpm refuses to reuse it (local environment issue)'
|
|
121
|
+
: kind === 'profileBundleMissing'
|
|
122
|
+
? 'the profile declares a bundle that is not installed in it (interrupted install or incomplete app update), so the host aborts before installing anything (local environment issue)'
|
|
115
123
|
: kind === 'registryAuth'
|
|
116
124
|
? 'the npm registry rejected the request with 401/403 — the configured registry needs credentials or blocks this network (local environment issue)'
|
|
117
125
|
: kind === 'profileExclusive'
|
|
@@ -13,6 +13,8 @@ export interface InstallResult {
|
|
|
13
13
|
error: string | null
|
|
14
14
|
stdout: string
|
|
15
15
|
stderr: string
|
|
16
|
+
/** 实际执行安装用的 npm 源(换源重试后是最终那一个):后续读元数据的注解要跟它同源 */
|
|
17
|
+
registry?: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface InstallTask {
|
|
@@ -46,6 +48,9 @@ export interface InstallTask {
|
|
|
46
48
|
needsReload?: boolean
|
|
47
49
|
/** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
|
|
48
50
|
bundleDeselected?: boolean
|
|
51
|
+
/** 安装/更新成功后,从 profile 的 node_modules 读到的**实际**版本。
|
|
52
|
+
* 客户端用它写版本账本(目录版本可能滞后,抄目录字段会让「有更新」永久失真)。 */
|
|
53
|
+
installedVersion?: string
|
|
49
54
|
/** 卸载入队时该包是否带客户端 UI(`dsh.client`):卸载后包已从磁盘删除,只能入队前快照,
|
|
50
55
|
* 卸载成功且曾存活时据此决定是否需要刷新页面摘除插件面板。 */
|
|
51
56
|
hadClientUi?: boolean
|
|
@@ -89,7 +89,7 @@ export function backfillOperationLog(output: string, profileDir?: string): strin
|
|
|
89
89
|
export function hasSubstantiveOutput(output: string): boolean {
|
|
90
90
|
return output.split(/\r?\n/).some((raw) => {
|
|
91
91
|
const line = raw.trim()
|
|
92
|
-
return line !== '' && !/^\[(?:exit\b|timed out\]|install-channel\]|stale pipe\]|error\]|pnpm log\])/.test(line)
|
|
92
|
+
return line !== '' && !/^\[(?:exit\b|timed out\]|install-channel\]|stale pipe\]|error\]|pnpm log\]|registry-fallback\]|allow build\])/.test(line)
|
|
93
93
|
})
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -71,11 +71,16 @@ export function userNpmRegistry(cwd: string): string {
|
|
|
71
71
|
/**
|
|
72
72
|
* 传输层失败才值得换源:超时、连接重置、DNS、5xx。
|
|
73
73
|
* 404 / 版本不存在 / 鉴权失败必须保持原样 —— 换源只会掩盖作者侧问题或装到滞后版本。
|
|
74
|
+
* 本机证书链不被信任(TLS 校验失败)同样不换源:换哪个公共镜像都会以同样方式失败,
|
|
75
|
+
* 白白多等两轮超时、把真正的根因淹进换源噪音里。
|
|
74
76
|
*/
|
|
75
77
|
export function isTransportFailure(text: string): boolean {
|
|
76
78
|
if (/\b(?:ERR_PNPM_FETCH_404|E404|ERR_PNPM_NO_MATCHING_VERSION|ETARGET|ERR_PNPM_FETCH_401|ERR_PNPM_FETCH_403|E401|E403)\b/i.test(text)) {
|
|
77
79
|
return false
|
|
78
80
|
}
|
|
81
|
+
if (/unable to verify the first certificate|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE|ERR_TLS_CERT_ALTNAME_INVALID|CERT_HAS_EXPIRED|CERT_UNTRUSTED|UNABLE_TO_GET_ISSUER_CERT/i.test(text)) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
79
84
|
return /\b(?:ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|EPIPE)\b|socket hang up|getaddrinfo|network request failed|connect timeout|request to .+ failed|FetchError|ERR_PNPM_FETCH_5\d\d|ERR_PNPM_META_FETCH_FAIL/i.test(text)
|
|
80
85
|
}
|
|
81
86
|
|