dsh-plugin 2.0.3 → 2.0.5
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/README.en.md +2 -0
- package/README.md +2 -0
- package/client/client.js +388 -335
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +22 -3
- package/lib/http/diagnostics.js +36 -9
- package/lib/services/install/task-queue.js +6 -1
- package/lib/services/probe.js +10 -5
- package/lib/services/profile/profile.js +14 -0
- package/lib/types/services/install/install-types.d.ts +3 -1
- package/lib/types/services/probe.d.ts +2 -1
- package/lib/types/services/profile/profile.d.ts +3 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +6 -1
- package/src/client/components/catalog/CatalogList.tsx +5 -1
- package/src/client/components/layout/SectionTabs.tsx +2 -1
- package/src/client/components/modals/NotificationsModal.tsx +4 -0
- package/src/client/components/modals/modals.tsx +11 -2
- package/src/client/components/views/MarketView.tsx +1 -0
- package/src/client/data/catalog.ts +9 -7
- package/src/client/hooks/useCatalog.ts +10 -4
- package/src/client/hooks/useTaskQueue.ts +14 -7
- package/src/client/locales.ts +18 -4
- package/src/client/logic/failures.ts +18 -4
- package/src/client/styles/Modal.module.css +9 -0
- package/src/client/styles/SectionTabs.module.css +18 -0
- package/src/server/http/catalog.ts +24 -3
- package/src/server/http/diagnostics.ts +36 -10
- package/src/server/services/install/install-types.ts +3 -1
- package/src/server/services/install/task-queue.ts +6 -1
- package/src/server/services/probe.ts +10 -5
- package/src/server/services/profile/profile.ts +15 -0
package/src/client/locales.ts
CHANGED
|
@@ -26,9 +26,11 @@ export const zh = {
|
|
|
26
26
|
loading: '正在加载插件数据…',
|
|
27
27
|
failed: '插件数据加载失败',
|
|
28
28
|
// 目录拉不出来时的精准提示:按「是否配置了代理」区分,引导排查网络 / 代理
|
|
29
|
-
failedDescNoProxy: '
|
|
30
|
-
failedDescProxy: '
|
|
29
|
+
failedDescNoProxy: '无法获取插件目录数据(有数 MB)。网络较慢时首次加载可能超时,可重试或点「强制刷新」跳过本地缓存重新下载;加载成功后会缓存 1 小时。若持续失败,请点「网络诊断」确认目录站点可达。',
|
|
30
|
+
failedDescProxy: '无法获取插件目录数据(有数 MB)。网络较慢,或设置里的 HTTP 代理不可达/链路抖动时,首次加载可能超时;可重试或点「强制刷新」跳过缓存重新下载。加载成功后会缓存 1 小时。',
|
|
31
31
|
retry: '重试',
|
|
32
|
+
forceReload: '强制刷新',
|
|
33
|
+
forceReloadHint: '跳过本地缓存的插件目录,重新从服务器下载(缓存异常或弱网重试时用)',
|
|
32
34
|
install: '安装',
|
|
33
35
|
// 需在终端手动安装的插件(webInstallable=false):两种情形共用一句 —— 命令网页端代执行不了
|
|
34
36
|
//(npx 安装器 / 本地 link),或命令能跑但装机过程要在终端完成(仓库带 prepare 构建钩子、
|
|
@@ -342,6 +344,8 @@ export const zh = {
|
|
|
342
344
|
failGitMissingHint: '系统找不到 git 命令(Git 未安装或未加入系统 PATH),无法从 GitHub 源安装插件。请先安装 Git(如 git-scm.com/downloads)或把 git 加入 PATH,重启 DSH 后重试。这不是插件本身的问题。',
|
|
343
345
|
failPnpmMissingHint: '安装器找到了 dsh,但系统里没有 pnpm 命令(dsh 用它来管理 profile 插件)。请先安装 pnpm(如 npm install -g pnpm),确认 pnpm 已加入 PATH 后重试。这不是插件本身的问题。',
|
|
344
346
|
failNpmMissingHint: '系统找不到 npm 命令(npm 未安装或未加入系统 PATH),无法执行全局安装。请先安装 Node.js(自带 npm),确认 npm 已加入 PATH 后重试。这不是插件本身的问题。',
|
|
347
|
+
failBuildToolchainHint: '这个插件依赖需要在本机编译的原生模块,但机器上缺少可用的编译工具链。请按系统安装后重试:Windows 安装 Visual Studio Build Tools(含「使用 C++ 的桌面开发」);macOS 执行 xcode-select --install;Linux 安装 build-essential、python3 与 make。若装好工具链后仍然失败,再向插件作者反馈,请其提供预构建产物。',
|
|
348
|
+
bundleDeselectedHint: '注意:插件市场在当前 profile 的 bundle 列表中被取消勾选,重启宿主后它不会出现。请到「设置 → 插件」重新勾选 dsh-plugin。',
|
|
345
349
|
failPnpmStoreHint: '安装器调用 pnpm 时报「store 位置不匹配」(ERR_PNPM_UNEXPECTED_STORE):你的 profile 目录里旧插件依赖是用另一个大版本的 pnpm 生成的,当前 pnpm 出于安全不认旧目录,导致任何插件装进该 profile 都会失败。请删除该 profile 目录下的 node_modules 与 pnpm-lock.yaml(可先到「系统日志」或 ~/.dsh/profiles/<profile>/hub.log 查看完整报错),再重新安装。这不是插件本身的问题。',
|
|
346
350
|
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 补丁条目后重试。',
|
|
347
351
|
// 404 依赖:点名 registry 上不存在的那个包(取不到包名时跳过这行),再给删除条目的指引
|
|
@@ -364,6 +368,8 @@ export const zh = {
|
|
|
364
368
|
failReason_pnpmMissing: '安装器通过 pnpm 安装插件,但机器上找不到 pnpm。',
|
|
365
369
|
failKind_npmMissing: '本机缺少 npm',
|
|
366
370
|
failReason_npmMissing: '安装流程需要 npm,但机器上找不到它。',
|
|
371
|
+
failKind_buildToolchainMissing: '本机缺少原生构建工具链',
|
|
372
|
+
failReason_buildToolchainMissing: '这个插件依赖需要在本机编译的原生模块,但找不到可用的编译器。',
|
|
367
373
|
failKind_pnpmStore: 'pnpm store 版本不匹配',
|
|
368
374
|
failReason_pnpmStore: 'profile 里的依赖由另一大版本的 pnpm 生成,当前 pnpm 不认。',
|
|
369
375
|
failKind_pnpmWorkspace: 'workspace 根校验阻止了安装',
|
|
@@ -413,6 +419,7 @@ export const zh = {
|
|
|
413
419
|
failFsUnavailableHint: '安装过程中 pnpm 无法把文件写进本机磁盘(这类报错与装哪个插件无关,任何插件装在同一目录都会失败,不是插件本身的问题)。请按报错对号入座:① 磁盘空间不足(ENOSPC / no space left on device,Windows 为 os error 112)——请清理 profile 所在磁盘的剩余空间后重试;② 磁盘或挂载点只读、带写保护(EROFS / read-only file system,Windows 为 os error 19)——请确认 profile 目录所在磁盘可写(外接盘/U 盘、带写保护的移动盘或只读挂载的分区都会触发),必要时把插件目录换到系统盘的可写位置;③ 进程可用的文件句柄被耗尽(EMFILE / ENFILE / too many open files)——请关闭占用大量文件的程序并重启 DeepSeek Harness 后重试。',
|
|
414
420
|
failPnpmWorkspaceHint: '安装器调用 pnpm 时报「不能把依赖装到 workspace 根目录」(ERR_PNPM_ADDING_TO_ROOT):你的 profile 目录被 pnpm 视为 workspace 根目录,而本次安装命令没有声明在根目录操作,pnpm 直接拒绝执行,因此任何插件装进来都会失败。这不是插件本身的问题——可在 profile 目录(~/.dsh/profiles/<profile>)的 .npmrc 里加一行 ignore-workspace-root-check=true 后重试,或把 DeepSeek Harness 升级到最新版再试。',
|
|
415
421
|
failPnpmTarballIntegrityHint: 'pnpm 报 ERR_PNPM_MISSING_TARBALL_INTEGRITY:锁文件中的 tarball 条目没有校验值,无法验证下载的包。先查看完整日志,完全退出 DeepSeek Harness,并检查、备份当前 profile 的 package.json、pnpm-lock.yaml 和 pnpm-workspace.yaml。确认依赖来源后,将 pnpm-lock.yaml 改名保留,再通过同一 DSH 安装器安装适配当前宿主版本的插件,重新生成锁文件。重建会重新解析 profile 依赖,请核对新旧版本;保持完整性校验开启。',
|
|
422
|
+
failPnpmTarballMismatchHint: 'pnpm 报 ERR_PNPM_TARBALL_INTEGRITY:profile 里某个从 GitHub 分支安装的依赖(例如 github:owner/repo#main)在远端更新了,tarball 内容变了,但 pnpm-lock.yaml 里存的还是旧校验和,于是整个 profile 的依赖安装都会卡在它上面。处理:打开 ~/.dsh/profiles/<profile>/pnpm-lock.yaml,删掉该 GitHub 依赖对应的条目(不确定是哪个就把整份锁文件改名备份),再重新安装;长期做法是把依赖固定到具体 commit(github:owner/repo#<commit>),分支再更新也不会改变校验和。',
|
|
416
423
|
failPnpmPolicyHint: '安装器调用 pnpm 时被其供应链安全策略拦截——装任何插件都会撞墙,不是插件本身的问题。分两种情况处理:① 提示「Minimum release age」(ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION):包发布还不满 24 小时,pnpm 11 出于安全会拒收这种刚发布的新包。可在 pnpm 配置(pnpm-workspace.yaml)里加 minimumReleaseAge: 0 豁免,或等满 24 小时后重试。② 提示「untrusted origin」(ERR_PNPM_UNTRUSTED_ORIGIN):依赖来源不被本机 pnpm 信任,通常是该 profile 里之前用 github: 或非官方 npm 源装过插件,锁文件记录了不受信任的来源。请删除该 profile 目录下的 node_modules 与 pnpm-lock.yaml 后重新安装(可先到「系统日志」或 ~/.dsh/profiles/<profile>/hub.log 查看完整报错)。',
|
|
417
424
|
// minimumReleaseAge 专属:从报错里算出确切的可重试时刻({time}),让用户知道「何时能再装」,
|
|
418
425
|
// 而不是笼统地让等 24 小时
|
|
@@ -483,9 +490,11 @@ export const en = {
|
|
|
483
490
|
loading: 'Loading plugin data…',
|
|
484
491
|
failed: 'Failed to load plugin data',
|
|
485
492
|
// 目录加载失败时的精准提示:按是否配置了代理区分
|
|
486
|
-
failedDescNoProxy: '
|
|
487
|
-
failedDescProxy: '
|
|
493
|
+
failedDescNoProxy: 'The plugin catalog could not be loaded (a few MB). The first load can time out on a slow connection — retry, or use "Force refresh" to skip the local cache and download it again. Once it succeeds the catalog is cached for an hour. If it keeps failing, run the network diagnostic.',
|
|
494
|
+
failedDescProxy: 'The plugin catalog could not be loaded (a few MB). A slow connection, or an unreachable/unstable HTTP proxy in Settings, can make the first load time out — retry, or use "Force refresh" to skip the cache. Once it succeeds the catalog is cached for an hour.',
|
|
488
495
|
retry: 'Retry',
|
|
496
|
+
forceReload: 'Force refresh',
|
|
497
|
+
forceReloadHint: 'Skip the cached catalog and download it again from the server (use when the cache looks wrong or on a slow network)',
|
|
489
498
|
install: 'Install',
|
|
490
499
|
// 需在终端手动安装的插件(webInstallable=false):两种情形共用一套措辞 ——
|
|
491
500
|
// 网页端完全跑不了该命令(npx 安装器 / 本地 link),或能跑但装机过程要在终端完成
|
|
@@ -799,6 +808,8 @@ export const en = {
|
|
|
799
808
|
failGitMissingHint: 'The git command could not be found (Git is not installed or not on the system PATH), so GitHub-source plugins cannot be installed. Please install Git (e.g. from git-scm.com/downloads) or add git to your PATH, restart DSH, then retry. This is not a problem with the plugin itself.',
|
|
800
809
|
failPnpmMissingHint: 'The installer found dsh, but pnpm is missing on this machine (dsh uses it to manage profile plugins). Please install pnpm first (e.g. npm install -g pnpm), make sure it is on your PATH, then retry. This is not a problem with the plugin itself.',
|
|
801
810
|
failNpmMissingHint: 'The npm command could not be found (npm is not installed or not on the system PATH), so a global install cannot run. Please install Node.js (which bundles npm), make sure npm is on your PATH, then retry. This is not a problem with the plugin itself.',
|
|
811
|
+
failBuildToolchainHint: 'This plugin depends on a native module that must be compiled on your machine, but no usable build toolchain is available. Install one and retry: on Windows install Visual Studio Build Tools with the "Desktop development with C++" workload; on macOS run xcode-select --install; on Linux install build-essential, python3 and make. If it still fails after the toolchain is installed, report it to the plugin author and ask for a prebuilt package.',
|
|
812
|
+
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.',
|
|
802
813
|
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.',
|
|
803
814
|
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.',
|
|
804
815
|
// 缺失依赖:点名 404 的包(解析不出时跳过该行),再给删除指引
|
|
@@ -821,6 +832,8 @@ export const en = {
|
|
|
821
832
|
failReason_pnpmMissing: 'The installer uses pnpm to add plugins, but pnpm cannot be found.',
|
|
822
833
|
failKind_npmMissing: 'npm is not installed on this machine',
|
|
823
834
|
failReason_npmMissing: 'The install flow needs npm, but it cannot be found.',
|
|
835
|
+
failKind_buildToolchainMissing: 'native build toolchain is missing',
|
|
836
|
+
failReason_buildToolchainMissing: 'A dependency of this plugin must be compiled locally, but no usable compiler was found.',
|
|
824
837
|
failKind_pnpmStore: 'pnpm store version mismatch',
|
|
825
838
|
failReason_pnpmStore: 'The profile dependencies were written by a different pnpm major, which the current pnpm does not accept.',
|
|
826
839
|
failKind_pnpmWorkspace: 'workspace-root check blocked the install',
|
|
@@ -870,6 +883,7 @@ export const en = {
|
|
|
870
883
|
failFsUnavailableHint: 'During install, pnpm could not write files to the local disk — this has nothing to do with which plugin you are installing: any plugin installed into the same directory would fail the same way, so it is not a problem with the plugin itself. Match the error you see: ① Out of space (ENOSPC / no space left on device, os error 112 on Windows) — free up space on the drive that holds the profile and retry. ② Read-only or write-protected volume (EROFS / read-only file system, os error 19 on Windows) — make sure the drive holding the profile directory is writable (external or USB drives, write-protected media, and read-only mounts all trigger this); move the plugin directory to a writable location on the system drive if needed. ③ File handles exhausted (EMFILE / ENFILE / too many open files) — close programs that hold many files open, restart DeepSeek Harness, then retry.',
|
|
871
884
|
failPnpmWorkspaceHint: 'pnpm refused to add the dependency to the workspace root (ERR_PNPM_ADDING_TO_ROOT): the profile directory is treated as a pnpm workspace root, and this install command did not declare that it operates at the root, so pnpm rejected it outright — no plugin can be installed into that profile. This is not a problem with the plugin itself. Add `ignore-workspace-root-check=true` to the `.npmrc` inside the profile directory (~/.dsh/profiles/<profile>) and retry, or upgrade DeepSeek Harness and try again.',
|
|
872
885
|
failPnpmTarballIntegrityHint: 'pnpm reported ERR_PNPM_MISSING_TARBALL_INTEGRITY: a tarball entry in the lockfile has no checksum to verify the downloaded package. Read the full log, fully quit DeepSeek Harness, and inspect and back up package.json, pnpm-lock.yaml and pnpm-workspace.yaml in the affected profile. Once the dependency sources are verified, rename pnpm-lock.yaml to preserve it, then use the same DSH installer to install a plugin version compatible with the host and regenerate the lockfile. This resolves profile dependencies again: compare the old and new versions, and keep integrity checks enabled.',
|
|
886
|
+
failPnpmTarballMismatchHint: 'pnpm reported ERR_PNPM_TARBALL_INTEGRITY: a dependency installed from a GitHub branch (for example github:owner/repo#main) changed upstream, so the tarball content no longer matches the checksum stored in pnpm-lock.yaml — and every install into that profile now stops on it. To fix: open ~/.dsh/profiles/<profile>/pnpm-lock.yaml, remove the entry for that GitHub dependency (or rename the whole lockfile as a backup if unsure which one it is), then reinstall. Long term, pin the dependency to a commit (github:owner/repo#<commit>) so branch updates no longer change its checksum.',
|
|
873
887
|
failPnpmPolicyHint: 'The installer was blocked by pnpm supply-chain security policy — every plugin install would hit the same wall, so this is not a problem with the plugin itself. Two cases: ① "Minimum release age" (ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION): the package was published less than 24 hours ago, and pnpm 11 refuses freshly-published packages for safety. Add minimumReleaseAge: 0 to your pnpm config (pnpm-workspace.yaml) to opt out, or wait until the 24 hours have passed and retry. ② "untrusted origin" (ERR_PNPM_UNTRUSTED_ORIGIN): a dependency source is not trusted by this machine, usually because plugins were previously installed via github: or a non-official npm registry, so the lockfile records an untrusted source. 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.',
|
|
874
888
|
// 仅 minimumReleaseAge 场景:从报错算出确切的可重试时刻({time}),让用户知道何时能装,
|
|
875
889
|
// 而不是笼统的「等 24 小时」
|
|
@@ -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' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'hostIncompatible' | 'network' | 'hostCliSpawn' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
|
|
151
|
+
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'buildToolchainMissing' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'hostIncompatible' | 'network' | 'hostCliSpawn' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
|
|
152
152
|
|
|
153
153
|
/** 失败动作口径:标题、上报正文与提示文案按安装/更新/卸载取词,不写死「安装」。 */
|
|
154
154
|
export type FailureAction = 'install' | 'update' | 'uninstall'
|
|
@@ -174,6 +174,13 @@ function missingTarballIntegrity(message: string): boolean {
|
|
|
174
174
|
return /\bERR_PNPM_MISSING_TARBALL_INTEGRITY\b/.test(message)
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/** GitHub 分支 tarball 内容变了、锁文件里还是旧校验和(ERR_PNPM_TARBALL_INTEGRITY)。
|
|
178
|
+
* \b 保证不会命中 ERR_PNPM_MISSING_TARBALL_INTEGRITY:前者是有校验值但对不上,
|
|
179
|
+
* 后者是锁文件里压根没有校验值。 */
|
|
180
|
+
function mismatchedTarballIntegrity(message: string): boolean {
|
|
181
|
+
return /\bERR_PNPM_TARBALL_INTEGRITY\b/.test(message)
|
|
182
|
+
}
|
|
183
|
+
|
|
177
184
|
/** pnpm 拒收「刚发布」包时,报错里带着发布时间;这里从报错文本算出「发布满 24h」的可重试时刻(UTC,分钟精度)。 */
|
|
178
185
|
export function releaseAgeRetryAfter(message: string): string | null {
|
|
179
186
|
const match = /published at (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/.exec(message)
|
|
@@ -188,7 +195,9 @@ export function releaseAgeRetryAfter(message: string): string | null {
|
|
|
188
195
|
* 可重试时刻时才走专属文案(给出确切的「何时可再试」),算不出时退回通用文案,
|
|
189
196
|
* 避免出现「预计可重试时间:」后接空值的残缺句子。
|
|
190
197
|
*/
|
|
191
|
-
export function pnpmPolicyHintOf(message: string): 'failPnpmTarballIntegrityHint' | 'failPnpmReleaseAgeHint' | 'failPnpmPolicyHint' {
|
|
198
|
+
export function pnpmPolicyHintOf(message: string): 'failPnpmTarballIntegrityHint' | 'failPnpmTarballMismatchHint' | 'failPnpmReleaseAgeHint' | 'failPnpmPolicyHint' {
|
|
199
|
+
// 校验和对不上(分支 tarball 变了):先于「缺校验值」,两者报错前缀不同但都叫 tarball integrity
|
|
200
|
+
if (mismatchedTarballIntegrity(message)) return 'failPnpmTarballMismatchHint'
|
|
192
201
|
if (missingTarballIntegrity(message)) return 'failPnpmTarballIntegrityHint'
|
|
193
202
|
if (/ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|Minimum release age|minimumReleaseAge/i.test(message)
|
|
194
203
|
&& releaseAgeRetryAfter(message) !== null) {
|
|
@@ -230,9 +239,9 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
230
239
|
// 本地服务来源校验拒绝(403 untrusted origin),请求没进安装流程。必须在 pnpmPolicy 之前
|
|
231
240
|
// —— 后者同名裸文本会把这条误判成供应链策略。用「整行以它结尾」区隔 pnpm 真实报错
|
|
232
241
|
if (/(^|\n)untrusted origin\s*$/.test(message)) return 'originRejected'
|
|
233
|
-
// pnpm 供应链策略拦截(缺 tarball integrity / minimumReleaseAge / untrusted origin)。
|
|
242
|
+
// pnpm 供应链策略拦截(缺 tarball integrity / 校验和不匹配 / minimumReleaseAge / untrusted origin)。
|
|
234
243
|
// 必须在 pnpmIgnoredBuild 之前
|
|
235
|
-
if (missingTarballIntegrity(message) || /ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|Minimum release age|minimumReleaseAge|untrusted origin/i.test(message)) return 'pnpmPolicy'
|
|
244
|
+
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'
|
|
236
245
|
// profile 残留旧版本 patch 声明,pnpm 中止安装。必须在兜底之前
|
|
237
246
|
if (/ERR_PNPM_UNUSED_PATCH|patches were not used/i.test(message)) return 'pnpmUnusedPatch'
|
|
238
247
|
// profile 文件被其他进程占用(os error 32 / EBUSY)。ASCII 特征免疫乱码
|
|
@@ -247,6 +256,8 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
247
256
|
// 网络/代理/源配置问题。必须在 prepare 之前判:git fetch 失败常被外层包成 "Command failed"
|
|
248
257
|
// tarball 下载失败的括号里可能是数字码(pnpm 自己的)或符号码(undici 的 UND_ERR_*),两种都要认
|
|
249
258
|
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|\bTLS\b|\bSSL\b|fetch failed|failed to fetch|\bGET https?:\/\/\S+\.tgz\s+error \([A-Za-z0-9_]+\)/i.test(message)) return 'network'
|
|
259
|
+
// 浏览器端请求超时(客户端 fetch 被 abort):请求没到宿主,属于连不上网络,与插件无关
|
|
260
|
+
if (/\[request-timeout\]/i.test(message)) return 'network'
|
|
250
261
|
// 装后校验发现入口文件缺失([packaging])= 分发物缺构建产物
|
|
251
262
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
252
263
|
// registry 上不存在的依赖(404),profile 任何安装都会卡在这条。必须在 pluginPrepare 之前判
|
|
@@ -257,6 +268,9 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
257
268
|
if (/\bERR_PNPM_NO_MATCHING_VERSION\b|\bETARGET\b|No matching version found/i.test(message)) return 'registryVersionStale'
|
|
258
269
|
// profile 残留指向不存在目录的本地依赖(link:/file:)。必须在 pluginPrepare 之前判
|
|
259
270
|
if (/\bERR_PNPM_LINKED_PKG_DIR_NOT_FOUND\b|Could not install from .+ as it does not exist/i.test(message)) return 'pnpmLinkedDirMissing'
|
|
271
|
+
// 原生模块需要本机编译,缺编译器/构建工具属于用户环境,不是插件的 prepare 失败。
|
|
272
|
+
// 必须排在 pluginPrepare 之前:这类日志同样带 ELIFECYCLE,否则会被推给作者。
|
|
273
|
+
if (/Unable to detect compiler type|gyp ERR!|Could not find any Visual Studio|No Xcode or CLT version detected|xcode-select|python(?:3)? is (?:not set|not found)|Could not find Python|\bmake: not found\b|\bgcc: not found\b|\bg\+\+: not found\b|\bMSB\d{4}\b/i.test(message)) return 'buildToolchainMissing'
|
|
260
274
|
// 构建脚本真的跑挂了才算插件问题。Command failed 区分大小写:只匹配 pnpm 的包装,
|
|
261
275
|
// 不匹配宿主那句小写 dsh: plugin command failed
|
|
262
276
|
if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|prepare-guard/i.test(message) || /Command failed/.test(message)) return 'pluginPrepare'
|
|
@@ -722,6 +722,15 @@
|
|
|
722
722
|
padding: 12px 0 6px;
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
+
/* 市场自更新后 bundle 被取消勾选:重启后市场不会出现,用警示色提示重新勾选 */
|
|
726
|
+
.bundleDeselectedHint {
|
|
727
|
+
padding: 12px 0 6px;
|
|
728
|
+
font-size: 12px;
|
|
729
|
+
line-height: 18px;
|
|
730
|
+
text-align: left;
|
|
731
|
+
color: var(--hub-warning);
|
|
732
|
+
}
|
|
733
|
+
|
|
725
734
|
/* 重启选择按钮对:稍后(次级文字)+ 立即重启(主按钮) */
|
|
726
735
|
.restartLater {
|
|
727
736
|
font-size: 12px;
|
|
@@ -12,10 +12,16 @@
|
|
|
12
12
|
flex-shrink: 0;
|
|
13
13
|
padding: 0 2px;
|
|
14
14
|
border-bottom: 1px solid var(--hub-border-1);
|
|
15
|
+
/* 以自身宽度做容器查询基准:面板变窄时 tab 只剩图标 */
|
|
16
|
+
container-type: inline-size;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
.tab,
|
|
18
20
|
.tabActive {
|
|
21
|
+
/* 标签不折行:窄容器下保持单行,避免中文被逐字竖排 */
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
/* 容器再窄也不压缩 tab:拒绝收缩,转为横向溢出而不是纵向下坠 */
|
|
24
|
+
flex-shrink: 0;
|
|
19
25
|
position: relative;
|
|
20
26
|
display: inline-flex;
|
|
21
27
|
align-items: center;
|
|
@@ -44,6 +50,18 @@
|
|
|
44
50
|
font-weight: 600;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/* 容器够窄时只留图标:四个 tab 与通知入口保持单行、不横向溢出(名称仍由 aria-label 提供) */
|
|
54
|
+
@container (max-width: 440px) {
|
|
55
|
+
.tabLabel {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.tab,
|
|
60
|
+
.tabActive {
|
|
61
|
+
padding: 0 2px;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/* tab 图标:16px 随字号等宽对齐,SVG 继承 currentColor 自动跟随选中/悬浮色 */
|
|
48
66
|
.tabIcon {
|
|
49
67
|
flex-shrink: 0;
|
|
@@ -23,6 +23,24 @@ function catalogCacheFile(profile: string, key: string): string {
|
|
|
23
23
|
return join(profileDirectory(profile), 'cache', `catalog-${key}.json`)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** 目录拉取超时:目录 JSON 约 3.6MB,20s 在弱网/代理链路上不够用;
|
|
27
|
+
* 客户端会在超时后重试,重试必须共享同一次拉取(见 fetchCatalogOnce)。 */
|
|
28
|
+
const CATALOG_FETCH_TIMEOUT_MS = 60_000
|
|
29
|
+
|
|
30
|
+
/** 并发拉取去重:客户端超时重试会并发打进来,共享同一次 curl,
|
|
31
|
+
* 避免多份 3.6MB 下载互相抢带宽、越重试越慢。 */
|
|
32
|
+
const inflightCatalog = new Map<string, ReturnType<typeof fetchViaCurl>>()
|
|
33
|
+
|
|
34
|
+
function fetchCatalogOnce(target: string, proxy: string): ReturnType<typeof fetchViaCurl> {
|
|
35
|
+
const key = `${target}\n${proxy}`
|
|
36
|
+
const running = inflightCatalog.get(key)
|
|
37
|
+
if (running !== undefined) return running
|
|
38
|
+
const task = fetchViaCurl(target, proxy, CATALOG_FETCH_TIMEOUT_MS)
|
|
39
|
+
.finally(() => { inflightCatalog.delete(key) })
|
|
40
|
+
inflightCatalog.set(key, task)
|
|
41
|
+
return task
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
/** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
|
|
27
45
|
* maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时兜底使用。 */
|
|
28
46
|
function readCatalogCache(file: string, maxAgeMs: number): string | null {
|
|
@@ -103,9 +121,12 @@ export function registerCatalogRoutes(webServer: WebServerService, profile: stri
|
|
|
103
121
|
const target = isStats
|
|
104
122
|
? 'https://api.dsh-plugin.org/stats.json'
|
|
105
123
|
: `https://api.dsh-plugin.org/plugins.${lang}.json`
|
|
106
|
-
// 1 小时本地缓存:命中直接返回(跨重启生效,插件市场秒开),未命中才走 curl
|
|
124
|
+
// 1 小时本地缓存:命中直接返回(跨重启生效,插件市场秒开),未命中才走 curl 拉远程。
|
|
125
|
+
// refresh=1 跳过 TTL 缓存直接回源(「强制刷新」用);成功后照样覆盖缓存,
|
|
126
|
+
// 失败仍回退旧缓存,所以强制刷新不会把可用的旧数据弄丢。
|
|
107
127
|
const cacheFile = catalogCacheFile(profile, isStats ? 'stats' : `plugins-${lang}`)
|
|
108
|
-
const
|
|
128
|
+
const forceRefresh = url.searchParams.get('refresh') === '1'
|
|
129
|
+
const cached = forceRefresh ? null : readCatalogCache(cacheFile, CATALOG_CACHE_TTL_MS)
|
|
109
130
|
if (cached !== null) {
|
|
110
131
|
try {
|
|
111
132
|
sendJson(response, 200, JSON.parse(cached))
|
|
@@ -114,7 +135,7 @@ export function registerCatalogRoutes(webServer: WebServerService, profile: stri
|
|
|
114
135
|
// 缓存正文损坏:忽略,走重新拉取并覆盖写盘
|
|
115
136
|
}
|
|
116
137
|
}
|
|
117
|
-
const r = await
|
|
138
|
+
const r = await fetchCatalogOnce(target, proxy)
|
|
118
139
|
if (!r.ok || r.body === '') {
|
|
119
140
|
// 实时拉取失败:退回任意年龄的本地缓存(上次成功数据),
|
|
120
141
|
// 避免证书吊销受阻 / 网络抖动时整个插件市场打不开 —— 数据可能过期,但好过空白。
|
|
@@ -71,6 +71,29 @@ function toolVersion(cmd: string, args: string[]): string | null {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** 本机实际生效的 npm registry:诊断与安装同口径 —— 设置里留空时读用户 npm 配置
|
|
75
|
+
* (含 profile 目录的 .npmrc),而不是硬探官方源,避免「能装但诊断说源不通」。
|
|
76
|
+
* 读不到(npm 缺失/超时/输出异常)时回退官方源。 */
|
|
77
|
+
function configuredNpmRegistry(profile: string): string {
|
|
78
|
+
const fromEnv = process.env.npm_config_registry ?? process.env.NPM_CONFIG_REGISTRY ?? ''
|
|
79
|
+
const valid = (v: string): string | null => (/^https?:\/\//.test(v) ? v.replace(/\/+$/, '') : null)
|
|
80
|
+
const viaEnv = valid(fromEnv)
|
|
81
|
+
if (viaEnv !== null) return viaEnv
|
|
82
|
+
try {
|
|
83
|
+
const cwd = join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile)
|
|
84
|
+
const r = spawnSync('npm', ['config', 'get', 'registry'], {
|
|
85
|
+
timeout: 3000,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
cwd,
|
|
88
|
+
shell: process.platform === 'win32',
|
|
89
|
+
})
|
|
90
|
+
const out = (r.stdout ?? '').trim().split(/\r?\n/).pop() ?? ''
|
|
91
|
+
return valid(out) ?? 'https://registry.npmjs.org'
|
|
92
|
+
} catch {
|
|
93
|
+
return 'https://registry.npmjs.org'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
74
97
|
/** 宿主环境快照:提交 bug 时随 issue 附上,便于作者复现(前端 /env 端点返回)。 */
|
|
75
98
|
export function hostEnv(profile: string): Record<string, string | null> {
|
|
76
99
|
return {
|
|
@@ -109,17 +132,20 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
|
|
|
109
132
|
}
|
|
110
133
|
const settings = loadSettings(profile)
|
|
111
134
|
// npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
|
|
112
|
-
//
|
|
113
|
-
const registry = settings.npmRegistry.replace(/\/+$/, '') ||
|
|
114
|
-
// 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub
|
|
115
|
-
//
|
|
116
|
-
// npm 行 display
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
// 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同口径。
|
|
136
|
+
const registry = settings.npmRegistry.replace(/\/+$/, '') || configuredNpmRegistry(profile)
|
|
137
|
+
// 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub 行探小仓库、
|
|
138
|
+
// 目录站按 Range 取 1KB(探市场真实下载的 plugins.zh.json,但不整份下载)。
|
|
139
|
+
// npm 行 display:显式配置、或本机 npm 源不是官方源时显示实际地址,
|
|
140
|
+
// 让用户看到「诊断的到底是哪个源」;官方源且未配置时留空(显示「跟随本机 npm 配置」)。
|
|
141
|
+
const allChecks: Array<{ key: string; url: string; display: string; cmd: string; proxy?: string; git?: boolean; range?: string }> = [
|
|
142
|
+
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' || registry !== 'https://registry.npmjs.org' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
119
143
|
// GitHub 行用真实 git 克隆握手(git ls-remote)打 dshplugin/hello-dsh 小仓库:
|
|
120
144
|
// 「网页能打开」和「git 能克隆」是两码事,握手成功才算通道通;小仓库秒级完成,不打 17MB 的 dsh-plugin-hub。
|
|
121
145
|
{ key: 'github', url: 'https://github.com/dshplugin/hello-dsh', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/hello-dsh', git: true },
|
|
122
|
-
|
|
146
|
+
// 目录行探市场真正下载的那个文件(plugins.zh.json),只取前 1KB:
|
|
147
|
+
// 只探 stats.json 会出现「诊断绿、市场加载不出来」—— 两者端点与体积完全不同。
|
|
148
|
+
{ key: 'catalog', url: 'https://api.dsh-plugin.org/plugins.zh.json', display: '', cmd: 'curl -r 0-1023 https://api.dsh-plugin.org/plugins.zh.json', range: '0-1023' },
|
|
123
149
|
]
|
|
124
150
|
// 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
|
|
125
151
|
// 验证「代理能不能把请求带出去」。与安装同口径:curl 子进程注入该代理 env。
|
|
@@ -140,7 +166,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
|
|
|
140
166
|
const effectiveProxy = settings.proxy !== ''
|
|
141
167
|
? settings.proxy
|
|
142
168
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
143
|
-
const probe = (url: string, proxy?: string) => probeUrl(url, proxy ?? effectiveProxy, 6000)
|
|
169
|
+
const probe = (url: string, proxy?: string, range?: string) => probeUrl(url, proxy ?? effectiveProxy, 6000, range)
|
|
144
170
|
// 逐路串行:一条命令一条结果地推送给终端,保持「程序在跑」的真实感
|
|
145
171
|
const results: Array<{ key: string; ok: boolean }> = []
|
|
146
172
|
for (const c of checks) {
|
|
@@ -148,7 +174,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
|
|
|
148
174
|
// git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时兜底
|
|
149
175
|
const r = c.git
|
|
150
176
|
? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
|
|
151
|
-
: await probe(c.url, c.proxy)
|
|
177
|
+
: await probe(c.url, c.proxy, c.range)
|
|
152
178
|
results.push({ key: c.key, ok: r.ok })
|
|
153
179
|
// git 行的 status 是退出码,客户端 git 行只显示 OK / 不可达,不显示成 HTTP 码。
|
|
154
180
|
// reason 透出失败细分原因(如 Windows 证书吊销受阻),客户端据此给对症提示而非笼统「不可达」。
|
|
@@ -41,9 +41,11 @@ export interface InstallTask {
|
|
|
41
41
|
/** 完成后是否仍需宿主重启才生效:仅「热挂载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
|
|
42
42
|
* 无 loader」等刷新解决不了的兜底场景为 true(弹窗给「立即重启」);其余一律 false。 */
|
|
43
43
|
needsRestart: boolean
|
|
44
|
-
/**
|
|
44
|
+
/** 完成结果是否只需刷新页面即生效(插件已热挂进运行中 loader,仅剩客户端 UI 待页面重新加载,
|
|
45
45
|
* 网页端与桌面端一致):true 时关闭结果弹窗即自动刷新页面,不给手动刷新按钮。 */
|
|
46
46
|
needsReload?: boolean
|
|
47
|
+
/** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
|
|
48
|
+
bundleDeselected?: boolean
|
|
47
49
|
/** 卸载入队时该包是否带客户端 UI(`dsh.client`):卸载后包已从磁盘删除,只能入队前快照,
|
|
48
50
|
* 卸载成功且曾存活时据此决定是否需要刷新页面摘除插件面板。 */
|
|
49
51
|
hadClientUi?: boolean
|
|
@@ -20,7 +20,7 @@ import { cleanLine, estimateProgress } from '../profile/progress.ts'
|
|
|
20
20
|
import type { LoaderHandle } from '../loader.ts'
|
|
21
21
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.ts'
|
|
22
22
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.ts'
|
|
23
|
-
import { addAllowBuildsKey, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.ts'
|
|
23
|
+
import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.ts'
|
|
24
24
|
import { appendLog } from '../log.ts'
|
|
25
25
|
|
|
26
26
|
/** 内存任务注册表,按任务 id 索引。 */
|
|
@@ -607,6 +607,11 @@ function spawnMutation(options: {
|
|
|
607
607
|
task.status = result.timedOut || result.exitCode !== 0 ? 'failed' : 'done'
|
|
608
608
|
if (task.status === 'done') {
|
|
609
609
|
task.progress = 100
|
|
610
|
+
// 市场自更新:若 bundle 已被取消勾选,重启后市场不会出现,结果页要明确提示
|
|
611
|
+
if ((action === 'add' || action === 'update') && (target === 'dsh-plugin' || target.startsWith('dsh-plugin@'))
|
|
612
|
+
&& !bundleIncludes(profile, 'dsh-plugin')) {
|
|
613
|
+
task.bundleDeselected = true
|
|
614
|
+
}
|
|
610
615
|
if (isGlobalNpm) {
|
|
611
616
|
// 全局 npm 安装:装的是本机 CLI 工具(如 dsh-tui),不进任何 profile,无需宿主重启
|
|
612
617
|
task.needsRestart = false
|
|
@@ -187,13 +187,16 @@ async function curlWithTlsFallback(args: string[], env: NodeJS.ProcessEnv, timeo
|
|
|
187
187
|
* 失败(curl 不存在 / 连接失败 / 超时)都归为不可达,全程不抛错。
|
|
188
188
|
* 证书吊销检查受阻(Windows Schannel)另记 reason,供诊断给出对症提示。
|
|
189
189
|
*/
|
|
190
|
-
async function curlProbe(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult> {
|
|
190
|
+
async function curlProbe(url: string, proxy: string, timeoutMs: number, range?: string): Promise<ProbeResult> {
|
|
191
191
|
const started = Date.now()
|
|
192
192
|
const discard = process.platform === 'win32' ? 'NUL' : '/dev/null'
|
|
193
193
|
// -sS(而非仅 -s):silent 会把连接/TLS 错误一并吞掉,而识别证书吊销受阻正需要 stderr;
|
|
194
194
|
// 子进程 stderr 由我们收集、不会回显给用户,所以补 -S 不产生噪音。
|
|
195
|
+
// range:目录通道路由探真实的 plugins.zh.json,但只取前 1KB(-r 0-1023,返回 206 即视为可达),
|
|
196
|
+
// 避免诊断把 3.6MB 全量目录下载一遍。
|
|
195
197
|
const r = await curlWithTlsFallback([
|
|
196
198
|
'-sS', '-o', discard, '-w', '%{http_code}',
|
|
199
|
+
...(range !== undefined ? ['-r', range] : []),
|
|
197
200
|
'--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))),
|
|
198
201
|
url,
|
|
199
202
|
], proxyEnv(proxy), timeoutMs)
|
|
@@ -212,11 +215,12 @@ async function curlProbe(url: string, proxy: string, timeoutMs: number): Promise
|
|
|
212
215
|
/**
|
|
213
216
|
* 探测一个 HTTPS 目标:proxy 非空注入给 curl 子进程走代理,否则直连。
|
|
214
217
|
* 供系统诊断 /diagnostics 使用;目标 URL 非法返回不可达。
|
|
218
|
+
* range(如 '0-1023')只取前 N 字节,用于探大文件端点而不下载全量。
|
|
215
219
|
*/
|
|
216
|
-
export function probeUrl(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult> {
|
|
220
|
+
export function probeUrl(url: string, proxy: string, timeoutMs: number, range?: string): Promise<ProbeResult> {
|
|
217
221
|
let target: URL
|
|
218
222
|
try { target = new URL(url) } catch { return Promise.resolve(probeFail()) }
|
|
219
|
-
return curlProbe(target.href, proxy, timeoutMs)
|
|
223
|
+
return curlProbe(target.href, proxy, timeoutMs, range)
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
/**
|
|
@@ -284,8 +288,9 @@ export async function finalUrl(url: string, proxy: string, timeoutMs: number): P
|
|
|
284
288
|
export function fetchViaCurl(
|
|
285
289
|
url: string, proxy: string, timeoutMs: number,
|
|
286
290
|
): Promise<{ ok: boolean; body: string; reason: ProbeFailReason | null }> {
|
|
287
|
-
// 同样用 -sS:正文之外还要 stderr 里的 TLS 失败线索(被 stderr
|
|
288
|
-
|
|
291
|
+
// 同样用 -sS:正文之外还要 stderr 里的 TLS 失败线索(被 stderr 收集,不外泄给用户)。
|
|
292
|
+
// --compressed:目录 JSON 约 3.6MB,源站支持 gzip,压缩后弱网下也能在超时内拉完。
|
|
293
|
+
const args = ['-sS', '--compressed', '--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))), url]
|
|
289
294
|
return curlWithTlsFallback(args, proxyEnv(proxy), timeoutMs).then((r) => ({
|
|
290
295
|
ok: r.code === 0,
|
|
291
296
|
body: r.stdout,
|
|
@@ -49,6 +49,21 @@ export function profileDirectory(profile: string): string {
|
|
|
49
49
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** profile 的 dsh.profile.bundles 是否仍包含指定包:取消勾选后 bundle patch 不再执行。
|
|
53
|
+
* 没有该清单或读取失败时返回 true —— 不归我们判断的情况不报警。 */
|
|
54
|
+
export function bundleIncludes(profile: string, name: string): boolean {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'package.json'), 'utf8')) as {
|
|
57
|
+
dsh?: { profile?: { bundles?: unknown } }
|
|
58
|
+
}
|
|
59
|
+
const bundles = pkg.dsh?.profile?.bundles
|
|
60
|
+
if (!Array.isArray(bundles)) return true
|
|
61
|
+
return bundles.includes(name)
|
|
62
|
+
} catch {
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
/** 把仓库身份转成规范的 GitHub 目标(git+https),仓库不合法时返回 null。
|
|
53
68
|
* 带 ref 时按 Git ref(`#tag` / `#commit`)拼接;安装通道已不再使用 git 目标,此函数仅做身份规范化。 */
|
|
54
69
|
export function githubTarget(repo: string, ref?: string | null): string | null {
|