dsh-plugin 1.6.10 → 1.6.12
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 +430 -360
- package/client/client.js.map +1 -1
- package/lib/services/install/registry-check.js +78 -0
- package/lib/services/install/task-queue.js +49 -5
- package/lib/types/services/install/registry-check.d.ts +15 -0
- package/lib/types/services/install/task-queue.d.ts +7 -1
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +6 -5
- package/src/client/components/modals/NotificationsModal.tsx +35 -9
- package/src/client/components/modals/modals.tsx +43 -17
- package/src/client/locales.ts +32 -14
- package/src/client/logic/failures.ts +36 -2
- package/src/client/logic/urls.ts +40 -31
- package/src/server/services/install/registry-check.ts +83 -0
- package/src/server/services/install/task-queue.ts +49 -5
package/src/client/locales.ts
CHANGED
|
@@ -308,6 +308,10 @@ export const zh = {
|
|
|
308
308
|
errorTitleUninstall: '卸载失败',
|
|
309
309
|
// 更新失败(通知中心区分安装/更新):更新目标覆盖重装失败,与首次安装失败不同语义
|
|
310
310
|
errorTitleUpdate: '更新失败',
|
|
311
|
+
// 动作词:失败提示与上报正文统一取词,不写死「安装」
|
|
312
|
+
actionInstall: '安装',
|
|
313
|
+
actionUpdate: '更新',
|
|
314
|
+
actionUninstall: '卸载',
|
|
311
315
|
errorPlugin: '插件',
|
|
312
316
|
errorClose: '知道了',
|
|
313
317
|
errorCopy: '复制错误信息',
|
|
@@ -319,12 +323,14 @@ export const zh = {
|
|
|
319
323
|
failHubIssueHint: '此失败已被判定为环境或插件中心自身问题(非插件作者问题),带着错误日志到插件中心仓库一键提交 Issue',
|
|
320
324
|
failHubIssueBig: '反馈给插件中心排查',
|
|
321
325
|
// 未知失败兜底:不归到插件作者,引导到插件中心仓库排查
|
|
322
|
-
failUnclassifiedHint: '
|
|
326
|
+
failUnclassifiedHint: '{action}失败,原因未能自动识别。这通常不是插件自身的问题(可能与本机环境或插件中心有关),请将错误信息反馈给插件中心,由维护者排查。',
|
|
323
327
|
// 插件中心自身状态类消息(插件未安装 / 不在此处卸载 / 已在队列中):已安装列表与实读快照不同步,非插件问题,不引导提 Issue
|
|
324
|
-
failHubStateHint: '
|
|
325
|
-
//
|
|
326
|
-
failTimeoutHint: '
|
|
327
|
-
failProfileLockHint: 'DSH 在写入插件清单时等不到文件锁、超时中止 —— 通常是同时有另一个 DSH 进程或安装任务在写同一个 profile(也可能是上一次的进程没退干净留下的锁)。请先关掉其他 dsh 窗口或正在跑的命令,再重试一次;若反复出现,重启一次 DSH
|
|
328
|
+
failHubStateHint: '插件列表状态已过期或与本地不一致:这次{action}的目标可能已经在列表里了(要升级请点「更新」),也可能已经不在已安装列表中,或已在队列中。这不是插件本身的问题 —— 请刷新页面后重试。',
|
|
329
|
+
// 超时被强杀,可能其实已生效:给提示,不引导提 Issue。措辞动作中性(卸载场景说「已经装上了」不通)
|
|
330
|
+
failTimeoutHint: '本次操作耗时超过本地上限、已被中断 —— 但很可能其实已经生效了(安装器在超时后会先核对本地已安装列表)。请刷新页面确认当前状态;若确实没生效,可在终端手动执行一次对应命令查看完整输出。这不是插件本身的问题。',
|
|
331
|
+
failProfileLockHint: 'DSH 在写入插件清单时等不到文件锁、超时中止 —— 通常是同时有另一个 DSH 进程或安装任务在写同一个 profile(也可能是上一次的进程没退干净留下的锁)。请先关掉其他 dsh 窗口或正在跑的命令,再重试一次;若反复出现,重启一次 DSH。这不是插件本身的问题,也不需要重复{action}。',
|
|
332
|
+
// 宿主自身入口起不来:环境问题,给提示、不引导提 Issue
|
|
333
|
+
failHostCliSpawnHint: 'DSH 没能启动自身来完成这次操作(系统报「找不到可执行文件」):通常是因为 DSH 的安装目录被移动或正在更新,或该 profile 目录还不存在。请重启一次 DSH 后重试;若反复出现,重新安装 DSH。这不是插件本身的问题,换别的插件也一样。',
|
|
328
334
|
// prepare 构建脚本实际执行失败(git 分发缺子模块/构建产物):插件打包分发问题,应提 Issue
|
|
329
335
|
failPrepareHint: '该插件安装时的构建脚本执行失败(git 分发常缺失子模块或构建产物),属插件打包分发问题。请向作者仓库提交 Issue 反馈。',
|
|
330
336
|
// 「插件打包问题」分支的次要入口:该判据由插件中心自己的预检/装后校验产出(不是 pnpm 原文),
|
|
@@ -334,12 +340,12 @@ export const zh = {
|
|
|
334
340
|
// 安装前预检 / 装后校验拦截([packaging]):git 分发缺插件需要的文件(入口 / 补丁 mount 的入口 / bundle 补丁)= 插件未适配官方默认安装方式
|
|
335
341
|
failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少它需要的文件(`main` / `exports` 声明的入口、补丁按本包名 mount 的入口,或 `dsh.bundle.patch` 声明的补丁),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物、改用预构建 release `.tgz` 直链(`releases/download/<tag>/<asset>.tgz`),或发布完整的 npm 版。',
|
|
336
342
|
// 原生依赖构建脚本被 pnpm 默认拦截(如 node-pty 的 ERR_PNPM_IGNORED_BUILDS):不单独算插件问题,引导到插件中心仓库排查
|
|
337
|
-
failIgnoredBuild: '该插件(或其依赖)需要在安装时执行构建脚本,被 pnpm 的安全白名单(allowBuilds
|
|
343
|
+
failIgnoredBuild: '该插件(或其依赖)需要在安装时执行构建脚本,被 pnpm 的安全白名单(allowBuilds)默认拦截,本次操作因此失败(如原生模块 node-pty,或 git 安装时插件自己的 prepare 脚本)。安装器已自动放行该 key 并重试过一次;仍失败通常说明构建本身没通过。可先在终端手动执行一次安装命令查看完整构建错误;若确属插件的构建/分发问题,建议作者预置构建产物、发布 npm 包或预构建 .tgz。需要协助排查时也可反馈给插件中心。',
|
|
338
344
|
// npm arborist 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题,升级 npm 即可
|
|
339
345
|
failNpmTooLow: '怀疑本机 npm 版本过低:npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
340
346
|
failNpmTooLowV: '本机 npm 版本过低(当前 v{v}):npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
341
347
|
// 安装前连通性预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):本机网络或代理问题,不是插件问题,不引导提 Issue
|
|
342
|
-
failNetworkHint: '
|
|
348
|
+
failNetworkHint: '{action}前检测到网络不通:无法连接到安装源(可能是断网、DNS 解析失败、被防火墙拦截,或代理配置有问题)。这不是插件本身的问题 —— 请检查网络连接,或到「设置」里确认 HTTP 代理配置,再到「系统诊断」运行一次连通性检测,然后重试。',
|
|
343
349
|
// 网络失败弹窗里的「无法访问 …」醒目行:直接告诉用户具体哪个地址连不上({url} 为提取的不可达地址)
|
|
344
350
|
failNetworkRegistryHint: '你的 npm/pnpm 源(registry)被配置为 {host},插件包需从该源下载但失败了。这通常是内网/自定义源没有同步该包(或需要内网认证)—— 请把 npm/pnpm 的 registry 指回官方源 https://registry.npmjs.org(或确认此源可访问该包)后重试。',
|
|
345
351
|
failNetworkTarget: '无法访问 {url} —— 你的网络连接不通',
|
|
@@ -357,6 +363,9 @@ export const zh = {
|
|
|
357
363
|
// 404 依赖:点名 registry 上不存在的那个包(取不到包名时跳过这行),再给删除条目的指引
|
|
358
364
|
failPnpmMissingDepTarget: '{pkg} 在 npm registry 上不存在(404)',
|
|
359
365
|
failPnpmMissingDepHint: '安装器调用 pnpm 时报「包不存在」(ERR_PNPM_FETCH_404):你的 profile 里留着一条指向 registry 上未发布包的依赖,pnpm 每次解析依赖都会先卡在它上面,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 package.json 里删除该依赖条目(同时删掉对应的 pnpm-lock.yaml 条目或整个锁文件)后重试。',
|
|
366
|
+
failRegistryMirrorLagHint: '安装器调用 pnpm 时报「包不存在」(ERR_PNPM_FETCH_404):该包在官方 npm 源上是有的,但你本机配置的 npm/pnpm 源(registry 或镜像)没有同步到它。请把 npm/pnpm 的 registry 指回官方源 https://registry.npmjs.org(或换一个已同步该包的镜像)后重试。这不是插件本身的问题。',
|
|
367
|
+
failPluginMissingDepTarget: '{pkg} 在官方 npm 源上不存在',
|
|
368
|
+
failPluginMissingDepHint: '该插件依赖的包在官方 npm 源上并不存在(已删除或从未发布):这是插件作者侧的依赖问题,不是你环境的问题。请向插件作者仓库反馈,请作者补上或修正该依赖,或改为随插件一起分发。',
|
|
360
369
|
failRegistryVersionStaleHint: '安装器调用 pnpm 时报「找不到匹配的版本」(ERR_PNPM_NO_MATCHING_VERSION):要装的这个版本在实际执行安装的 npm 源里查不到。常见原因是你设置的 npmRegistry 与本机 ~/.npmrc 用的不是同一个源——官方源刚发布、镜像还没同步上来(快则几分钟、慢则几小时),也可能只是版本号写错了。这不是插件本身的问题。可在插件市场设置里把 npmRegistry 改成与本机 ~/.npmrc 一致(或反过来),或那一次显式走官方源(https://registry.npmjs.org)、改用离线包后重试;若该版本刚发布不久,等镜像同步后重试即可。',
|
|
361
370
|
failPnpmLinkedDirMissingHint: '安装器调用 pnpm 时报「要安装的目录不存在」(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND):你的 profile 里留着一条指向本机不存在目录的本地依赖(link: / file: 形式,例如用 dsh plugin add <本地路径> 装过开发中的插件,之后那个目录被删除或移动了)。pnpm 每次解析 profile 依赖都会先卡在它上面,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 package.json 里删除该本地依赖条目(同时删掉对应的 pnpm-lock.yaml 条目或整个锁文件),或把该目录恢复回原路径后重试。',
|
|
362
371
|
failFileLockedHint: '安装过程中 pnpm 无法替换 profile 里的文件:有程序正占用着它(Windows 常见报错 os error 32「另一个程序正在使用此文件」)。通常是 DeepSeek Harness 自身或杀毒软件的实时扫描持有文件句柄。这不是插件本身的问题——请完全退出 DeepSeek Harness(Windows 可在任务管理器确认进程已结束,必要时临时关闭杀毒软件实时防护)后重试。',
|
|
@@ -719,6 +728,10 @@ export const en = {
|
|
|
719
728
|
errorTitleUninstall: 'Removal failed',
|
|
720
729
|
// Update failed (notification center distinguishes install/update): update target reinstall failed
|
|
721
730
|
errorTitleUpdate: 'Update failed',
|
|
731
|
+
// Action words: failure hints and filed reports take their wording from here
|
|
732
|
+
actionInstall: 'install',
|
|
733
|
+
actionUpdate: 'update',
|
|
734
|
+
actionUninstall: 'removal',
|
|
722
735
|
errorPlugin: 'Plugin',
|
|
723
736
|
errorClose: 'Got it',
|
|
724
737
|
errorCopy: 'Copy error',
|
|
@@ -730,12 +743,14 @@ export const en = {
|
|
|
730
743
|
failHubIssueHint: 'This failure was classified as an environment or Plugin Hub issue (not a plugin-author issue). Open a pre-filled issue on the Plugin Hub repo with this error log.',
|
|
731
744
|
failHubIssueBig: 'Report to the Plugin Hub for triage',
|
|
732
745
|
// Unknown-failure fallback: not routed to the plugin author, but to the hub repo for triage
|
|
733
|
-
failUnclassifiedHint: 'The
|
|
746
|
+
failUnclassifiedHint: 'The {action} failed and the cause could not be identified automatically. This is usually not the plugin itself (it may relate to your local environment or the Plugin Hub). Please report the error to the Plugin Hub so the maintainers can triage it.',
|
|
734
747
|
// The hub's own state-guard messages (plugin not installed / cannot be uninstalled here / already queued / already installed): the installed list is out of sync with the live snapshot — not a plugin issue, no issue prompt
|
|
735
|
-
failHubStateHint: 'The plugin list is out of date or out of sync: this
|
|
736
|
-
// The
|
|
737
|
-
failTimeoutHint: '
|
|
738
|
-
failProfileLockHint: 'DSH timed out waiting for the file lock while writing the plugin list — usually because another DSH process or install task is writing the same profile (sometimes a previous run left the lock behind). Close other dsh windows or running commands, then retry; if it keeps happening, restart DSH once. This is not a plugin problem, and
|
|
748
|
+
failHubStateHint: 'The plugin list is out of date or out of sync: the target of this {action} may already be in the list (use "Update" to upgrade), may no longer be in the installed list, or may already be queued. This is not a plugin problem — refresh the page and retry.',
|
|
749
|
+
// The command hit the local time limit and was killed: it may well have taken effect already (the installer re-checks the installed list after a timeout) — actionable hint, no issue prompt
|
|
750
|
+
failTimeoutHint: 'This operation exceeded the local time limit and was interrupted — but it may well have taken effect already (the installer re-checks the installed list after a timeout). Refresh the page to confirm the current state; if it did not take effect, run the corresponding command in a terminal to see the full output. This is not a plugin problem.',
|
|
751
|
+
failProfileLockHint: 'DSH timed out waiting for the file lock while writing the plugin list — usually because another DSH process or install task is writing the same profile (sometimes a previous run left the lock behind). Close other dsh windows or running commands, then retry; if it keeps happening, restart DSH once. This is not a plugin problem, and there is no need to repeat the {action}.',
|
|
752
|
+
// The host could not launch itself to run the operation: not a plugin issue — hint only, no issue prompt
|
|
753
|
+
failHostCliSpawnHint: 'DSH could not start itself to perform this operation (the system reported that the executable was not found). This usually means the DSH installation folder has been moved or is being updated, or that the profile directory does not exist yet. Restart DSH and retry; if it keeps happening, reinstall DSH. This is not a plugin problem — other plugins would fail the same way.',
|
|
739
754
|
// prepare script actually failed (git tarball misses submodules/build output): a packaging issue of the plugin itself — file an issue
|
|
740
755
|
failPrepareHint: 'This plugin failed while running its build scripts during install (git tarballs often miss submodules or build output). This is a packaging issue of the plugin itself — please report it to the author repository.',
|
|
741
756
|
// Secondary entry on the "plugin packaging" branch: that verdict comes from the hub's own
|
|
@@ -746,11 +761,11 @@ export const en = {
|
|
|
746
761
|
// Pre-install / post-install guard hit ([packaging]): git distribution lacks a file the package needs (entry / entry a patch mounts / bundle patch) = not adapted to the official default install method
|
|
747
762
|
failPackagingHint: 'This plugin does not support the official default install method: its git distribution lacks a file the package needs (the entry from `main` / `exports`, an entry its own patch mounts by name, or the patch declared by `dsh.bundle.patch`), so it has not been adapted to the official install flow. Please file an Issue on the author\'s repo asking for committed build output, a prebuilt release `.tgz` (`releases/download/<tag>/<asset>.tgz`), or a complete npm release.',
|
|
748
763
|
// native-module build scripts ignored by pnpm by default (e.g. node-pty, ERR_PNPM_IGNORED_BUILDS): not a plugin-side issue on its own, route to the hub repo for triage
|
|
749
|
-
failIgnoredBuild: 'This plugin (or one of its dependencies) needs to run build scripts at install time, which pnpm blocks by default via its allowBuilds allowlist — so
|
|
764
|
+
failIgnoredBuild: 'This plugin (or one of its dependencies) needs to run build scripts at install time, which pnpm blocks by default via its allowBuilds allowlist — so this operation fails (e.g. native modules like node-pty, or the plugin\'s own prepare script on git installs). The installer already auto-allows that key and retries once; if it still fails, the build itself is usually the problem. Run the install command in a terminal to see the full build error; if it is indeed the plugin\'s build or distribution, the author can ship prebuilt output, an npm release, or a prebuilt .tgz. You can also report it to the Plugin Hub for triage.',
|
|
750
765
|
failNpmTooLow: 'Your local npm may be too old: npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
751
766
|
failNpmTooLowV: 'Your local npm is too old (v{v}): npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
752
767
|
// Pre-install connectivity probe / connection failure in the install log ([network] / git fetch timeout / DNS / TLS): a local network or proxy issue, not a plugin issue — no issue link
|
|
753
|
-
failNetworkHint: 'The network could not be reached before
|
|
768
|
+
failNetworkHint: 'The network could not be reached before the {action}: the install source is unreachable (offline, DNS failure, firewall block, or a misconfigured proxy). This is not a problem with the plugin itself — please check your connection, confirm the HTTP proxy in Settings, run a connectivity check under System Diagnostics, then retry.',
|
|
754
769
|
// Prominent "cannot reach …" line in the network-failure dialog: shows the exact unreachable address ({url})
|
|
755
770
|
failNetworkRegistryHint: 'Your npm/pnpm registry is set to {host}, and the package tarball could not be downloaded from it. This usually means a private/internal mirror that has not synced the package (or requires intranet auth) — point the registry back to https://registry.npmjs.org (or make sure this source can serve the package), then retry.',
|
|
756
771
|
failNetworkTarget: 'Cannot reach {url} — your network connection is down',
|
|
@@ -770,6 +785,9 @@ export const en = {
|
|
|
770
785
|
// then give the removal instructions
|
|
771
786
|
failPnpmMissingDepTarget: '{pkg} was not found on the npm registry (404)',
|
|
772
787
|
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.',
|
|
788
|
+
failRegistryMirrorLagHint: 'pnpm reported that a package does not exist on the npm registry (ERR_PNPM_FETCH_404): that package does exist on the official npm registry, but the npm/pnpm registry (or mirror) configured on your machine has not synced it. Point the npm/pnpm registry back at the official one (https://registry.npmjs.org), or switch to a mirror that has the package, then retry. This is not a problem with the plugin itself.',
|
|
789
|
+
failPluginMissingDepTarget: '{pkg} does not exist on the official npm registry',
|
|
790
|
+
failPluginMissingDepHint: 'A package this plugin depends on does not exist on the official npm registry (removed, or never published). This is a dependency problem on the plugin author side, not a problem with your environment. Please report it to the plugin repository so the author can fix that dependency or ship it together with the plugin.',
|
|
773
791
|
failRegistryVersionStaleHint: 'pnpm reported that no matching version was found (ERR_PNPM_NO_MATCHING_VERSION): the version being installed does not exist in the npm registry that actually performs the install. This usually means the npmRegistry configured in the hub differs from the source in your local ~/.npmrc — the official registry has published it but the mirror has not synced yet (it can take minutes to hours) — or the version number was simply mistyped. This is not a problem with the plugin itself. Align the hub\'s npmRegistry with your local ~/.npmrc (or the other way round), or explicitly use the official registry (https://registry.npmjs.org) for that one install, or use an offline package, then retry; if the version was published only recently, waiting for the mirror to sync is enough.',
|
|
774
792
|
failPnpmLinkedDirMissingHint: 'pnpm reported that a directory it needs to install from does not exist (ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND): your profile keeps a local dependency (a link: / file: entry, e.g. from `dsh plugin add <local path>`) whose target directory has since been deleted or moved. pnpm hits it every time it resolves profile dependencies, so no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that local dependency from package.json inside the profile directory (~/.dsh/profiles/<profile>) — along with its entry in pnpm-lock.yaml, or the lockfile itself — or restore the directory to its original path, then retry.',
|
|
775
793
|
failFileLockedHint: 'During install, pnpm could not replace a file inside the profile because another program is holding it (on Windows this shows as os error 32, "the process cannot access the file because it is being used by another process"). Usually DeepSeek Harness itself or a real-time antivirus scan holds the handle. This is not a problem with the plugin itself — fully quit DeepSeek Harness (on Windows, confirm in Task Manager that the process has ended, and temporarily disable real-time antivirus protection if needed), then retry.',
|
|
@@ -151,7 +151,26 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
151
151
|
return next
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hostCliSpawn' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
|
|
155
|
+
|
|
156
|
+
/** 失败动作口径:标题、上报正文与提示文案按安装/更新/卸载取词,不写死「安装」。 */
|
|
157
|
+
export type FailureAction = 'install' | 'update' | 'uninstall'
|
|
158
|
+
|
|
159
|
+
/** 动作词(英文):名词短语,标题与句子里都成立。 */
|
|
160
|
+
export function actionWordEn(action: FailureAction): string {
|
|
161
|
+
return action === 'uninstall' ? 'removal' : action === 'update' ? 'update' : 'install'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 动作词(中文):安装 / 更新 / 卸载。 */
|
|
165
|
+
export function actionWordZh(action: FailureAction): string {
|
|
166
|
+
return action === 'uninstall' ? '卸载' : action === 'update' ? '更新' : '安装'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 记录的动作:老记录只有 kind,按 kind 兜底。 */
|
|
170
|
+
export function actionOfRecord(rec: { kind: NotificationRecord['kind']; action?: FailureAction }): FailureAction {
|
|
171
|
+
if (rec.action !== undefined) return rec.action
|
|
172
|
+
return rec.kind === 'uninstall' ? 'uninstall' : rec.kind === 'update' ? 'update' : 'install'
|
|
173
|
+
}
|
|
155
174
|
|
|
156
175
|
/** Match the pnpm error code, not a generic mention of checksum or integrity. */
|
|
157
176
|
function missingTarballIntegrity(message: string): boolean {
|
|
@@ -230,6 +249,8 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
230
249
|
// 装后校验发现入口文件缺失([packaging])= git 分发缺构建产物
|
|
231
250
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
232
251
|
// registry 上不存在的依赖(404),profile 任何安装都会卡在这条。必须在 pluginPrepare 之前判
|
|
252
|
+
// [registry-missing](服务端官方源复核后注入)= 作者侧缺依赖,须先于 pnpmMissingDep(同样含 404 文本)
|
|
253
|
+
if (/\[registry-missing\]/i.test(message)) return 'pluginMissingDep'
|
|
233
254
|
if (/ERR_PNPM_FETCH_404|404 Not Found|Not Found - 404/i.test(message)) return 'pnpmMissingDep'
|
|
234
255
|
// 要装的版本在实际安装源里没有(镜像滞后或版本写错)。必须在 pluginPrepare 之前判
|
|
235
256
|
if (/\bERR_PNPM_NO_MATCHING_VERSION\b|\bETARGET\b|No matching version found/i.test(message)) return 'registryVersionStale'
|
|
@@ -252,6 +273,11 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
252
273
|
// dsh 进程或安装任务在写,锁一直没释放。既不是插件问题、也不是用户环境坏了 —— 把并发收掉重试即可。
|
|
253
274
|
// 摆放位置同样在一切错误码分支之后:日志里真有 pnpm 报错时仍以真实报错为准。
|
|
254
275
|
if (/atomic-write: timed out waiting for the writer lock|timed out waiting for the writer lock/i.test(message)) return 'profileLock'
|
|
276
|
+
// 宿主自己的 allowBuilds 摘要行(pnpm 原文只写进 diagnostics 日志文件,终端只剩这一句)
|
|
277
|
+
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'
|
|
278
|
+
// 宿主自身入口起不来(服务端 [host-spawn] 标记):环境问题,给提示、不引导提 Issue。
|
|
279
|
+
// 不匹配 `spawn … ENOENT` 文本 —— 那个形态任何 spawn(curl/git/taskkill…)都会产出
|
|
280
|
+
if (/\[host-spawn\]/i.test(message)) return 'hostCliSpawn'
|
|
255
281
|
// 其余未知失败:引导到插件中心仓库排查
|
|
256
282
|
return 'hub'
|
|
257
283
|
}
|
|
@@ -318,13 +344,21 @@ export function coreErrorCode(message: string): string | null {
|
|
|
318
344
|
return m ? m[0].replace(/^\[|\]$/g, '') : null
|
|
319
345
|
}
|
|
320
346
|
|
|
347
|
+
/** 「包在 registry 不存在」失败的提示变体:服务端用官方源复核过(会注入 `[registry-mirror-lag]`)
|
|
348
|
+
* 时,说明包在官方源上存在、是本机镜像源未同步 → 提示换源;否则沿用既有的「profile 残留依赖」
|
|
349
|
+
* 提示(#73:指向从未发布包的残留条目,任何插件都装不进来)。 */
|
|
350
|
+
export function registryMissingHintOf(message: string): 'failPnpmMissingDepHint' | 'failRegistryMirrorLagHint' {
|
|
351
|
+
return /\[registry-mirror-lag\]/i.test(message) ? 'failRegistryMirrorLagHint' : 'failPnpmMissingDepHint'
|
|
352
|
+
}
|
|
353
|
+
|
|
321
354
|
/** 提取「registry 上不存在的包名」:从 404 行(`ERR_PNPM_FETCH_404 GET <registry>/<pkg>: Not Found - 404`)
|
|
322
355
|
* 里取请求路径中的包名(含 scope,如 `@scope/name`);非 404 或提取不到返回 null。 */
|
|
323
356
|
export function missingRegistryPackageOf(message: string): string | null {
|
|
324
357
|
for (const line of message.split(/\r?\n/)) {
|
|
325
358
|
if (!/ERR_PNPM_FETCH_404|404/.test(line)) continue
|
|
326
359
|
const m = line.match(/GET\s+https?:\/\/[^\s/]+\/((?:@[^/\s:]+\/)?[^/\s:]+)/)
|
|
327
|
-
|
|
360
|
+
// pnpm 输出里 scope 包是 URL 编码形态(`@scope%2Fname`):还原成 `@scope/name` 再展示
|
|
361
|
+
if (m) return m[1].replace(/%40/gi, '@').replace(/%2f/gi, '/')
|
|
328
362
|
}
|
|
329
363
|
return null
|
|
330
364
|
}
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { EnvInfo, HubPlugin, LocaleId } from '../types.ts'
|
|
11
11
|
import { GITHUB_URL, HUB_REPO, PLUGIN_VERSION, SITE_URL } from './constants.ts'
|
|
12
|
-
import { MAX_CORE_CHARS, classifyFailure, coreErrorCode, pnpmPolicyHintOf, summarizeError } from './failures.ts'
|
|
12
|
+
import { MAX_CORE_CHARS, actionWordEn, actionWordZh, classifyFailure, coreErrorCode, pnpmPolicyHintOf, summarizeError } from './failures.ts'
|
|
13
13
|
import { executedCommandOf } from './install-command.ts'
|
|
14
|
-
import type { FailureKind } from './failures.ts'
|
|
14
|
+
import type { FailureAction, FailureKind } from './failures.ts'
|
|
15
15
|
|
|
16
16
|
/** dsh-plugin.org 中文页挂在 /zh/ 前缀下,英文在根路径。 */
|
|
17
17
|
export function langPathOf(lang: LocaleId): string {
|
|
@@ -41,18 +41,18 @@ export function pluginSiteUrl(repo: string): string {
|
|
|
41
41
|
return siteUrl(`plugins/${repo}`)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
/** 错误原因标题(对外英文):点明问题出在哪一侧,便于 issue 列表分流;
|
|
45
|
+
* 动作级原因(未分类/超时…)带动作词,卸载失败不写成 install failed。 */
|
|
46
|
+
function reasonTitleOf(kind: FailureKind, action: FailureAction): string {
|
|
47
|
+
const word = actionWordEn(action)
|
|
48
48
|
switch (kind) {
|
|
49
|
-
case 'npmTooOld': return
|
|
49
|
+
case 'npmTooOld': return `npm too old for this ${word}`
|
|
50
50
|
case 'dshMissing': return 'dsh command not found on the user machine'
|
|
51
51
|
case 'pnpmMissing': return 'pnpm command not found on the user machine'
|
|
52
52
|
case 'pnpmStore': return 'pnpm store version mismatch on the user machine'
|
|
53
|
-
case 'pnpmWorkspace': return
|
|
53
|
+
case 'pnpmWorkspace': return `pnpm workspace-root check blocked the ${word} on the user machine`
|
|
54
54
|
case 'originRejected': return 'request origin rejected by the local service on the user machine'
|
|
55
|
-
case 'pnpmPolicy': return
|
|
55
|
+
case 'pnpmPolicy': return `pnpm supply-chain policy blocked the ${word} on the user machine`
|
|
56
56
|
case 'pnpmUnusedPatch': return 'stale pnpm patch entry on the user machine'
|
|
57
57
|
case 'pnpmMissingDep': return 'dependency not found on the npm registry on the user machine'
|
|
58
58
|
case 'registryVersionStale': return 'requested version not available on the npm registry in use on the user machine'
|
|
@@ -63,11 +63,13 @@ function reasonTitleOf(kind: FailureKind): string {
|
|
|
63
63
|
case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
|
|
64
64
|
case 'pluginPrepare': return 'plugin distribution incomplete'
|
|
65
65
|
case 'network': return 'network failure on the user side'
|
|
66
|
-
case '
|
|
67
|
-
case '
|
|
66
|
+
case 'hostCliSpawn': return 'the DSH host could not be launched on the user machine'
|
|
67
|
+
case 'hubState': return `stale ${word} state in the plugin hub`
|
|
68
|
+
case 'timeout': return `${word} timed out`
|
|
68
69
|
case 'profileLock': return 'DSH profile is locked by another process'
|
|
69
|
-
case '
|
|
70
|
-
|
|
70
|
+
case 'pluginMissingDep': return 'a dependency of the plugin is not published on npm'
|
|
71
|
+
case 'hub': return `${word} failed (cause unclassified)`
|
|
72
|
+
default: return `plugin ${word} failed`
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -80,16 +82,21 @@ function reasonTitleOf(kind: FailureKind): string {
|
|
|
80
82
|
* attempts(尝试过的安装方式,npm 反查 + 实际执行命令):作者看到我们查过/试过的命令,
|
|
81
83
|
* 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
|
|
82
84
|
*/
|
|
83
|
-
export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
|
|
85
|
+
export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[], action: FailureAction = 'install'): string {
|
|
84
86
|
// 原因判定:按失败类型归类,便于作者判断问题是否出在插件侧。
|
|
85
87
|
// [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
|
|
86
88
|
// (package.json 声明的文件在发布物里缺失),作者照此补齐即可。
|
|
87
89
|
const kind = classifyFailure(message)
|
|
88
90
|
const executed = executedCommandOf(command, attempts)
|
|
91
|
+
// 动作词:标题与正文的「安装/更新/卸载」统一取自这里
|
|
92
|
+
const zhWord = actionWordZh(action)
|
|
89
93
|
// 标题 = 错误原因(非动作词),让 issue 列表一眼可分流
|
|
90
|
-
const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
|
|
94
|
+
const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind, action)}: ${repo}`
|
|
91
95
|
const reason = /\[packaging\]/i.test(message)
|
|
92
96
|
? 'plugin distribution is incomplete — a file the package needs (its entry from main / exports, an entry a bundled patch mounts, or the patch declared by dsh.bundle.patch) is missing from the published package (github tarball or npm package); please commit build output or publish a complete package'
|
|
97
|
+
: /\[registry-missing\]/i.test(message)
|
|
98
|
+
// 服务端用官方 registry 复核过:缺的包在官方源上也不存在,且不是用户 profile 的残留依赖
|
|
99
|
+
? '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'
|
|
93
100
|
: kind === 'network'
|
|
94
101
|
? 'network connectivity issue on the user side (DNS, proxy, firewall, or a blocked connection)'
|
|
95
102
|
: kind === 'pluginPrepare'
|
|
@@ -119,16 +126,15 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
119
126
|
`- Cause: ${reason}`,
|
|
120
127
|
...(code ? [`- Key error: \`${code}\``] : []),
|
|
121
128
|
'',
|
|
122
|
-
// 来源说明:标题(链官网)+ 一句来源(链仓库)+
|
|
123
|
-
`## [DSH Plugin 插件市场](${SITE_URL}) ·
|
|
124
|
-
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL})
|
|
125
|
-
`-
|
|
126
|
-
`-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// 查过/试过的命令就能直接指认
|
|
129
|
+
// 来源说明:标题(链官网)+ 一句来源(链仓库)+ 实际执行的命令(含 --profile)与执行结果,链接由常量动态拼接
|
|
130
|
+
`## [DSH Plugin 插件市场](${SITE_URL}) · ${zhWord} Plugin 失败错误信息`,
|
|
131
|
+
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件市场的安装程序自动生成,随本次${zhWord}失败一并提交。`,
|
|
132
|
+
`- 实际执行的${zhWord}命令:${executed ? `\`${executed}\`` : 'unknown (no executed command recorded)'}`,
|
|
133
|
+
`- 执行结果:${zhWord}失败,未能${zhWord}该插件。`,
|
|
134
|
+
// 尝试过的通道(npm 反查 + 实际执行命令,按先后顺序):作者据此指认正确的 npm 包名。
|
|
135
|
+
// 不写「安装」:卸载失败也走这条正文。
|
|
130
136
|
...(attempts && attempts.length > 0
|
|
131
|
-
? ['', '## Attempted
|
|
137
|
+
? ['', '## Attempted channels(已尝试的方式)', ...attempts.map((a) => `- ${a}`)]
|
|
132
138
|
: []),
|
|
133
139
|
// [packaging] 场景:说明三种官方安装通道(npm 包 / git 直装 / 预构建 release .tgz),当前插件在对应通道下分发不完整
|
|
134
140
|
...(/\[packaging\]/i.test(message)
|
|
@@ -206,19 +212,22 @@ export function hubIssueUrl(
|
|
|
206
212
|
attempts?: string[],
|
|
207
213
|
/** 见 HubIssueVariant;缺省 'triage',既有正文保持不变 */
|
|
208
214
|
variant: HubIssueVariant = 'triage',
|
|
215
|
+
/** 上报动作:标题与正文按它取词 */
|
|
216
|
+
action: FailureAction = 'install',
|
|
209
217
|
): string {
|
|
210
218
|
const kind = classifyFailure(message)
|
|
211
219
|
const executed = executedCommandOf(command, attempts)
|
|
220
|
+
const zhWord = actionWordZh(action)
|
|
212
221
|
const repo = pluginRepo && pluginRepo.trim() !== '' ? pluginRepo.trim() : 'unknown'
|
|
213
222
|
const title = variant === 'misjudge'
|
|
214
223
|
// 标题就点明「可能是误判」,维护者一眼能区分它和未分类兜底
|
|
215
224
|
? `[dsh-plugin-hub] possible misjudgment: ${repo}`
|
|
216
|
-
: `[dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
|
|
225
|
+
: `[dsh-plugin-hub] ${reasonTitleOf(kind, action)}: ${repo}`
|
|
217
226
|
const code = coreErrorCode(message)
|
|
218
227
|
const build = (coreChars: number): string => {
|
|
219
228
|
const body = [
|
|
220
229
|
'## Summary',
|
|
221
|
-
`- Auto-classified cause: ${reasonTitleOf(kind)}`,
|
|
230
|
+
`- Auto-classified cause: ${reasonTitleOf(kind, action)}`,
|
|
222
231
|
`- Target plugin: \`${repo}\``,
|
|
223
232
|
...(code ? [`- Key error: \`${code}\``] : []),
|
|
224
233
|
// 为什么投递到这里而非插件仓库:只有确有证据是插件自身构建失败(pluginPrepare)才投插件仓库
|
|
@@ -232,12 +241,12 @@ export function hubIssueUrl(
|
|
|
232
241
|
]
|
|
233
242
|
: ['- Why filed here: the installer only routes a failure to the plugin repo when there is positive evidence the plugin build itself failed (pluginPrepare). This failure does not match that, so it is routed here for triage.']),
|
|
234
243
|
'',
|
|
235
|
-
`## [DSH Plugin 插件市场](${SITE_URL}) ·
|
|
236
|
-
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL})
|
|
237
|
-
`-
|
|
238
|
-
`-
|
|
244
|
+
`## [DSH Plugin 插件市场](${SITE_URL}) · ${zhWord} Plugin 失败错误信息`,
|
|
245
|
+
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件市场的安装程序自动生成,随本次${zhWord}失败一并提交。`,
|
|
246
|
+
`- 实际执行的${zhWord}命令:${executed ? `\`${executed}\`` : 'unknown (no executed command recorded)'}`,
|
|
247
|
+
`- 执行结果:${zhWord}失败,未能${zhWord}该插件。`,
|
|
239
248
|
...(attempts && attempts.length > 0
|
|
240
|
-
? ['', '## Attempted
|
|
249
|
+
? ['', '## Attempted channels(已尝试的方式)', ...attempts.map((a) => `- ${a}`)]
|
|
241
250
|
: []),
|
|
242
251
|
'',
|
|
243
252
|
'## Environment',
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* 官方 registry 存在性复核:用官方源(绕开本机镜像)复查 404 的包,产出标记行区分
|
|
7
|
+
* 「profile 残留依赖」「插件依赖在官方源也不存在(作者侧)」「本机镜像未同步」三种情形;
|
|
8
|
+
* 标记由 task-queue 追加到任务输出,前端按标记归类。fetch 与 profile 目录可注入,便于单测。
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
/** pnpm 输出里 scope 包是 URL 编码形态(`@scope%2Fname`):比较依赖名、拼查询 URL 前先还原成
|
|
14
|
+
* `@scope/name`。 */
|
|
15
|
+
function decodePackageName(raw: string): string {
|
|
16
|
+
return raw.replace(/%40/gi, '@').replace(/%2f/gi, '/')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 从失败输出里提取「registry 上不存在的包名」(含 scope,如 `@scope/name`):读 404 行的请求路径。
|
|
20
|
+
* 提取不到返回 null。 */
|
|
21
|
+
export function missingPackageOf(output: string): string | null {
|
|
22
|
+
for (const line of output.split(/\r?\n/)) {
|
|
23
|
+
if (!/ERR_PNPM_FETCH_404|404/.test(line)) continue
|
|
24
|
+
const m = line.match(/GET\s+https?:\/\/[^\s/]+\/((?:@[^/\s:]+\/)?[^/\s:]+)/)
|
|
25
|
+
if (m) return decodePackageName(m[1])
|
|
26
|
+
}
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 读 profile 自己声明的依赖名(profile 目录下 package.json 的 dependencies 键)。读不到返回空数组。 */
|
|
31
|
+
export function profileDependencyNames(profileDir: string): string[] {
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')) as { dependencies?: Record<string, unknown> }
|
|
34
|
+
return Object.keys(raw.dependencies ?? {})
|
|
35
|
+
} catch {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 官方 registry 上是否存在该包:明确 404 → false(不存在),2xx → true,其余(限流/网络异常)→ null
|
|
41
|
+
* —— 判不了就不判,绝不妄下结论。 */
|
|
42
|
+
async function existsOnOfficialRegistry(
|
|
43
|
+
name: string,
|
|
44
|
+
fetchImpl: typeof fetch,
|
|
45
|
+
timeoutMs: number,
|
|
46
|
+
): Promise<boolean | null> {
|
|
47
|
+
// scope 包的路径要编码:`@scope/name` → `%40scope%2Fname`
|
|
48
|
+
const url = `https://registry.npmjs.org/${name.replace('@', '%40').replace('/', '%2F')}`
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetchImpl(url, {
|
|
51
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
52
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
53
|
+
})
|
|
54
|
+
if (res.status === 404) return false
|
|
55
|
+
return res.ok ? true : null
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 产出「包不存在」失败的责任方标记行;判不了返回 null(前端维持既有提示)。
|
|
63
|
+
* - `[registry-missing]` 缺的包在官方源上也不存在 → 作者侧问题(前端引导投作者)
|
|
64
|
+
* - `[registry-mirror-lag]` 官方源上有 → 本机镜像源未同步(前端提示换源,不投任何一方)
|
|
65
|
+
*/
|
|
66
|
+
export async function registryMissingMarker(
|
|
67
|
+
output: string,
|
|
68
|
+
opts: { profileDir: string; fetchImpl?: typeof fetch; timeoutMs?: number },
|
|
69
|
+
): Promise<string | null> {
|
|
70
|
+
const name = missingPackageOf(output)
|
|
71
|
+
if (name === null) return null
|
|
72
|
+
// ① profile 自己声明的残留依赖(#73):用户环境问题,交给既有的 pnpmMissingDep 提示(该提示就是
|
|
73
|
+
// 教用户删 profile 里那条依赖),这里不再产出标记、不改归属
|
|
74
|
+
if (profileDependencyNames(opts.profileDir).some((dep) => dep.toLowerCase() === name.toLowerCase())) return null
|
|
75
|
+
const exists = await existsOnOfficialRegistry(name, opts.fetchImpl ?? fetch, opts.timeoutMs ?? 8000)
|
|
76
|
+
if (exists === false) {
|
|
77
|
+
return `[registry-missing] ${name} is not published on the official npm registry (registry.npmjs.org) — a dependency of this plugin is missing or was removed; author-side issue`
|
|
78
|
+
}
|
|
79
|
+
if (exists === true) {
|
|
80
|
+
return `[registry-mirror-lag] ${name} exists on the official npm registry — the configured registry or mirror has not synced it; local environment issue`
|
|
81
|
+
}
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
14
14
|
import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
|
|
15
15
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.ts'
|
|
16
16
|
import { npmTooLowMarker } from './npm-check.ts'
|
|
17
|
+
import { registryMissingMarker } from './registry-check.ts'
|
|
17
18
|
import { backfillOperationLog } from './operation-log.ts'
|
|
18
19
|
import { cleanLine, estimateProgress } from '../profile/progress.ts'
|
|
19
20
|
import type { LoaderHandle } from '../loader.ts'
|
|
@@ -148,6 +149,13 @@ function pushLine(task: InstallTask, line: string): void {
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/** spawn 的 cwd:profile 目录(asar 路径不能作 cwd);目录不存在时回退进程工作目录,
|
|
153
|
+
* 否则 Node 报的是 `spawn <file> ENOENT`,与「可执行文件不存在」无法区分。 */
|
|
154
|
+
function spawnCwdFor(profile: string): string {
|
|
155
|
+
const dir = profileDirectory(profile)
|
|
156
|
+
return existsSync(dir) ? dir : process.cwd()
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
function cliInvocation(profile: string): Invocation {
|
|
152
160
|
const entry = process.argv[1]
|
|
153
161
|
if (entry !== undefined) {
|
|
@@ -161,8 +169,8 @@ function cliInvocation(profile: string): Invocation {
|
|
|
161
169
|
return {
|
|
162
170
|
file: process.execPath,
|
|
163
171
|
prefixArgs: ['--expose-internals', bootstrap],
|
|
164
|
-
//
|
|
165
|
-
cwd:
|
|
172
|
+
// cwd 用 profile 目录(asar 路径不能作 cwd),不存在时由 spawnCwdFor 回退
|
|
173
|
+
cwd: spawnCwdFor(profile),
|
|
166
174
|
useShell: false,
|
|
167
175
|
// Electron 可执行文件只有带这个变量才按 Node 运行引导脚本
|
|
168
176
|
env: { ELECTRON_RUN_AS_NODE: '1' },
|
|
@@ -474,15 +482,19 @@ function spawnMutation(options: {
|
|
|
474
482
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
475
483
|
})
|
|
476
484
|
} catch (error) {
|
|
485
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
486
|
+
// 宿主自身入口起不来 → 补标记,前端据此归因
|
|
487
|
+
const marker = hostSpawnMarker(invocation, error)
|
|
477
488
|
if (task) {
|
|
478
489
|
task.status = 'failed'
|
|
479
490
|
task.exitCode = null
|
|
480
|
-
pushLine(task, `[error] ${
|
|
491
|
+
pushLine(task, `[error] ${detail}`)
|
|
492
|
+
if (marker !== null) pushLine(task, marker)
|
|
481
493
|
}
|
|
482
494
|
resolveOnce({
|
|
483
495
|
exitCode: null,
|
|
484
496
|
timedOut: false,
|
|
485
|
-
error:
|
|
497
|
+
error: marker === null ? detail : `${detail}\n${marker}`,
|
|
486
498
|
stdout,
|
|
487
499
|
stderr,
|
|
488
500
|
})
|
|
@@ -553,12 +565,15 @@ function spawnMutation(options: {
|
|
|
553
565
|
})
|
|
554
566
|
|
|
555
567
|
child.once('error', (error) => {
|
|
568
|
+
// 同同步抛出那条路径:宿主自身入口起不来 → 补标记
|
|
569
|
+
const marker = hostSpawnMarker(invocation, error)
|
|
556
570
|
if (task && task.status !== 'cancelled') {
|
|
557
571
|
task.status = 'failed'
|
|
558
572
|
task.exitCode = null
|
|
559
573
|
pushLine(task, `[error] ${error.message}`)
|
|
574
|
+
if (marker !== null) pushLine(task, marker)
|
|
560
575
|
}
|
|
561
|
-
settleOnce({ exitCode: null, timedOut: false, error: error.message
|
|
576
|
+
settleOnce({ exitCode: null, timedOut: false, error: marker === null ? error.message : `${error.message}\n${marker}`, stdout, stderr })
|
|
562
577
|
})
|
|
563
578
|
|
|
564
579
|
child.once('close', (code) => {
|
|
@@ -711,6 +726,33 @@ function annotateNpmTooLow(result: InstallResult, options: {
|
|
|
711
726
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
|
|
712
727
|
}
|
|
713
728
|
|
|
729
|
+
/** 安装失败且日志含 404 时,用官方源复查一次并注入标记(逻辑见 registry-check.ts)。
|
|
730
|
+
* 异步是因为要联网;只在失败路径上发生。 */
|
|
731
|
+
async function annotateRegistryMissing(result: InstallResult, options: {
|
|
732
|
+
action: 'add' | 'remove' | 'update'
|
|
733
|
+
profile: string
|
|
734
|
+
task?: InstallTask
|
|
735
|
+
}): Promise<InstallResult> {
|
|
736
|
+
if (options.action === 'remove' || result.exitCode === 0 || result.timedOut) return result
|
|
737
|
+
const line = await registryMissingMarker(`${result.stderr}\n${result.stdout}`, {
|
|
738
|
+
profileDir: profileDirectory(options.profile),
|
|
739
|
+
})
|
|
740
|
+
if (line === null) return result
|
|
741
|
+
if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
|
|
742
|
+
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* 宿主自身 CLI 调不起来时的 ASCII 标记行:spawn 我们解析出的宿主入口报 ENOENT 时返回
|
|
747
|
+
* `[host-spawn]`,供前端单独归因、不引导提 Issue。PATH 上的 dsh/npm 各有自己的标记,返回 null。
|
|
748
|
+
* 导出供单测覆盖判据。
|
|
749
|
+
*/
|
|
750
|
+
export function hostSpawnMarker(invocation: Invocation, error: unknown): string | null {
|
|
751
|
+
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') return null
|
|
752
|
+
if (invocation.file === 'dsh' || invocation.file === 'npm') return null
|
|
753
|
+
return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`
|
|
754
|
+
}
|
|
755
|
+
|
|
714
756
|
/**
|
|
715
757
|
* Run a plugin mutation with one recovery path: when `add` fails because
|
|
716
758
|
* pnpm's allowBuilds gate blocks a build script — either the git-hosted
|
|
@@ -804,6 +846,8 @@ export async function runPluginMutation(options: {
|
|
|
804
846
|
result = { ...result, stderr: `${result.stderr}\n${backfilled}` }
|
|
805
847
|
}
|
|
806
848
|
}
|
|
849
|
+
// 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
|
|
850
|
+
result = await annotateRegistryMissing(result, options)
|
|
807
851
|
// npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
|
|
808
852
|
return annotateNpmTooLow(result, options)
|
|
809
853
|
}
|