dsh-plugin 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.js +515 -362
- package/client/client.js.map +1 -1
- package/lib/services/install/operation-log.js +1 -1
- package/lib/services/install/registry-fallback.js +5 -0
- package/lib/services/install/task-queue.js +40 -3
- package/lib/services/npm-release-age.js +10 -6
- package/lib/services/profile/installed-versions.js +32 -4
- package/lib/types/services/install/install-types.d.ts +5 -0
- package/lib/types/services/install/registry-fallback.d.ts +2 -0
- package/lib/types/services/install/task-queue.d.ts +5 -0
- package/lib/types/services/npm-release-age.d.ts +1 -1
- package/lib/types/services/profile/installed-versions.d.ts +3 -0
- package/package.json +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +29 -4
- package/src/client/components/modals/modals.tsx +28 -4
- package/src/client/hooks/useCatalog.ts +45 -25
- package/src/client/hooks/useTaskQueue.ts +30 -8
- package/src/client/locales.ts +32 -8
- package/src/client/logic/failures.ts +34 -8
- package/src/client/logic/installed.ts +12 -5
- package/src/client/logic/release-age.ts +53 -5
- package/src/client/logic/urls.ts +11 -3
- package/src/server/services/install/install-types.ts +5 -0
- package/src/server/services/install/operation-log.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +5 -0
- package/src/server/services/install/task-queue.ts +37 -3
- package/src/server/services/npm-release-age.ts +10 -6
- package/src/server/services/profile/installed-versions.ts +30 -4
|
@@ -95,7 +95,7 @@ export function backfillOperationLog(output, profileDir) {
|
|
|
95
95
|
export function hasSubstantiveOutput(output) {
|
|
96
96
|
return output.split(/\r?\n/).some((raw) => {
|
|
97
97
|
const line = raw.trim();
|
|
98
|
-
return line !== '' && !/^\[(?:exit\b|timed out\]|install-channel\]|stale pipe\]|error\]|pnpm log\])/.test(line);
|
|
98
|
+
return line !== '' && !/^\[(?:exit\b|timed out\]|install-channel\]|stale pipe\]|error\]|pnpm log\]|registry-fallback\]|allow build\])/.test(line);
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
@@ -71,11 +71,16 @@ export function userNpmRegistry(cwd) {
|
|
|
71
71
|
/**
|
|
72
72
|
* 传输层失败才值得换源:超时、连接重置、DNS、5xx。
|
|
73
73
|
* 404 / 版本不存在 / 鉴权失败必须保持原样 —— 换源只会掩盖作者侧问题或装到滞后版本。
|
|
74
|
+
* 本机证书链不被信任(TLS 校验失败)同样不换源:换哪个公共镜像都会以同样方式失败,
|
|
75
|
+
* 白白多等两轮超时、把真正的根因淹进换源噪音里。
|
|
74
76
|
*/
|
|
75
77
|
export function isTransportFailure(text) {
|
|
76
78
|
if (/\b(?:ERR_PNPM_FETCH_404|E404|ERR_PNPM_NO_MATCHING_VERSION|ETARGET|ERR_PNPM_FETCH_401|ERR_PNPM_FETCH_403|E401|E403)\b/i.test(text)) {
|
|
77
79
|
return false;
|
|
78
80
|
}
|
|
81
|
+
if (/unable to verify the first certificate|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_VERIFY_LEAF_SIGNATURE|ERR_TLS_CERT_ALTNAME_INVALID|CERT_HAS_EXPIRED|CERT_UNTRUSTED|UNABLE_TO_GET_ISSUER_CERT/i.test(text)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
79
84
|
return /\b(?:ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|EPIPE)\b|socket hang up|getaddrinfo|network request failed|connect timeout|request to .+ failed|FetchError|ERR_PNPM_FETCH_5\d\d|ERR_PNPM_META_FETCH_FAIL/i.test(text);
|
|
80
85
|
}
|
|
81
86
|
/** 日志里展示用的源主机名(不打印可能带凭据的完整 URL)。 */
|
|
@@ -612,6 +612,12 @@ function spawnMutation(options) {
|
|
|
612
612
|
task.status = result.timedOut || result.exitCode !== 0 ? 'failed' : 'done';
|
|
613
613
|
if (task.status === 'done') {
|
|
614
614
|
task.progress = 100;
|
|
615
|
+
// 实际装上的版本:目录版本可能滞后,客户端账本必须记这个值,否则「有更新」永久失真
|
|
616
|
+
if (!isGlobalNpm && action !== 'remove') {
|
|
617
|
+
const installed = installedVersionOf(profile, target);
|
|
618
|
+
if (installed !== null)
|
|
619
|
+
task.installedVersion = installed;
|
|
620
|
+
}
|
|
615
621
|
// 市场自更新:若 bundle 已被取消勾选,重启后市场不会出现,结果页要明确提示
|
|
616
622
|
if ((action === 'add' || action === 'update') && (target === 'dsh-plugin' || target.startsWith('dsh-plugin@'))
|
|
617
623
|
&& !bundleIncludes(profile, 'dsh-plugin')) {
|
|
@@ -660,9 +666,39 @@ function spawnMutation(options) {
|
|
|
660
666
|
pushLine(task, result.timedOut ? '[timed out]' : `[exit ${result.exitCode ?? '?'}]${printedNothing ? ' (no output captured)' : ''}`);
|
|
661
667
|
}
|
|
662
668
|
}
|
|
663
|
-
|
|
669
|
+
// 带上最终生效的源:后续注解(如 [dsh-too-old])读元数据必须与安装同源,
|
|
670
|
+
// 否则换过源之后会去另一个源查,查不到就静默漏报
|
|
671
|
+
return activeRegistry === undefined ? result : { ...result, registry: activeRegistry };
|
|
664
672
|
})();
|
|
665
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* 安装成功后从 profile 的 `node_modules/<pkg>/package.json` 读**实际**版本;
|
|
676
|
+
* 定位不到包名或读不到返回 null。客户端拿它写版本账本 —— 目录里的版本可能滞后。
|
|
677
|
+
*/
|
|
678
|
+
export function installedVersionOf(profile, target) {
|
|
679
|
+
const profileDir = profileDirectory(profile);
|
|
680
|
+
let deps = {};
|
|
681
|
+
try {
|
|
682
|
+
const pkg = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
683
|
+
deps = pkg.dependencies ?? {};
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const targetRepo = githubRepoOf(target)?.toLowerCase();
|
|
689
|
+
const name = targetRepo !== undefined
|
|
690
|
+
? Object.keys(deps).find((k) => githubRepoOf(deps[k])?.toLowerCase() === targetRepo)
|
|
691
|
+
: deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target);
|
|
692
|
+
if (!name)
|
|
693
|
+
return null;
|
|
694
|
+
try {
|
|
695
|
+
const meta = JSON.parse(readFileSync(join(profileDir, 'node_modules', name, 'package.json'), 'utf8'));
|
|
696
|
+
return typeof meta.version === 'string' && meta.version !== '' ? meta.version : null;
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
666
702
|
/**
|
|
667
703
|
* 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
|
|
668
704
|
* dsh.bundle.patch)是否真实存在。npm 包也可能漏发构建产物(lib/ 等),pnpm 装完没有
|
|
@@ -762,8 +798,9 @@ async function annotateDshTooOld(result, options) {
|
|
|
762
798
|
// 全局 npm 装的是本机 CLI 工具,不进 profile,没有宿主版本要求
|
|
763
799
|
if ((options.globalNpm ?? []).length > 0)
|
|
764
800
|
return result;
|
|
765
|
-
//
|
|
766
|
-
|
|
801
|
+
// 用安装**实际**使用的源读元数据:换源重试后候选表首位可能并不是真正生效的那个,
|
|
802
|
+
// 用错源会读不到包而静默漏报;私有源/镜像里同样能读到包声明的 engines
|
|
803
|
+
const line = await dshTooOldMarker(options.target, { registry: result.registry ?? options.registries?.[0] });
|
|
767
804
|
if (line === null)
|
|
768
805
|
return result;
|
|
769
806
|
if (options.task && options.task.status !== 'cancelled')
|
|
@@ -18,7 +18,10 @@ export const RELEASE_AGE_TIMEOUT_MS = 8000;
|
|
|
18
18
|
/** 响应体上限:完整 packument 可能达数 MB,超限直接放弃,避免异常大包拖垮服务。 */
|
|
19
19
|
const MAX_PACKUMENT_BYTES = 10 * 1024 * 1024;
|
|
20
20
|
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
21
|
-
/**
|
|
21
|
+
/** 会话内缓存(源 + 包名 → 结果):只存成功结果,一次网络抖动不会把判断永久粘住。
|
|
22
|
+
* 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
|
|
23
|
+
* 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错门禁。 */
|
|
24
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
22
25
|
const releaseCache = new Map();
|
|
23
26
|
/** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
|
|
24
27
|
export function latestReleaseOf(packument) {
|
|
@@ -96,12 +99,13 @@ async function fetchPackument(url, proxy, timeoutMs) {
|
|
|
96
99
|
}
|
|
97
100
|
return getJson(url, timeoutMs);
|
|
98
101
|
}
|
|
99
|
-
/**
|
|
102
|
+
/** 查询一个包的最新版本与发布时间(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
|
|
100
103
|
export async function lookupReleaseAge(packageName, registry, proxy, timeoutMs = RELEASE_AGE_TIMEOUT_MS) {
|
|
101
|
-
const cached = releaseCache.get(packageName);
|
|
102
|
-
if (cached !== undefined)
|
|
103
|
-
return cached;
|
|
104
104
|
const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY;
|
|
105
|
+
const key = `${base}\n${packageName}`;
|
|
106
|
+
const cached = releaseCache.get(key);
|
|
107
|
+
if (cached !== undefined && Date.now() - cached.at < CACHE_TTL_MS)
|
|
108
|
+
return cached.release;
|
|
105
109
|
const body = await fetchPackument(`${base}/${encodeURIComponent(packageName)}`, proxy, timeoutMs);
|
|
106
110
|
if (body === null)
|
|
107
111
|
return null;
|
|
@@ -114,6 +118,6 @@ export async function lookupReleaseAge(packageName, registry, proxy, timeoutMs =
|
|
|
114
118
|
}
|
|
115
119
|
const release = latestReleaseOf(parsed);
|
|
116
120
|
if (release !== null)
|
|
117
|
-
releaseCache.set(
|
|
121
|
+
releaseCache.set(key, { at: Date.now(), release });
|
|
118
122
|
return release;
|
|
119
123
|
}
|
|
@@ -17,16 +17,33 @@
|
|
|
17
17
|
* 存储:`DSH_HOME/profiles/<profile>/gro.ngilp-hsd-versions.json`
|
|
18
18
|
* { "<owner>/<repo>"(小写): { version, updatedAt, installedAt } }
|
|
19
19
|
*/
|
|
20
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
20
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { dirname, join } from 'node:path';
|
|
23
23
|
export function versionsFilePath(profile) {
|
|
24
24
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile, 'gro.ngilp-hsd-versions.json');
|
|
25
25
|
}
|
|
26
|
+
/** 读取某 profile 的版本账本;缺失/损坏/字段不全一律归一化成完整形状。
|
|
27
|
+
* 安装一开始只会写入 npm 包名映射(版本要等任务结束才知道),所以磁盘上可能存在
|
|
28
|
+
* 只有 npmPackage 的记录 —— 原样下发会让客户端的版本比较拿到 undefined 直接抛错。 */
|
|
26
29
|
export function readInstalledVersions(profile) {
|
|
27
30
|
try {
|
|
28
31
|
const raw = JSON.parse(readFileSync(versionsFilePath(profile), 'utf8'));
|
|
29
|
-
|
|
32
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
33
|
+
return {};
|
|
34
|
+
const table = {};
|
|
35
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
36
|
+
if (value === null || typeof value !== 'object')
|
|
37
|
+
continue;
|
|
38
|
+
const rec = value;
|
|
39
|
+
table[key] = {
|
|
40
|
+
version: typeof rec.version === 'string' ? rec.version : '',
|
|
41
|
+
updatedAt: typeof rec.updatedAt === 'string' ? rec.updatedAt : '',
|
|
42
|
+
installedAt: typeof rec.installedAt === 'string' ? rec.installedAt : '',
|
|
43
|
+
...(typeof rec.npmPackage === 'string' && rec.npmPackage !== '' ? { npmPackage: rec.npmPackage } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return table;
|
|
30
47
|
}
|
|
31
48
|
catch {
|
|
32
49
|
return {};
|
|
@@ -48,7 +65,14 @@ export function recordInstalledVersion(profile, repo, version, updatedAt) {
|
|
|
48
65
|
export function recordResolvedNpmPackage(profile, repo, npmPackage) {
|
|
49
66
|
const table = readInstalledVersions(profile);
|
|
50
67
|
const key = repo.toLowerCase();
|
|
51
|
-
|
|
68
|
+
// 显式写全字段:这一步发生在安装开始时,版本还不知道 —— 但落盘记录必须是完整形状,
|
|
69
|
+
// 否则只带 npmPackage 的残缺记录会在客户端版本比较时崩掉整块界面
|
|
70
|
+
table[key] = {
|
|
71
|
+
version: table[key]?.version ?? '',
|
|
72
|
+
updatedAt: table[key]?.updatedAt ?? '',
|
|
73
|
+
installedAt: table[key]?.installedAt ?? '',
|
|
74
|
+
npmPackage,
|
|
75
|
+
};
|
|
52
76
|
writeVersions(profile, table);
|
|
53
77
|
}
|
|
54
78
|
/** 清除某仓库记录的版本(卸载时调用)。 */
|
|
@@ -60,5 +84,9 @@ export function removeInstalledVersion(profile, repo) {
|
|
|
60
84
|
function writeVersions(profile, table) {
|
|
61
85
|
const file = versionsFilePath(profile);
|
|
62
86
|
mkdirSync(dirname(file), { recursive: true });
|
|
63
|
-
|
|
87
|
+
// 先写临时文件再原子替换:直接覆盖时进程被杀/磁盘满会留下截断的 JSON,
|
|
88
|
+
// 下次读取整份账本都会失效(更新检测静默停摆),而用户看不到任何报错
|
|
89
|
+
const tmp = `${file}.tmp`;
|
|
90
|
+
writeFileSync(tmp, JSON.stringify(table, null, 2) + '\n', 'utf8');
|
|
91
|
+
renameSync(tmp, file);
|
|
64
92
|
}
|
|
@@ -12,6 +12,8 @@ export interface InstallResult {
|
|
|
12
12
|
error: string | null;
|
|
13
13
|
stdout: string;
|
|
14
14
|
stderr: string;
|
|
15
|
+
/** 实际执行安装用的 npm 源(换源重试后是最终那一个):后续读元数据的注解要跟它同源 */
|
|
16
|
+
registry?: string;
|
|
15
17
|
}
|
|
16
18
|
export interface InstallTask {
|
|
17
19
|
id: number;
|
|
@@ -44,6 +46,9 @@ export interface InstallTask {
|
|
|
44
46
|
needsReload?: boolean;
|
|
45
47
|
/** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
|
|
46
48
|
bundleDeselected?: boolean;
|
|
49
|
+
/** 安装/更新成功后,从 profile 的 node_modules 读到的**实际**版本。
|
|
50
|
+
* 客户端用它写版本账本(目录版本可能滞后,抄目录字段会让「有更新」永久失真)。 */
|
|
51
|
+
installedVersion?: string;
|
|
47
52
|
/** 卸载入队时该包是否带客户端 UI(`dsh.client`):卸载后包已从磁盘删除,只能入队前快照,
|
|
48
53
|
* 卸载成功且曾存活时据此决定是否需要刷新页面摘除插件面板。 */
|
|
49
54
|
hadClientUi?: boolean;
|
|
@@ -12,6 +12,8 @@ export declare function userNpmRegistry(cwd: string): string;
|
|
|
12
12
|
/**
|
|
13
13
|
* 传输层失败才值得换源:超时、连接重置、DNS、5xx。
|
|
14
14
|
* 404 / 版本不存在 / 鉴权失败必须保持原样 —— 换源只会掩盖作者侧问题或装到滞后版本。
|
|
15
|
+
* 本机证书链不被信任(TLS 校验失败)同样不换源:换哪个公共镜像都会以同样方式失败,
|
|
16
|
+
* 白白多等两轮超时、把真正的根因淹进换源噪音里。
|
|
15
17
|
*/
|
|
16
18
|
export declare function isTransportFailure(text: string): boolean;
|
|
17
19
|
/** 日志里展示用的源主机名(不打印可能带凭据的完整 URL)。 */
|
|
@@ -77,6 +77,11 @@ export declare function startPluginMutation(options: {
|
|
|
77
77
|
* 必须钉住安装,不能再用 @latest 被带回门禁内的最新版 */
|
|
78
78
|
installSpec?: string;
|
|
79
79
|
}): InstallTask;
|
|
80
|
+
/**
|
|
81
|
+
* 安装成功后从 profile 的 `node_modules/<pkg>/package.json` 读**实际**版本;
|
|
82
|
+
* 定位不到包名或读不到返回 null。客户端拿它写版本账本 —— 目录里的版本可能滞后。
|
|
83
|
+
*/
|
|
84
|
+
export declare function installedVersionOf(profile: string, target: string): string | null;
|
|
80
85
|
/**
|
|
81
86
|
* 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
|
|
82
87
|
* dsh.bundle.patch)是否真实存在。npm 包也可能漏发构建产物(lib/ 等),pnpm 装完没有
|
|
@@ -11,5 +11,5 @@ export declare const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
|
|
|
11
11
|
export declare function latestReleaseOf(packument: unknown): NpmRelease | null;
|
|
12
12
|
/** 请求体 → 去重后的合法包名列表;非数组 / 超量 / 含非法包名一律 null(拒绝非法输入)。 */
|
|
13
13
|
export declare function parseReleaseAgeRequest(body: unknown): string[] | null;
|
|
14
|
-
/**
|
|
14
|
+
/** 查询一个包的最新版本与发布时间(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
|
|
15
15
|
export declare function lookupReleaseAge(packageName: string, registry: string, proxy: string, timeoutMs?: number): Promise<NpmRelease | null>;
|
|
@@ -10,6 +10,9 @@ export interface InstalledVersionRecord {
|
|
|
10
10
|
}
|
|
11
11
|
export type InstalledVersionTable = Record<string, InstalledVersionRecord>;
|
|
12
12
|
export declare function versionsFilePath(profile: string): string;
|
|
13
|
+
/** 读取某 profile 的版本账本;缺失/损坏/字段不全一律归一化成完整形状。
|
|
14
|
+
* 安装一开始只会写入 npm 包名映射(版本要等任务结束才知道),所以磁盘上可能存在
|
|
15
|
+
* 只有 npmPackage 的记录 —— 原样下发会让客户端的版本比较拿到 undefined 直接抛错。 */
|
|
13
16
|
export declare function readInstalledVersions(profile: string): InstalledVersionTable;
|
|
14
17
|
/** 记录某仓库安装时的目录信号(版本 + 仓库更新时间)。 */
|
|
15
18
|
export declare function recordInstalledVersion(profile: string, repo: string, version: string, updatedAt: string): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
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",
|
|
@@ -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, authRegistryHostOf, exclusiveProfileOf, desktopCliFallbackOf, dshTooOldInfoOf, missingLocalDirOf } from '../../logic/failures.ts'
|
|
17
|
+
import { actionOfRecord, classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf, exclusiveProfileOf, desktopCliFallbackOf, dshTooOldInfoOf, unresolvedBundleOf, missingLocalDirOf } 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'
|
|
@@ -306,10 +306,10 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
306
306
|
// 给 .npmrc 豁免指引,不引导提 Issue
|
|
307
307
|
return h('div', { className: styles.failPrepareHint }, t('failPnpmWorkspaceHint'))
|
|
308
308
|
}
|
|
309
|
-
if (kind === '
|
|
310
|
-
// profile 的 modules 目录与当前 pnpm
|
|
309
|
+
if (kind === 'pnpmModulesMismatch') {
|
|
310
|
+
// profile 的 modules 目录与当前 pnpm 配置不一致(hoist / 虚拟目录长度 / node-linker 等):
|
|
311
311
|
// 该 profile 内的安装/卸载都会失败 —— 重建 modules 目录或统一配置即可,不引导提 Issue
|
|
312
|
-
return h('div', { className: styles.failPrepareHint }, t('
|
|
312
|
+
return h('div', { className: styles.failPrepareHint }, t('failPnpmModulesMismatchHint'))
|
|
313
313
|
}
|
|
314
314
|
if (kind === 'pnpmStore') {
|
|
315
315
|
// pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录的依赖是
|
|
@@ -327,6 +327,16 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
327
327
|
// 发现补丁没被用上就中止安装,任何插件都装不进来 —— 不引导提 Issue
|
|
328
328
|
return h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
|
|
329
329
|
}
|
|
330
|
+
if (kind === 'profileState') {
|
|
331
|
+
// profile 的 pnpm 元数据与本机不一致(lockfile 过期 / packageManager 版本不符 /
|
|
332
|
+
// 补丁未生效):pnpm 在真正装插件之前就中止,该 profile 内的安装都会失败 ——
|
|
333
|
+
// 给重建指引,不引导提 Issue
|
|
334
|
+
return h('div', { className: styles.failPrepareHint }, t('failProfileStateHint'))
|
|
335
|
+
}
|
|
336
|
+
if (kind === 'nodeEngine') {
|
|
337
|
+
// 本机 Node / pnpm 版本低于包声明的下限:升级即可解决 —— 不引导提 Issue
|
|
338
|
+
return h('div', { className: styles.failPrepareHint }, t('failNodeEngineHint'))
|
|
339
|
+
}
|
|
330
340
|
if (kind === 'pnpmMissingDep') {
|
|
331
341
|
// profile 里留着指向 registry 上不存在(404)的包的依赖:pnpm 连得上 registry,
|
|
332
342
|
// 是被明确告知包不存在 —— 该 profile 的任何安装都会先卡在这条依赖上,与本次要装的
|
|
@@ -454,6 +464,14 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
454
464
|
]),
|
|
455
465
|
])
|
|
456
466
|
}
|
|
467
|
+
if (kind === 'tlsTrust') {
|
|
468
|
+
// 本机证书链不被信任(公司 SSL 审计 / 杀软 / 抓包代理):给 CA 与镜像的具体处置,
|
|
469
|
+
// 不引导提 Issue —— 换任何插件都会一样失败
|
|
470
|
+
return h('div', null, [
|
|
471
|
+
h('div', { className: styles.failPrepareHint }, t('failTlsTrustHint')),
|
|
472
|
+
onRunDiagnostics ? h('button', { className: styles.failDiagBtn, onClick: onRunDiagnostics }, t('failNetworkRunDiag')) : null,
|
|
473
|
+
])
|
|
474
|
+
}
|
|
457
475
|
if (kind === 'network') {
|
|
458
476
|
// 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
459
477
|
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
@@ -497,6 +515,13 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
497
515
|
]) : null,
|
|
498
516
|
])
|
|
499
517
|
}
|
|
518
|
+
if (kind === 'profileBundleMissing') {
|
|
519
|
+
// profile 声明的某个 bundle 没装进 node_modules:宿主在安装前就中止,
|
|
520
|
+
// 与本次插件无关 —— 给「让宿主补装 profile 依赖 / 重装应用」指引,不引导提 Issue
|
|
521
|
+
const info = unresolvedBundleOf(r.message)
|
|
522
|
+
return h('div', { className: styles.failPrepareHint },
|
|
523
|
+
t('failProfileBundleMissingHint', { pkg: info?.pkg ?? 'unknown', profile: env?.profile ?? '<profile>' }))
|
|
524
|
+
}
|
|
500
525
|
if (kind === 'hubState') {
|
|
501
526
|
// 插件中心的列表状态已过期(安装器自身守卫消息):非插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
502
527
|
return h('div', null, [
|
|
@@ -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, authRegistryHostOf, exclusiveProfileOf, desktopCliFallbackOf, dshTooOldInfoOf, missingLocalDirOf } from '../../logic/failures.ts'
|
|
18
|
+
import { classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf, exclusiveProfileOf, desktopCliFallbackOf, dshTooOldInfoOf, unresolvedBundleOf, missingLocalDirOf } from '../../logic/failures.ts'
|
|
19
19
|
import type { FailureAction } from '../../logic/failures.ts'
|
|
20
20
|
|
|
21
21
|
/** 完成结果视图:绿色对勾 + 标题/描述;
|
|
@@ -445,10 +445,10 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
445
445
|
// 调 pnpm add 时未声明在根操作,任何插件都会失败,不是插件问题 ——
|
|
446
446
|
// 给 .npmrc 豁免指引,不引导提 Issue
|
|
447
447
|
? h('div', { className: styles.failPrepareHint }, t('failPnpmWorkspaceHint'))
|
|
448
|
-
: failureKind === '
|
|
449
|
-
// profile 的 modules 目录与当前 pnpm
|
|
448
|
+
: failureKind === 'pnpmModulesMismatch'
|
|
449
|
+
// profile 的 modules 目录与当前 pnpm 配置不一致(hoist / 虚拟目录长度 / node-linker 等):
|
|
450
450
|
// 该 profile 内的安装/卸载都会失败 —— 重建 modules 目录或统一配置即可,不引导提 Issue
|
|
451
|
-
? h('div', { className: styles.failPrepareHint }, t('
|
|
451
|
+
? h('div', { className: styles.failPrepareHint }, t('failPnpmModulesMismatchHint'))
|
|
452
452
|
: failureKind === 'pnpmStore'
|
|
453
453
|
// pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录的依赖是
|
|
454
454
|
// 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
|
|
@@ -462,6 +462,15 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
462
462
|
// profile 里留着指向旧版本 dsh-plugin 的补丁声明(ERR_PNPM_UNUSED_PATCH):pnpm 发现
|
|
463
463
|
// 补丁没被用上就中止整次安装,任何插件都装不进来 —— 给删除条目的指引,不引导提 Issue
|
|
464
464
|
? h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
|
|
465
|
+
: failureKind === 'profileState'
|
|
466
|
+
// profile 的 pnpm 元数据与本机不一致(lockfile 过期 / packageManager 版本不符 /
|
|
467
|
+
// 补丁未生效):pnpm 在真正装插件之前就中止,该 profile 内的安装都会失败 ——
|
|
468
|
+
// 给重建指引,不引导提 Issue
|
|
469
|
+
? h('div', { className: styles.failPrepareHint }, t('failProfileStateHint'))
|
|
470
|
+
: failureKind === 'nodeEngine'
|
|
471
|
+
// 本机 Node / pnpm 版本低于包声明的下限:升级即可解决,装任何同类插件都会失败 ——
|
|
472
|
+
// 不引导提 Issue
|
|
473
|
+
? h('div', { className: styles.failPrepareHint }, t('failNodeEngineHint'))
|
|
465
474
|
: failureKind === 'pnpmMissingDep'
|
|
466
475
|
// profile 里留着指向 registry 上不存在(404)的包的依赖:pnpm 连得上 registry,
|
|
467
476
|
// 是被明确告知包不存在 —— 该 profile 的任何安装都会先卡在这条依赖上,与本次要装的
|
|
@@ -593,6 +602,13 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
593
602
|
}, t('failMisjudgeLink')),
|
|
594
603
|
]),
|
|
595
604
|
])
|
|
605
|
+
: failureKind === 'tlsTrust'
|
|
606
|
+
// 本机证书链不被信任(公司 SSL 审计 / 杀软 / 抓包代理):给 CA 与镜像的具体处置,
|
|
607
|
+
// 不引导提 Issue —— 换任何插件都会一样失败
|
|
608
|
+
? h('div', null, [
|
|
609
|
+
h('div', { className: styles.failPrepareHint }, t('failTlsTrustHint')),
|
|
610
|
+
onRunDiagnostics ? h('button', { className: styles.failDiagBtn, onClick: onRunDiagnostics }, t('failNetworkRunDiag')) : null,
|
|
611
|
+
])
|
|
596
612
|
: failureKind === 'network'
|
|
597
613
|
// 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
598
614
|
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
@@ -635,6 +651,14 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
635
651
|
}, t('failMisjudgeLink')),
|
|
636
652
|
]) : null,
|
|
637
653
|
])
|
|
654
|
+
: failureKind === 'profileBundleMissing'
|
|
655
|
+
// profile 声明的某个 bundle 没装进 node_modules:宿主在安装前就中止,
|
|
656
|
+
// 与本次插件无关 —— 给「让宿主补装 profile 依赖 / 重装应用」指引,不引导提 Issue
|
|
657
|
+
? (() => {
|
|
658
|
+
const info = unresolvedBundleOf(message)
|
|
659
|
+
return h('div', { className: styles.failPrepareHint },
|
|
660
|
+
t('failProfileBundleMissingHint', { pkg: info?.pkg ?? 'unknown', profile: env?.profile ?? '<profile>' }))
|
|
661
|
+
})()
|
|
638
662
|
: failureKind === 'hubState'
|
|
639
663
|
? h('div', null, [
|
|
640
664
|
// 插件中心的列表状态已过期(安装器自身守卫消息):不是插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
@@ -15,8 +15,8 @@ import type { HubAboutInfo, HubUpdateInfo, HubUpdateState } from '../types.ts'
|
|
|
15
15
|
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
|
-
import { fetchInstalled, fetchNpmReleaseAge } from '../data/host.ts'
|
|
19
|
-
import { isReleaseTooFresh, pickHubUpdate } from '../logic/release-age.ts'
|
|
18
|
+
import { fetchInstalled, fetchNpmReleaseAge, type NpmReleaseInfo } from '../data/host.ts'
|
|
19
|
+
import { isReleaseTooFresh, isUpdateOffered, pickHubUpdate } from '../logic/release-age.ts'
|
|
20
20
|
import {
|
|
21
21
|
hasUpdateOf, installedItemsOf, installedNameOf, installedVersionOf, npmPackageOf,
|
|
22
22
|
} from '../logic/installed.ts'
|
|
@@ -25,8 +25,8 @@ import type { InstalledItem, InstalledVersionSignal } from '../logic/installed.t
|
|
|
25
25
|
/** 插件市场自身仓库:DSH Plugin Hub 不显示在目录里(自己不进自己的插件列表) */
|
|
26
26
|
const SELF_REPO = 'dshplugin/dsh-plugin-hub'
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
const
|
|
28
|
+
/** 共享的空发布信息表:无候选包时复用,避免每次渲染新建对象。 */
|
|
29
|
+
const EMPTY_RELEASES: Record<string, NpmReleaseInfo | null> = {}
|
|
30
30
|
|
|
31
31
|
/** 市场各排序的默认方向:全部按倒序(Star/Fork 多、更新/收录近的在前) */
|
|
32
32
|
const SORT_DEFAULT_DIR: Record<SortKey, 'asc' | 'desc'> = {
|
|
@@ -190,18 +190,18 @@ export function useCatalog(lang: LocaleId) {
|
|
|
190
190
|
const installedVersion = (p: HubPlugin): string | null => installedVersionOf(p, versions)
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
*
|
|
193
|
+
* 更新可用性校验状态:npm registry 上每个包的最新可安装版本与发布时间。
|
|
194
194
|
* key 是这批候选包名的指纹,与已查出的 key 一致才算就绪,避免查询未回来时先显示更新。
|
|
195
195
|
*/
|
|
196
|
-
const [releaseAge, setReleaseAge] = useState<{ key: string;
|
|
196
|
+
const [releaseAge, setReleaseAge] = useState<{ key: string; releases: Record<string, NpmReleaseInfo | null> }>({ key: '', releases: EMPTY_RELEASES })
|
|
197
197
|
|
|
198
|
-
/**
|
|
198
|
+
/** 已安装项原始列表(未套校验):先算出来才能收集需要查发布信息的候选包名。 */
|
|
199
199
|
const rawInstalledItems = useMemo<InstalledItem[]>(
|
|
200
200
|
() => installedItemsOf(plugins, installed, versions, installPaths, loadedNames, dshCapableNames),
|
|
201
201
|
[plugins, installed, versions, installPaths, loadedNames, dshCapableNames],
|
|
202
202
|
)
|
|
203
203
|
|
|
204
|
-
/**
|
|
204
|
+
/** 校验候选:有更新、且有 npm 包名(更新走 npm registry)的已安装插件包名。 */
|
|
205
205
|
const releaseAgeCandidates = useMemo(() => {
|
|
206
206
|
const names = new Set<string>()
|
|
207
207
|
for (const item of rawInstalledItems) {
|
|
@@ -212,53 +212,73 @@ export function useCatalog(lang: LocaleId) {
|
|
|
212
212
|
return [...names].sort()
|
|
213
213
|
}, [rawInstalledItems, versions])
|
|
214
214
|
const releaseAgeKey = releaseAgeCandidates.join('\n')
|
|
215
|
+
/** 包名 → 已装版本:判断 npm 上最新版是否真的比已装版本新。 */
|
|
216
|
+
const installedByPackage = useMemo(() => {
|
|
217
|
+
const map = new Map<string, string>()
|
|
218
|
+
for (const item of rawInstalledItems) {
|
|
219
|
+
if (!item.plugin) continue
|
|
220
|
+
const pkg = npmPackageOf(item.plugin, versions)
|
|
221
|
+
const version = installedVersionOf(item.plugin, versions)
|
|
222
|
+
if (pkg !== null && version !== null) map.set(pkg, version)
|
|
223
|
+
}
|
|
224
|
+
return map
|
|
225
|
+
}, [rawInstalledItems, versions])
|
|
215
226
|
|
|
216
227
|
useEffect(() => {
|
|
217
228
|
if (releaseAgeKey === releaseAge.key) return
|
|
218
229
|
if (releaseAgeCandidates.length === 0) {
|
|
219
|
-
setReleaseAge({ key: '',
|
|
230
|
+
setReleaseAge({ key: '', releases: EMPTY_RELEASES })
|
|
220
231
|
return
|
|
221
232
|
}
|
|
222
233
|
let cancelled = false
|
|
223
|
-
// 批量一次查询;失败/返回 null
|
|
234
|
+
// 批量一次查询;失败/返回 null 时按原逻辑照常提示,网络问题不吃掉真实更新
|
|
224
235
|
void fetchNpmReleaseAge(releaseAgeCandidates).then((result) => {
|
|
225
236
|
if (cancelled) return
|
|
226
|
-
|
|
227
|
-
const held = new Set<string>()
|
|
228
|
-
for (const name of releaseAgeCandidates) {
|
|
229
|
-
const info = result?.[name]
|
|
230
|
-
if (info && isReleaseTooFresh(info.publishedAt, now)) held.add(name)
|
|
231
|
-
}
|
|
232
|
-
setReleaseAge({ key: releaseAgeKey, held })
|
|
237
|
+
setReleaseAge({ key: releaseAgeKey, releases: result ?? EMPTY_RELEASES })
|
|
233
238
|
})
|
|
234
239
|
return () => { cancelled = true }
|
|
235
240
|
}, [releaseAgeKey, releaseAge.key])
|
|
236
241
|
|
|
237
|
-
/**
|
|
242
|
+
/** 校验结果是否已就绪(候选为空也算就绪):未就绪时不写更新提醒,等查询回来再定。 */
|
|
238
243
|
const releaseAgeReady = releaseAgeKey === releaseAge.key
|
|
239
|
-
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 不能提示更新的包:
|
|
247
|
+
* - npm 上最新版还没过供应链门禁(发布不满 24h,pnpm 会静默回退,点了也装不上);
|
|
248
|
+
* - npm 上最新版并不高于已装版本 —— 更新走的是 `pkg@latest`,此时无论目录标到哪个 tag,
|
|
249
|
+
* 都不可能装上(作者可能只打了 GitHub release 没发 npm),提示只会变成永远点不掉的循环。
|
|
250
|
+
*/
|
|
251
|
+
const updateBlocked = useMemo(() => {
|
|
252
|
+
const blocked = new Set<string>()
|
|
253
|
+
if (!releaseAgeReady) return blocked
|
|
254
|
+
const now = Date.now()
|
|
255
|
+
for (const pkg of releaseAgeCandidates) {
|
|
256
|
+
if (!isUpdateOffered(releaseAge.releases[pkg] ?? null, installedByPackage.get(pkg) ?? null, now)) blocked.add(pkg)
|
|
257
|
+
}
|
|
258
|
+
return blocked
|
|
259
|
+
}, [releaseAgeReady, releaseAgeCandidates, releaseAge.releases, installedByPackage])
|
|
240
260
|
|
|
241
261
|
/**
|
|
242
262
|
* 是否有更新:仅对已安装插件有意义。
|
|
243
263
|
* 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
|
|
244
264
|
* (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
|
|
245
|
-
*
|
|
265
|
+
* 再用 npm registry 的实际最新版本复核:装不上的(太新 / 不比已装新)不打更新标记。
|
|
246
266
|
*/
|
|
247
267
|
const hasUpdate = (p: HubPlugin): boolean => {
|
|
248
268
|
if (!hasUpdateOf(p, installed, versions)) return false
|
|
249
269
|
const pkg = npmPackageOf(p, versions)
|
|
250
|
-
return pkg === null || !
|
|
270
|
+
return pkg === null || !updateBlocked.has(pkg)
|
|
251
271
|
}
|
|
252
272
|
|
|
253
|
-
/** 已安装项统一列表(目录元数据 +
|
|
273
|
+
/** 已安装项统一列表(目录元数据 + 运行时信息合并):被拦下的去掉更新标记,驱动「已安装」tab。 */
|
|
254
274
|
const installedItems = useMemo<InstalledItem[]>(() => {
|
|
255
|
-
if (
|
|
275
|
+
if (updateBlocked.size === 0) return rawInstalledItems
|
|
256
276
|
return rawInstalledItems.map((item) => {
|
|
257
277
|
if (!item.hasUpdate || !item.plugin) return item
|
|
258
278
|
const pkg = npmPackageOf(item.plugin, versions)
|
|
259
|
-
return pkg !== null &&
|
|
279
|
+
return pkg !== null && updateBlocked.has(pkg) ? { ...item, hasUpdate: false } : item
|
|
260
280
|
})
|
|
261
|
-
}, [rawInstalledItems,
|
|
281
|
+
}, [rawInstalledItems, updateBlocked, versions])
|
|
262
282
|
|
|
263
283
|
/**
|
|
264
284
|
* Hub 自身更新状态:Hub 就是当前运行的应用(始终已安装,无需 installedName 命中)。
|