dsh-plugin 2.0.5 → 2.1.0
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 +11 -11
- package/README.md +11 -11
- package/client/client.js +418 -317
- package/client/client.js.map +1 -1
- package/lib/http/install.js +42 -11
- package/lib/http/settings.js +1 -1
- package/lib/services/install/decode-output.js +23 -0
- package/lib/services/install/operation-log.js +61 -11
- package/lib/services/install/preflight.js +36 -0
- package/lib/services/install/registry-fallback.js +89 -0
- package/lib/services/install/task-queue.js +84 -34
- package/lib/services/settings.js +1 -0
- package/lib/types/services/install/decode-output.d.ts +4 -0
- package/lib/types/services/install/install-types.d.ts +4 -0
- package/lib/types/services/install/operation-log.d.ts +10 -4
- package/lib/types/services/install/preflight.d.ts +11 -0
- package/lib/types/services/install/registry-fallback.d.ts +18 -0
- package/lib/types/services/install/task-queue.d.ts +18 -0
- package/lib/types/services/settings.d.ts +3 -1
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +11 -2
- package/src/client/components/modals/NotificationsModal.tsx +8 -1
- package/src/client/components/modals/modals.tsx +9 -1
- package/src/client/components/views/SettingsView.tsx +10 -0
- package/src/client/hooks/useCatalog.ts +10 -8
- package/src/client/hooks/useSettings.ts +4 -1
- package/src/client/hooks/useTaskQueue.ts +7 -1
- package/src/client/locales.ts +12 -0
- package/src/client/logic/failures.ts +17 -1
- package/src/client/logic/installed.ts +10 -6
- package/src/client/logic/release-age.ts +40 -0
- package/src/client/logic/urls.ts +4 -1
- package/src/client/types.ts +13 -0
- package/src/server/http/install.ts +46 -11
- package/src/server/http/settings.ts +1 -1
- package/src/server/services/install/decode-output.ts +22 -0
- package/src/server/services/install/install-types.ts +4 -0
- package/src/server/services/install/operation-log.ts +58 -11
- package/src/server/services/install/preflight.ts +37 -0
- package/src/server/services/install/registry-fallback.ts +89 -0
- package/src/server/services/install/task-queue.ts +91 -32
- package/src/server/services/settings.ts +4 -1
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 从 DSH 输出里解析 operation 日志路径(`diagnostics: <path>`)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 去掉首尾引号与句末标点(不同版本/语言的摘要行尾部可能带 `。`、`.`、`)`);
|
|
4
|
+
* 相对路径原样返回,由读取方按 profile 目录解析。
|
|
5
5
|
*/
|
|
6
6
|
export declare function operationLogPathOf(output: string): string | null;
|
|
7
7
|
/**
|
|
8
8
|
* 回捞 operation 日志尾部,作为失败详情返回;拿不到(无路径 / 文件不存在 / 读失败 /
|
|
9
|
-
*
|
|
9
|
+
* 内容为空 / 路径越界)返回 null。尾部截到与子进程输出相同的上限。
|
|
10
10
|
*/
|
|
11
|
-
export declare function backfillOperationLog(output: string): string | null;
|
|
11
|
+
export declare function backfillOperationLog(output: string, profileDir?: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* 输出里是否有 dsh 自己的内容 —— 只有 `[exit N]`、`[install-channel]`、`[timed out]`
|
|
14
|
+
* 这类本队列自己打的标记时返回 false。用于把「宿主什么都没回显」的失败显式留痕,
|
|
15
|
+
* 否则上报正文只剩退出码,无从排查。
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasSubstantiveOutput(output: string): boolean;
|
|
12
18
|
/**
|
|
13
19
|
* 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
|
|
14
20
|
* 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 缺 git 的失败标记:前端按 `[git-missing]` 归类并给「安装 git」指引(乱码免疫)。 */
|
|
2
|
+
export declare const GIT_MISSING_DETAIL = "[git-missing] the git CLI was not found on this machine (install git and add it to the PATH, then retry)";
|
|
3
|
+
/** 命令存在性探测:which/where 找 PATH 里的命令。
|
|
4
|
+
* 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
|
|
5
|
+
export declare function commandOnPath(cmd: string, env?: NodeJS.ProcessEnv): boolean;
|
|
6
|
+
/** 该安装目标是否需要 git:github: / git+ / git@ 与 owner/repo 简写都要先 clone;
|
|
7
|
+
* release 直链(https://…releases/download/…tgz)由 pnpm 直接下载,不需要 git。
|
|
8
|
+
* owner/repo 段首必须是字母数字:`./x`、`../x` 这类相对路径是本地目录,不能误判。 */
|
|
9
|
+
export declare function needsGit(target: string): boolean;
|
|
10
|
+
/** git 源安装前置检查:需要 git 但本机 PATH 里没有 → 返回 [git-missing] 文本;否则 null。 */
|
|
11
|
+
export declare function gitMissingDetail(target: string, env?: NodeJS.ProcessEnv): string | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** 去掉尾部斜杠,避免同一源因写法不同被当成两个候选。 */
|
|
2
|
+
export declare function normalizeRegistry(value: string): string;
|
|
3
|
+
/** 是否属于允许自动切换的公共源白名单。 */
|
|
4
|
+
export declare function isPublicRegistry(value: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
|
|
7
|
+
* 只有公共源才追加后备(最多 3 个);关闭降级或源不在白名单时只返回它自己。
|
|
8
|
+
*/
|
|
9
|
+
export declare function registryCandidates(configured: string, userRegistry: string, enabled: boolean): string[];
|
|
10
|
+
/** 本机 npm 实际生效的源(含 profile 目录的 .npmrc);读不到返回空串。 */
|
|
11
|
+
export declare function userNpmRegistry(cwd: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* 传输层失败才值得换源:超时、连接重置、DNS、5xx。
|
|
14
|
+
* 404 / 版本不存在 / 鉴权失败必须保持原样 —— 换源只会掩盖作者侧问题或装到滞后版本。
|
|
15
|
+
*/
|
|
16
|
+
export declare function isTransportFailure(text: string): boolean;
|
|
17
|
+
/** 日志里展示用的源主机名(不打印可能带凭据的完整 URL)。 */
|
|
18
|
+
export declare function registryHost(value: string): string;
|
|
@@ -38,6 +38,15 @@ export declare function cancelTask(id: number): boolean;
|
|
|
38
38
|
* 导出供单测覆盖路径推导。
|
|
39
39
|
*/
|
|
40
40
|
export declare function desktopCliBootstrap(entry: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
|
|
43
|
+
* 其次更新走 `@latest`,git 源与全局 npm 安装保持原样。
|
|
44
|
+
*/
|
|
45
|
+
export declare function npmInstallTargetOf(target: string, opts: {
|
|
46
|
+
update: boolean;
|
|
47
|
+
global: boolean;
|
|
48
|
+
pinned?: string;
|
|
49
|
+
}): string;
|
|
41
50
|
/**
|
|
42
51
|
* 插件变更入队并返回其任务。任务严格串行:队列 worker 只在上一个结束后才启动下一个。
|
|
43
52
|
* 完成前可通过 `getTask(id)` / `activeTask()` 查看进度。
|
|
@@ -62,6 +71,11 @@ export declare function startPluginMutation(options: {
|
|
|
62
71
|
/** 自定义安装入口渠道(NPM 包卡片):任务输出行与系统日志据此溯源「从哪个入口发起」;
|
|
63
72
|
* 目录插件安装不传 */
|
|
64
73
|
installChannel?: 'npm';
|
|
74
|
+
/** npm 源候选(首选在前):传输层失败时按顺序换源重试,空/单个表示不降级 */
|
|
75
|
+
registries?: string[];
|
|
76
|
+
/** 精确安装 spec(如 `dsh-plugin@2.0.5`):Hub 自我更新提示的是已过供应链门禁的版本,
|
|
77
|
+
* 必须钉住安装,不能再用 @latest 被带回门禁内的最新版 */
|
|
78
|
+
installSpec?: string;
|
|
65
79
|
}): InstallTask;
|
|
66
80
|
/**
|
|
67
81
|
* 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
|
|
@@ -101,4 +115,8 @@ export declare function runPluginMutation(options: {
|
|
|
101
115
|
loader?: LoaderHandle;
|
|
102
116
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
103
117
|
globalNpm?: string[];
|
|
118
|
+
/** npm 源候选(首选在前):传输层失败时按顺序换源重试 */
|
|
119
|
+
registries?: string[];
|
|
120
|
+
/** 精确安装 spec(如 `dsh-plugin@2.0.5`):见 startPluginMutation 同名说明 */
|
|
121
|
+
installSpec?: string;
|
|
104
122
|
}): Promise<InstallResult>;
|
|
@@ -3,8 +3,10 @@ export interface HubSettings {
|
|
|
3
3
|
checkUpdatesOnStart: boolean;
|
|
4
4
|
/** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
|
|
5
5
|
proxy: string;
|
|
6
|
-
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm
|
|
6
|
+
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置) */
|
|
7
7
|
npmRegistry: string;
|
|
8
|
+
/** 公共源传输失败时自动换下一个镜像重试(自定义/内网源不切换) */
|
|
9
|
+
registryFallback: boolean;
|
|
8
10
|
/** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
|
|
9
11
|
enableNpmInstall: boolean;
|
|
10
12
|
/** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log;可填目录或 .log 文件路径) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 10000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):10000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -76,6 +76,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
76
76
|
const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
|
|
77
77
|
/** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
|
|
78
78
|
const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
|
|
79
|
+
/** Hub 自我更新指定的精确版本:连续发版时提示的是「已过供应链门禁」的旧一版,
|
|
80
|
+
* 安装必须钉在该版本上,否则又会被 @latest 带回门禁内的最新版。 */
|
|
81
|
+
const [confirmPinVersion, setConfirmPinVersion] = useState<string | null>(null)
|
|
79
82
|
/** 命令行安装确认弹窗:记录待安装的裸目标(npm 包名)——
|
|
80
83
|
* 与应用商店同一套确认/进度/结果弹窗,提供时 plugin 传 null 走 customTarget 模式 */
|
|
81
84
|
const [confirmCustomTarget, setConfirmCustomTarget] = useState<string | null>(null)
|
|
@@ -450,6 +453,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
450
453
|
onInstall: (p, opts) => {
|
|
451
454
|
setInstallDone(false)
|
|
452
455
|
setConfirmIsUpdate(opts?.update ?? false)
|
|
456
|
+
setConfirmPinVersion(null)
|
|
453
457
|
// 与卸载一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务而禁用安装按钮
|
|
454
458
|
queue.clearModalTask()
|
|
455
459
|
setConfirmPlugin(p)
|
|
@@ -484,6 +488,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
484
488
|
setDetailItem(null)
|
|
485
489
|
setInstallDone(false)
|
|
486
490
|
setConfirmIsUpdate(true)
|
|
491
|
+
setConfirmPinVersion(null)
|
|
487
492
|
queue.clearModalTask()
|
|
488
493
|
setConfirmPlugin(item.plugin)
|
|
489
494
|
},
|
|
@@ -547,7 +552,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
547
552
|
// 暂缓提示显示同一标题但只给「稍后再说」,无更新显示当前版本 + 记录 + 「已是最新」。
|
|
548
553
|
// 版本号与本机版本一律由 hubUpdateState / hubCurrentVersion 决定,不用远端版号冒充本机。
|
|
549
554
|
showHubUpdate && h(HubUpdateModal, {
|
|
550
|
-
|
|
555
|
+
// 提示目标是「已过供应链门禁」的版本,可能不是 latest —— 展示它与它的变更记录
|
|
556
|
+
info: catalog.hubUpdateTarget ?? catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
|
|
551
557
|
state: catalog.hubUpdateState,
|
|
552
558
|
currentVersion: catalog.hubCurrentVersion,
|
|
553
559
|
lang,
|
|
@@ -559,6 +565,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
559
565
|
if (hubEntry) {
|
|
560
566
|
setInstallDone(false)
|
|
561
567
|
setConfirmIsUpdate(true)
|
|
568
|
+
// 钉住本次提示的版本:最新版可能仍在门禁内,@latest 会被 pnpm 拒绝
|
|
569
|
+
setConfirmPinVersion(catalog.hubUpdateTarget?.version ?? null)
|
|
562
570
|
queue.clearModalTask()
|
|
563
571
|
setConfirmPlugin(hubEntry)
|
|
564
572
|
}
|
|
@@ -595,6 +603,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
595
603
|
setShowNotifications(false)
|
|
596
604
|
setInstallDone(false)
|
|
597
605
|
setConfirmIsUpdate(true)
|
|
606
|
+
setConfirmPinVersion(null)
|
|
598
607
|
queue.clearModalTask()
|
|
599
608
|
setConfirmPlugin(plugin)
|
|
600
609
|
},
|
|
@@ -673,7 +682,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
673
682
|
if (reload) window.location.reload()
|
|
674
683
|
},
|
|
675
684
|
onCopy: () => copyCommand(confirmPlugin),
|
|
676
|
-
onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true } : undefined),
|
|
685
|
+
onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true, pinVersion: confirmPinVersion ?? undefined } : undefined),
|
|
677
686
|
// 重启:结果视图「立即重启」也会中断进行中的任务 → 先弹重启确认弹窗
|
|
678
687
|
onRestart: () => setShowRestartConfirm(true),
|
|
679
688
|
}),
|
|
@@ -14,7 +14,7 @@ import type { MouseEvent } from 'react'
|
|
|
14
14
|
import styles from '../../styles/Modal.module.css'
|
|
15
15
|
import type { EnvInfo, Translate } from '../../types.ts'
|
|
16
16
|
import type { NotificationRecord } from '../../logic/failures.ts'
|
|
17
|
-
import { actionOfRecord, classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
17
|
+
import { actionOfRecord, classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf } from '../../logic/failures.ts'
|
|
18
18
|
import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
|
|
19
19
|
import { pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
|
|
20
20
|
import { CloseIcon } from '../ui/icons.tsx'
|
|
@@ -397,6 +397,13 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
397
397
|
// 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
|
|
398
398
|
return h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
|
|
399
399
|
}
|
|
400
|
+
if (kind === 'registryAuth') {
|
|
401
|
+
// npm 源返回 401/403:私有源需要凭据,或当前网络/IP 不被允许 —— 本机环境问题,
|
|
402
|
+
// 给「配认证 / 换回官方源 / 切公共镜像」指引,不引导提 Issue
|
|
403
|
+
const host = authRegistryHostOf(r.message)
|
|
404
|
+
return h('div', { className: styles.failPrepareHint },
|
|
405
|
+
t(host ? 'failRegistryAuthHint' : 'failRegistryAuthHintNoHost', host ? { host } : undefined))
|
|
406
|
+
}
|
|
400
407
|
if (kind === 'network') {
|
|
401
408
|
// 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
402
409
|
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
@@ -15,7 +15,7 @@ import { CloseIcon, ConfirmIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
|
|
|
15
15
|
import { ProgressView } from './ProgressView.tsx'
|
|
16
16
|
import { installCommandOf } from '../../logic/install-command.ts'
|
|
17
17
|
import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
|
|
18
|
-
import { classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
18
|
+
import { classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf } from '../../logic/failures.ts'
|
|
19
19
|
import type { FailureAction } from '../../logic/failures.ts'
|
|
20
20
|
|
|
21
21
|
/** 完成结果视图:绿色对勾 + 标题/描述;
|
|
@@ -530,6 +530,14 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
530
530
|
// 请求来源没通过本地 hub 服务的校验(服务端 403 untrusted origin):请求根本没进
|
|
531
531
|
// 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
|
|
532
532
|
? h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
|
|
533
|
+
: failureKind === 'registryAuth'
|
|
534
|
+
// npm 源返回 401/403:私有源需要凭据,或当前网络/IP 不被允许 —— 本机环境问题,
|
|
535
|
+
// 给「配认证 / 换回官方源 / 切公共镜像」指引,不引导提 Issue
|
|
536
|
+
? (() => {
|
|
537
|
+
const host = authRegistryHostOf(message)
|
|
538
|
+
return h('div', { className: styles.failPrepareHint },
|
|
539
|
+
t(host ? 'failRegistryAuthHint' : 'failRegistryAuthHintNoHost', host ? { host } : undefined))
|
|
540
|
+
})()
|
|
533
541
|
: failureKind === 'network'
|
|
534
542
|
// 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
535
543
|
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
@@ -176,6 +176,16 @@ export function SettingsView({ t, settings, update, reset, env, onCopy, openSect
|
|
|
176
176
|
className: `${styles.controlDropdown} ${dropdownStyles.dropdownFill}`,
|
|
177
177
|
}),
|
|
178
178
|
}),
|
|
179
|
+
// 自动换源:只在公共源之间切换;自定义/内网源不降级,避免私有包名泄露
|
|
180
|
+
h(SettingRow, {
|
|
181
|
+
title: t('settingsRegistryFallback'),
|
|
182
|
+
desc: t('settingsRegistryFallbackDesc'),
|
|
183
|
+
children: h(Toggle, {
|
|
184
|
+
checked: settings.registryFallback,
|
|
185
|
+
onChange: (v) => update({ registryFallback: v }),
|
|
186
|
+
title: t('settingsRegistryFallback'),
|
|
187
|
+
}),
|
|
188
|
+
}),
|
|
179
189
|
// 代理地址:文本框独占一行,整行输入,便于粘贴长代理地址;停笔后实时探测连通性,
|
|
180
190
|
// 不通仅提示(可保存)—— 用户可能先填地址后开代理
|
|
181
191
|
h(SettingRow, {
|
|
@@ -16,7 +16,7 @@ import type { SortKey } from '../logic/constants.ts'
|
|
|
16
16
|
import { fetchCatalog, fetchStats } from '../data/catalog.ts'
|
|
17
17
|
import { fetchHubAbout, fetchHubUpdate } from '../data/hub.ts'
|
|
18
18
|
import { fetchInstalled, fetchNpmReleaseAge } from '../data/host.ts'
|
|
19
|
-
import { isReleaseTooFresh } from '../logic/release-age.ts'
|
|
19
|
+
import { isReleaseTooFresh, pickHubUpdate } from '../logic/release-age.ts'
|
|
20
20
|
import {
|
|
21
21
|
hasUpdateOf, installedItemsOf, installedNameOf, installedVersionOf, npmPackageOf,
|
|
22
22
|
} from '../logic/installed.ts'
|
|
@@ -272,13 +272,14 @@ export function useCatalog(lang: LocaleId) {
|
|
|
272
272
|
* 落回「无更新」——回落会让弹窗在本机版本落后时谎称「已是最新」。
|
|
273
273
|
*/
|
|
274
274
|
const hubCurrentVersion = PLUGIN_VERSION || versions[HUB_REPO]?.version || ''
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
275
|
+
// 候选含 recentVersions:最新版还在门禁内时回退到「更早、但已过门禁」的版本,
|
|
276
|
+
// 避免连续快速发版把所有提示窗口都吞掉(见 pickHubUpdate)。
|
|
277
|
+
const hubUpdateResult = hubUpdateInfo === null
|
|
278
|
+
? { state: 'none' as HubUpdateState, target: null }
|
|
279
|
+
: pickHubUpdate(hubUpdateInfo, hubCurrentVersion, Date.now())
|
|
280
|
+
const hubUpdateState: HubUpdateState = hubUpdateResult.state
|
|
281
|
+
/** 本次提示的目标版本(available 时有值;pending 时是暂缓中的最高版本) */
|
|
282
|
+
const hubUpdateTarget = hubUpdateResult.target
|
|
282
283
|
const hubHasUpdate = hubUpdateState === 'available'
|
|
283
284
|
|
|
284
285
|
/** 当前分类下的插件(「全部」时为整个目录)。 */
|
|
@@ -334,6 +335,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
334
335
|
hasUpdate,
|
|
335
336
|
hubHasUpdate,
|
|
336
337
|
hubUpdateState,
|
|
338
|
+
hubUpdateTarget,
|
|
337
339
|
releaseAgeReady,
|
|
338
340
|
hubCurrentVersion,
|
|
339
341
|
hubUpdateInfo,
|
|
@@ -15,8 +15,10 @@ export interface HubSettings {
|
|
|
15
15
|
checkUpdatesOnStart: boolean
|
|
16
16
|
/** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
|
|
17
17
|
proxy: string
|
|
18
|
-
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm
|
|
18
|
+
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置) */
|
|
19
19
|
npmRegistry: string
|
|
20
|
+
/** 公共源传输失败时自动换下一个镜像重试(自定义/内网源不切换) */
|
|
21
|
+
registryFallback: boolean
|
|
20
22
|
/** 启用命令行安装 NPM 包(默认开) */
|
|
21
23
|
enableNpmInstall: boolean
|
|
22
24
|
/** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log) */
|
|
@@ -27,6 +29,7 @@ export const DEFAULT_SETTINGS: HubSettings = {
|
|
|
27
29
|
checkUpdatesOnStart: true,
|
|
28
30
|
proxy: '',
|
|
29
31
|
npmRegistry: '',
|
|
32
|
+
registryFallback: true,
|
|
30
33
|
enableNpmInstall: true,
|
|
31
34
|
logPath: '',
|
|
32
35
|
}
|
|
@@ -433,6 +433,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
433
433
|
source: 'catalog' | 'custom'
|
|
434
434
|
/** 更新已安装目标(放行 add 覆盖重装) */
|
|
435
435
|
update?: boolean
|
|
436
|
+
/** 钉住精确版本(Hub 自我更新用):最新版可能仍在 pnpm 供应链门禁内,
|
|
437
|
+
* 必须安装「已过门禁」的那个版本,不能再用 @latest。 */
|
|
438
|
+
pinVersion?: string
|
|
436
439
|
/** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
|
|
437
440
|
globalNpm?: string[]
|
|
438
441
|
/** 自定义安装入口渠道(NPM 包卡片):随请求上报服务端落日志,
|
|
@@ -473,6 +476,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
473
476
|
display: input.repo,
|
|
474
477
|
source: input.source,
|
|
475
478
|
mode: input.update ? 'update' : undefined,
|
|
479
|
+
// 钉住版本(Hub 自我更新):服务端据此执行 `add <pkg>@<version>` 而不是 @latest
|
|
480
|
+
...(input.pinVersion ? { version: input.pinVersion } : {}),
|
|
476
481
|
// 界面语言带给服务端:安装前网络探测等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
|
|
477
482
|
...(langKey ? { lang: langKey } : {}),
|
|
478
483
|
// 安装入口渠道(只可能是 npm):服务端日志/报错提示据此溯源
|
|
@@ -523,7 +528,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
523
528
|
}
|
|
524
529
|
|
|
525
530
|
/** 目录插件安装:安装目标取目录数据的 npm 包名,走 catalog 白名单。 */
|
|
526
|
-
const installNow = async (p: HubPlugin, opts?: { update?: boolean }) => {
|
|
531
|
+
const installNow = async (p: HubPlugin, opts?: { update?: boolean; pinVersion?: string }) => {
|
|
527
532
|
const repo = p.source?.repo ?? ''
|
|
528
533
|
if (!repo) return
|
|
529
534
|
// 目录条目必带 npm 包名:直接用包名安装(用户无感知)。
|
|
@@ -538,6 +543,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
538
543
|
command: installCommandOf(p, true),
|
|
539
544
|
source: 'catalog',
|
|
540
545
|
update: opts?.update,
|
|
546
|
+
pinVersion: opts?.pinVersion,
|
|
541
547
|
})
|
|
542
548
|
}
|
|
543
549
|
|
package/src/client/locales.ts
CHANGED
|
@@ -117,6 +117,8 @@ export const zh = {
|
|
|
117
117
|
proxyCheckRevocation: '证书吊销检查受阻(Windows Schannel 取不到 CRL/OCSP),握手被中断;属本机 TLS 环境问题,非代理地址不通',
|
|
118
118
|
settingsMirror: 'npm 镜像源',
|
|
119
119
|
settingsMirrorDesc: '默认跟随本机 npm 配置;官方源慢可换国内镜像',
|
|
120
|
+
settingsRegistryFallback: '连不上时自动换源',
|
|
121
|
+
settingsRegistryFallbackDesc: '公共源超时或被拦截时,自动改用下一个镜像重试(最多 3 个);自定义/内网源不会切换,避免私有包名外泄',
|
|
120
122
|
mirrorNone: '未配置(跟随本机 npm 配置)',
|
|
121
123
|
mirrorOfficial: '官方源 registry.npmjs.org',
|
|
122
124
|
mirrorNpmmirror: '阿里云 npmmirror',
|
|
@@ -386,6 +388,8 @@ export const zh = {
|
|
|
386
388
|
failReason_pluginMissingDep: '插件声明依赖的包在官方 npm 源上不存在(已删除或从未发布)。',
|
|
387
389
|
failKind_registryVersionStale: '该版本在所用源上不存在',
|
|
388
390
|
failReason_registryVersionStale: '实际安装用的源还没同步到这个版本,或版本号写错。',
|
|
391
|
+
failKind_registryAuth: 'npm 源拒绝了访问',
|
|
392
|
+
failReason_registryAuth: 'registry 返回 401/403:这个源需要登录凭据,或当前网络不被允许。',
|
|
389
393
|
failKind_pnpmLinkedDirMissing: 'profile 里有失效的本地依赖',
|
|
390
394
|
failReason_pnpmLinkedDirMissing: '有一条指向本机不存在目录的 link/file 依赖,pnpm 每次解析都卡住。',
|
|
391
395
|
failKind_fileLocked: '文件被其他程序占用',
|
|
@@ -413,6 +417,8 @@ export const zh = {
|
|
|
413
417
|
failKind_hub: '出现未知错误',
|
|
414
418
|
failReason_hub: '我们还没能识别这条报错,你可以一键反馈给我们。',
|
|
415
419
|
failRegistryVersionStaleHint: '安装器调用 pnpm 时报「找不到匹配的版本」(ERR_PNPM_NO_MATCHING_VERSION):要装的这个版本在实际执行安装的 npm 源里查不到。常见原因是你设置的 npmRegistry 与本机 ~/.npmrc 用的不是同一个源——官方源刚发布、镜像还没同步上来(快则几分钟、慢则几小时),也可能只是版本号写错了。这不是插件本身的问题。可在插件市场设置里把 npmRegistry 改成与本机 ~/.npmrc 一致(或反过来),或那一次显式走官方源(https://registry.npmjs.org)、改用离线包后重试;若该版本刚发布不久,等镜像同步后重试即可。',
|
|
420
|
+
failRegistryAuthHint: '安装器访问 npm 源时被拒绝(HTTP 401/403):这个源需要登录凭据,或不允许当前网络/IP 访问。如果你在用公司或私有源,请先配好认证(npm login,或在 ~/.npmrc 写入 //{host}/:_authToken=…);如果不需要私有源,把 registry 换回官方源 https://registry.npmjs.org,或在插件市场设置里切换到公共镜像后重试。这不是插件本身的问题。',
|
|
421
|
+
failRegistryAuthHintNoHost: '安装器访问 npm 源时被拒绝(HTTP 401/403):这个源需要登录凭据,或不允许当前网络/IP 访问。如果你在用公司或私有源,请先配好认证(npm login,或在 ~/.npmrc 写入 //<registry>/:_authToken=…);如果不需要私有源,把 registry 换回官方源 https://registry.npmjs.org,或在插件市场设置里切换到公共镜像后重试。这不是插件本身的问题。',
|
|
416
422
|
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 条目或整个锁文件),或把该目录恢复回原路径后重试。',
|
|
417
423
|
failFileLockedHint: '安装过程中 pnpm 无法替换 profile 里的文件:有程序正占用着它(Windows 常见报错 os error 32「另一个程序正在使用此文件」)。通常是 DeepSeek Harness 自身或杀毒软件的实时扫描持有文件句柄。这不是插件本身的问题——请完全退出 DeepSeek Harness(Windows 可在任务管理器确认进程已结束,必要时临时关闭杀毒软件实时防护)后重试。',
|
|
418
424
|
failAccessDeniedHint: '安装过程中 pnpm 写入 profile 里的文件被系统拒绝访问(Windows 常见报错 os error 5「拒绝访问」,英文为 Access is denied;Node 下的 EPERM / EACCES 同理)。常见原因是该文件或目录正被其他进程占用、文件带只读属性、目录权限受限,或杀毒软件实时防护拦截了写入——这与「文件正被占用」(os error 32)同属本机文件系统问题,任何插件装进该目录都会一样失败,不是插件本身的问题。请完全退出 DeepSeek Harness(Windows 可在任务管理器确认进程已结束,必要时临时关闭杀毒软件实时防护)后重试;若仍然失败,请检查 profile 目录及其文件是否被设为只读,并以管理员身份重新运行。',
|
|
@@ -583,6 +589,8 @@ export const en = {
|
|
|
583
589
|
proxyCheckRevocation: 'Certificate revocation check blocked (Windows Schannel could not fetch CRL/OCSP), so the handshake was aborted — a local TLS issue, not a bad proxy address',
|
|
584
590
|
settingsMirror: 'npm mirror',
|
|
585
591
|
settingsMirrorDesc: 'Unset by default — follows local npm config; pick a domestic mirror if slow',
|
|
592
|
+
settingsRegistryFallback: 'Switch mirrors on failure',
|
|
593
|
+
settingsRegistryFallbackDesc: 'When a public registry times out or is blocked, retry with the next mirror (up to 3). Custom/intranet registries are never switched, so private package names do not leak.',
|
|
586
594
|
mirrorNone: 'Unset (follow local npm config)',
|
|
587
595
|
mirrorOfficial: 'Official registry.npmjs.org',
|
|
588
596
|
mirrorNpmmirror: 'Alibaba npmmirror',
|
|
@@ -850,6 +858,8 @@ export const en = {
|
|
|
850
858
|
failReason_pluginMissingDep: 'A package the plugin depends on does not exist on the official npm registry.',
|
|
851
859
|
failKind_registryVersionStale: 'this version is not available on the registry in use',
|
|
852
860
|
failReason_registryVersionStale: 'The registry that actually performs the install has not synced this version yet, or the version is mistyped.',
|
|
861
|
+
failKind_registryAuth: 'the npm registry rejected access',
|
|
862
|
+
failReason_registryAuth: 'The registry returned 401/403: it needs credentials, or the current network is not allowed.',
|
|
853
863
|
failKind_pnpmLinkedDirMissing: 'dangling local dependency in the profile',
|
|
854
864
|
failReason_pnpmLinkedDirMissing: 'A link/file dependency points at a directory that no longer exists, so pnpm stalls on it.',
|
|
855
865
|
failKind_fileLocked: 'a file is held by another program',
|
|
@@ -877,6 +887,8 @@ export const en = {
|
|
|
877
887
|
failKind_hub: 'unknown error',
|
|
878
888
|
failReason_hub: 'We could not identify this error yet — you can report it to us in one click.',
|
|
879
889
|
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.',
|
|
890
|
+
failRegistryAuthHint: 'The installer was denied access to the npm registry (HTTP 401/403): that registry requires credentials, or it does not allow the current network/IP. If you are using a company or private registry, configure authentication first (npm login, or write //{host}/:_authToken=… into ~/.npmrc). If you do not need a private registry, point the registry back to the official one (https://registry.npmjs.org), or switch to a public mirror in the hub settings, then retry. This is not a problem with the plugin itself.',
|
|
891
|
+
failRegistryAuthHintNoHost: 'The installer was denied access to the npm registry (HTTP 401/403): that registry requires credentials, or it does not allow the current network/IP. If you are using a company or private registry, configure authentication first (npm login, or write //<registry>/:_authToken=… into ~/.npmrc). If you do not need a private registry, point the registry back to the official one (https://registry.npmjs.org), or switch to a public mirror in the hub settings, then retry. This is not a problem with the plugin itself.',
|
|
880
892
|
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.',
|
|
881
893
|
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.',
|
|
882
894
|
failAccessDeniedHint: 'During install, Windows denied pnpm access to a file inside the profile (this shows as os error 5, "Access is denied"; on Node the equivalent codes are EPERM / EACCES). Typical causes: the file or directory is held by another process, the file is marked read-only, the directory permissions are restricted, or a real-time antivirus scan blocked the write. This is the same class of local file-system problem as a locked file (os error 32) — no plugin can be installed into that directory, so it 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. If it still fails, check whether the profile directory or its files are read-only and run as administrator.',
|
|
@@ -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' | 'buildToolchainMissing' | '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' | 'registryAuth' | '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'
|
|
@@ -253,6 +253,9 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
253
253
|
if (/os error 112\b|os error 19\b|ERROR_DISK_FULL|ERROR_WRITE_PROTECT|\bENOSPC\b|\bEROFS\b|\bEMFILE\b|\bENFILE\b|no space left on device|read-only file system|too many open files/i.test(message)) return 'fsUnavailable'
|
|
254
254
|
// 构建脚本被 allowBuilds 拦截。必须在 network 之前判:日志尾部混重试残留的连接失败特征
|
|
255
255
|
if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:|ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED/i.test(message)) return 'pnpmIgnoredBuild'
|
|
256
|
+
// npm 源拒绝了访问(401/403):私有源要登录凭据,或当前网络/IP 不被源允许。
|
|
257
|
+
// 必须在 network 之前判:pnpm 的 401/403 日志也常带 "fetch failed" 字样,先判网络会误报成「网络不通」
|
|
258
|
+
if (/ERR_PNPM_FETCH_40[13]\b|\bE40[13]\b|(?:Unauthorized|Forbidden)\s*-\s*40[13]\b/i.test(message)) return 'registryAuth'
|
|
256
259
|
// 网络/代理/源配置问题。必须在 prepare 之前判:git fetch 失败常被外层包成 "Command failed"
|
|
257
260
|
// tarball 下载失败的括号里可能是数字码(pnpm 自己的)或符号码(undici 的 UND_ERR_*),两种都要认
|
|
258
261
|
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'
|
|
@@ -353,6 +356,9 @@ export function summarizeError(message: string, maxChars: number = MAX_CORE_CHAR
|
|
|
353
356
|
}
|
|
354
357
|
let out: string
|
|
355
358
|
if (core.length === 0) {
|
|
359
|
+
// 整段本来就是短消息时直接给全量:head+tail 兜底会把同一段重复两遍,正文反而更难读
|
|
360
|
+
const whole = message.trim()
|
|
361
|
+
if (whole.length <= maxChars) return whole
|
|
356
362
|
// 无关键行时退化为「头部 + 尾部」快照:优先保尾部完整,头部按预算压缩,总长不超上限
|
|
357
363
|
const tail = message.trimEnd().slice(-1000)
|
|
358
364
|
const sep = '\n…\n'
|
|
@@ -431,3 +437,13 @@ export function registryHostOf(message: string): string | null {
|
|
|
431
437
|
const host = m[1].replace(/:\d+$/, '')
|
|
432
438
|
return PUBLIC_REGISTRY_HOSTS.has(host) ? null : host
|
|
433
439
|
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 鉴权失败(401/403)时被拒绝的源主机名:从 `ERR_PNPM_FETCH_401 GET https://host/pkg: Unauthorized - 401`
|
|
443
|
+
* 里取,取不到返回 null(提示文案退回通用说法,不拼出残缺句子)。
|
|
444
|
+
*/
|
|
445
|
+
export function authRegistryHostOf(message: string): string | null {
|
|
446
|
+
const m = /ERR_PNPM_FETCH_40[13]\s+GET\s+https?:\/\/([^\s/:]+)/i.exec(message)
|
|
447
|
+
?? /GET\s+https?:\/\/([^\s/:]+)\S*\s*:?\s*(?:Unauthorized|Forbidden)\s*-\s*40[13]/i.exec(message)
|
|
448
|
+
return m === null ? null : m[1]
|
|
449
|
+
}
|
|
@@ -48,6 +48,12 @@ export interface InstalledItem {
|
|
|
48
48
|
dshCapable: boolean
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** 本地路径安装(file:/link:/绝对或相对路径):身份在本机,不能按 npm 包名认领目录条目。 */
|
|
52
|
+
function isLocalSpec(value: string): boolean {
|
|
53
|
+
const v = value.trim()
|
|
54
|
+
return /^(file|link):/i.test(v) || v.startsWith('.') || v.startsWith('/') || /^[A-Za-z]:[\\/]/.test(v)
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
/** 安装时记录的目录信号(按仓库小写 key 查表)。 */
|
|
52
58
|
function signalOf(repo: string | null, versions: Record<string, InstalledVersionSignal>): InstalledVersionSignal | null {
|
|
53
59
|
if (!repo) return null
|
|
@@ -66,15 +72,13 @@ export function installedNameOf(
|
|
|
66
72
|
for (const [name, spec] of Object.entries(installed)) {
|
|
67
73
|
if (repoFromInstallTarget(spec).toLowerCase() === needle) return name
|
|
68
74
|
}
|
|
69
|
-
// npm
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// 只认 npm 安装的依赖(spec 无法解析出仓库身份):git/本地安装的依赖身份由仓库匹配决定,
|
|
73
|
-
// 包名同名不代表同仓库——否则「git 装 A 仓库」会被「npm 包名同名但仓库不同」的目录条目
|
|
74
|
-
// 误认领成已安装(假阳性),installedItems 里出现同 key 双条目,排序一变化就渲染出幽灵行。
|
|
75
|
+
// 只认 npm 安装的依赖(spec 无法解析出仓库身份、也不是本地路径):git 安装的依赖身份由仓库
|
|
76
|
+
// 匹配决定,file:/link: 安装的本地插件身份在本机 —— 包名同名不代表同仓库,否则「本地装 A」
|
|
77
|
+
// 会被目录里同名的 B 认领,已安装页显示成 B 的信息。
|
|
75
78
|
const pkg = ((plugin.source?.npmPackage || versions[repo.toLowerCase()]?.npmPackage) ?? '').toLowerCase()
|
|
76
79
|
if (pkg) {
|
|
77
80
|
for (const [name, spec] of Object.entries(installed)) {
|
|
81
|
+
if (isLocalSpec(spec)) continue
|
|
78
82
|
if (isRepoLike(repoFromInstallTarget(spec))) continue
|
|
79
83
|
if (name.toLowerCase() === pkg) return name
|
|
80
84
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 供应链门禁的时间判定(纯函数):pnpm 会静默回退发布未满门槛的版本,
|
|
7
7
|
* 故发布太新的版本不该提示更新。Hub 自身更新与插件更新共用同一门槛。
|
|
8
8
|
*/
|
|
9
|
+
import type { HubUpdateInfo, HubUpdateState, HubUpdateVersion } from '../types.ts'
|
|
9
10
|
|
|
10
11
|
/** pnpm 供应链门禁:dsh profile 默认 minimumReleaseAge=1440(分钟);+30 分钟缓冲抵消网络/CDN 边缘时间偏差。 */
|
|
11
12
|
export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
|
|
@@ -17,3 +18,42 @@ export function isReleaseTooFresh(publishedAt: string | null | undefined, now: n
|
|
|
17
18
|
if (Number.isNaN(ms)) return false
|
|
18
19
|
return now - ms < MIN_RELEASE_AGE_MS
|
|
19
20
|
}
|
|
21
|
+
|
|
22
|
+
/** 版本号比较(逐段数字;缺失段按 0)。返回负数/0/正数,非法输入按相等处理。 */
|
|
23
|
+
export function compareVersions(a: string, b: string): number {
|
|
24
|
+
const parse = (v: string): number[] => v.split(/[.+-]/).map((part) => Number.parseInt(part, 10)).filter((n) => !Number.isNaN(n))
|
|
25
|
+
const va = parse(a)
|
|
26
|
+
const vb = parse(b)
|
|
27
|
+
const len = Math.max(va.length, vb.length)
|
|
28
|
+
for (let i = 0; i < len; i++) {
|
|
29
|
+
const d = (va[i] ?? 0) - (vb[i] ?? 0)
|
|
30
|
+
if (d !== 0) return d
|
|
31
|
+
}
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 选出本次真正该提示的更新版本。
|
|
37
|
+
*
|
|
38
|
+
* 只看最新版会漏更新:连续发版时间隔小于供应链门禁时,最新版永远「太新」,
|
|
39
|
+
* 提示被整体跳过。这里把最新版与 recentVersions 一起当候选,取「高于当前版本且已过门禁」
|
|
40
|
+
* 的最高版本;一个都没有但仍存在更高版本 → pending(暂缓,不谎称已是最新)。
|
|
41
|
+
*/
|
|
42
|
+
export function pickHubUpdate(info: HubUpdateInfo, currentVersion: string, now: number): { state: HubUpdateState; target: HubUpdateVersion | null } {
|
|
43
|
+
if (currentVersion === '') return { state: 'latest', target: null }
|
|
44
|
+
const seen = new Set<string>()
|
|
45
|
+
const candidates: HubUpdateVersion[] = []
|
|
46
|
+
for (const entry of [{ version: info.version, publishedAt: info.publishedAt, notes: info.notes }, ...(info.recentVersions ?? [])]) {
|
|
47
|
+
if (typeof entry.version !== 'string' || entry.version === '' || seen.has(entry.version)) continue
|
|
48
|
+
seen.add(entry.version)
|
|
49
|
+
candidates.push(entry)
|
|
50
|
+
}
|
|
51
|
+
const higher = candidates
|
|
52
|
+
.filter((c) => compareVersions(c.version, currentVersion) > 0)
|
|
53
|
+
.sort((a, b) => compareVersions(b.version, a.version))
|
|
54
|
+
if (higher.length === 0) return { state: 'latest', target: null }
|
|
55
|
+
const ready = higher.find((c) => !isReleaseTooFresh(c.publishedAt, now))
|
|
56
|
+
return ready !== undefined
|
|
57
|
+
? { state: 'available', target: ready }
|
|
58
|
+
: { state: 'pending', target: higher[0]! }
|
|
59
|
+
}
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -54,7 +54,8 @@ function reasonTitleOf(kind: FailureKind, action: FailureAction): string {
|
|
|
54
54
|
case 'pnpmPolicy': return `pnpm supply-chain policy blocked the ${word} on the user machine`
|
|
55
55
|
case 'pnpmUnusedPatch': return 'stale pnpm patch entry on the user machine'
|
|
56
56
|
case 'pnpmMissingDep': return 'dependency not found on the npm registry on the user machine'
|
|
57
|
-
case 'registryVersionStale': return
|
|
57
|
+
case 'registryVersionStale': return `requested version not available on the npm registry in use on the user machine`
|
|
58
|
+
case 'registryAuth': return `npm registry rejected the credentials for this ${word} on the user machine`
|
|
58
59
|
case 'pnpmLinkedDirMissing': return 'dangling local (link:) dependency in the user profile'
|
|
59
60
|
case 'fileLocked': return 'file locked by another process on the user machine'
|
|
60
61
|
case 'accessDenied': return 'file write access denied on the user machine'
|
|
@@ -101,6 +102,8 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
101
102
|
? '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'
|
|
102
103
|
: kind === 'network'
|
|
103
104
|
? 'network connectivity issue on the user side (DNS, proxy, firewall, or a blocked connection)'
|
|
105
|
+
: kind === 'registryAuth'
|
|
106
|
+
? 'the npm registry rejected the request with 401/403 — the configured registry needs credentials or blocks this network (local environment issue)'
|
|
104
107
|
: kind === 'pluginPrepare'
|
|
105
108
|
? 'plugin prepare/build script failed during install (packaging/distribution issue)'
|
|
106
109
|
: kind === 'pnpmIgnoredBuild'
|
package/src/client/types.ts
CHANGED
|
@@ -108,6 +108,19 @@ export interface HubUpdateInfo {
|
|
|
108
108
|
publishedAt?: string | null
|
|
109
109
|
/** Markdown 变更记录;可传字符串,或 { zh, en } 双语言对象按界面语言取 */
|
|
110
110
|
notes?: string | Record<string, string> | null
|
|
111
|
+
/**
|
|
112
|
+
* 最近若干发版(含最新版),按版本号降序无关、由客户端自行排序。
|
|
113
|
+
* 用途:连续快速发版时,最新版往往还在 pnpm 的 24h 供应链门禁内;
|
|
114
|
+
* 客户端据此回退到「更早、但已过门禁」的版本提示更新,而不是整体跳过。
|
|
115
|
+
*/
|
|
116
|
+
recentVersions?: HubUpdateVersion[]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 单个发版记录:版本号 + 与 npm registry 同源的发布时间 + 该版本变更记录。 */
|
|
120
|
+
export interface HubUpdateVersion {
|
|
121
|
+
version: string
|
|
122
|
+
publishedAt?: string | null
|
|
123
|
+
notes?: string | Record<string, string> | null
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
/**
|