dsh-plugin 2.3.2 → 2.3.3
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 +3 -9
- package/README.md +3 -9
- package/client/client.js +367 -362
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +4 -4
- package/lib/http/diagnostics.js +4 -4
- package/lib/http/install.js +13 -13
- package/lib/http/npm-release-age.js +3 -3
- package/lib/http/shared.js +1 -1
- package/lib/services/host-version.js +2 -2
- package/lib/services/install/dsh-compat.js +1 -1
- package/lib/services/install/npm-resolve.js +1 -1
- package/lib/services/install/operation-log.js +3 -3
- package/lib/services/install/preflight.js +1 -1
- package/lib/services/install/registry-fallback.js +3 -3
- package/lib/services/install/release-target.js +2 -2
- package/lib/services/install/task-queue.js +35 -20
- package/lib/services/loader.js +8 -8
- package/lib/services/npm-release-age.js +8 -8
- package/lib/services/probe.js +12 -12
- package/lib/services/profile/installed-versions.js +2 -2
- package/lib/services/profile/profile.js +2 -2
- package/lib/types/services/host-version.d.ts +1 -1
- package/lib/types/services/install/install-types.d.ts +5 -5
- package/lib/types/services/install/operation-log.d.ts +1 -1
- package/lib/types/services/install/preflight.d.ts +1 -1
- package/lib/types/services/install/registry-fallback.d.ts +1 -1
- package/lib/types/services/install/release-target.d.ts +1 -1
- package/lib/types/services/install/task-queue.d.ts +10 -7
- package/lib/types/services/loader.d.ts +5 -5
- package/lib/types/services/npm-release-age.d.ts +5 -5
- package/lib/types/services/probe.d.ts +4 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +29 -29
- package/src/client/components/catalog/CatalogList.tsx +1 -1
- package/src/client/components/catalog/PluginCard.tsx +3 -3
- package/src/client/components/modals/AboutModal.tsx +2 -2
- package/src/client/components/modals/ConfirmDialog.tsx +2 -2
- package/src/client/components/modals/HubUpdateModal.tsx +3 -3
- package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
- package/src/client/components/modals/LogsPathModal.tsx +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +2 -2
- package/src/client/components/modals/modals.tsx +10 -10
- package/src/client/components/views/CustomInstallView.tsx +2 -2
- package/src/client/components/views/InstalledView.tsx +2 -2
- package/src/client/components/views/SettingsView.tsx +3 -3
- package/src/client/data/catalog.ts +1 -1
- package/src/client/data/host.ts +2 -2
- package/src/client/data/hub.ts +3 -3
- package/src/client/hooks/useCatalog.ts +14 -14
- package/src/client/hooks/useSettings.ts +2 -2
- package/src/client/hooks/useTaskQueue.ts +30 -23
- package/src/client/locales.ts +15 -15
- package/src/client/logic/constants.ts +1 -1
- package/src/client/logic/ensure-plugin-css.ts +2 -2
- package/src/client/logic/failures.ts +9 -9
- package/src/client/logic/install-command.ts +1 -2
- package/src/client/logic/installed.ts +1 -1
- package/src/client/logic/release-age.ts +10 -10
- package/src/client/logic/urls.ts +3 -3
- package/src/client/styles/Header.module.css +1 -1
- package/src/client/styles/Modal.module.css +2 -2
- package/src/client/styles/SectionTabs.module.css +1 -1
- package/src/client/styles/tokens.module.css +1 -1
- package/src/client/types.ts +4 -4
- package/src/server/http/catalog.ts +4 -4
- package/src/server/http/diagnostics.ts +4 -4
- package/src/server/http/install.ts +13 -13
- package/src/server/http/npm-release-age.ts +3 -3
- package/src/server/http/shared.ts +1 -1
- package/src/server/services/host-version.ts +2 -2
- package/src/server/services/install/dsh-compat.ts +1 -1
- package/src/server/services/install/install-types.ts +5 -5
- package/src/server/services/install/npm-resolve.ts +1 -1
- package/src/server/services/install/operation-log.ts +3 -3
- package/src/server/services/install/preflight.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +3 -3
- package/src/server/services/install/release-target.ts +2 -2
- package/src/server/services/install/task-queue.ts +40 -26
- package/src/server/services/loader.ts +10 -10
- package/src/server/services/npm-release-age.ts +10 -10
- package/src/server/services/probe.ts +12 -12
- package/src/server/services/profile/installed-versions.ts +2 -2
- package/src/server/services/profile/profile.ts +2 -2
package/lib/http/catalog.js
CHANGED
|
@@ -32,7 +32,7 @@ function fetchCatalogOnce(target, proxy) {
|
|
|
32
32
|
return task;
|
|
33
33
|
}
|
|
34
34
|
/** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
|
|
35
|
-
* maxAgeMs 传 Infinity
|
|
35
|
+
* maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时使用。 */
|
|
36
36
|
function readCatalogCache(file, maxAgeMs) {
|
|
37
37
|
try {
|
|
38
38
|
const entry = JSON.parse(readFileSync(file, 'utf8'));
|
|
@@ -46,7 +46,7 @@ function readCatalogCache(file, maxAgeMs) {
|
|
|
46
46
|
}
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/** 写缓存:目录不存在则创建;写失败直接忽略(缓存只是加速,不影响功能)。 */
|
|
50
50
|
function writeCatalogCache(file, body) {
|
|
51
51
|
try {
|
|
52
52
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -92,7 +92,7 @@ export function registerCatalogRoutes(webServer, profile) {
|
|
|
92
92
|
if (!requireMethod(request, response, 'GET'))
|
|
93
93
|
return;
|
|
94
94
|
// 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
|
|
95
|
-
// 转发(curl 子进程注入代理 env),与 npm / git
|
|
95
|
+
// 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理规则,
|
|
96
96
|
// 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
|
|
97
97
|
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
98
98
|
const settings = loadSettings(profile);
|
|
@@ -131,7 +131,7 @@ export function registerCatalogRoutes(webServer, profile) {
|
|
|
131
131
|
return;
|
|
132
132
|
}
|
|
133
133
|
catch {
|
|
134
|
-
//
|
|
134
|
+
// 备用缓存也损坏:落到下面的 502
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
// 带上 curl 退出码与 stderr 末行:只回 error 字段时,用户贴出来的响应里
|
package/lib/http/diagnostics.js
CHANGED
|
@@ -13,7 +13,7 @@ import { hostDshVersion, toolVersion } from '../services/host-version.js';
|
|
|
13
13
|
import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
|
|
14
14
|
import { appendLog } from '../services/log.js';
|
|
15
15
|
import { readJsonBody, requireTrustedPost, sendJson } from './shared.js';
|
|
16
|
-
/** 本机实际生效的 npm registry
|
|
16
|
+
/** 本机实际生效的 npm registry:诊断与安装同一套规则 —— 设置里留空时读用户 npm 配置
|
|
17
17
|
* (含 profile 目录的 .npmrc),而不是硬探官方源,避免「能装但诊断说源不通」。
|
|
18
18
|
* 读不到(npm 缺失/超时/输出异常)时回退官方源。 */
|
|
19
19
|
function configuredNpmRegistry(profile) {
|
|
@@ -76,7 +76,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
|
|
|
76
76
|
}
|
|
77
77
|
const settings = loadSettings(profile);
|
|
78
78
|
// npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
|
|
79
|
-
// 未配置(空串)时探测本机 npm 配置里实际生效的源 ——
|
|
79
|
+
// 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同一套规则。
|
|
80
80
|
const registry = settings.npmRegistry.replace(/\/+$/, '') || configuredNpmRegistry(profile);
|
|
81
81
|
// 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub 行探小仓库、
|
|
82
82
|
// 目录站按 Range 取 1KB(探市场真实下载的 plugins.zh.json,但不整份下载)。
|
|
@@ -92,7 +92,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
|
|
|
92
92
|
{ 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' },
|
|
93
93
|
];
|
|
94
94
|
// 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
|
|
95
|
-
//
|
|
95
|
+
// 验证「代理能不能把请求带出去」。与安装同一套规则:curl 子进程注入该代理 env。
|
|
96
96
|
// 设置里未配置代理时不显示该行(跟随系统代理/直连,无「代理本身」可验证)。
|
|
97
97
|
if (settings.proxy !== '') {
|
|
98
98
|
allChecks.push({ key: 'proxy', url: 'https://github.com', display: settings.proxy, cmd: `curl -s -o /dev/null -x ${settings.proxy} https://github.com`, proxy: settings.proxy });
|
|
@@ -115,7 +115,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
|
|
|
115
115
|
const results = [];
|
|
116
116
|
for (const c of checks) {
|
|
117
117
|
send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display });
|
|
118
|
-
// git 行走真实克隆握手(gitLsRemote),其余走 curl
|
|
118
|
+
// git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时保护
|
|
119
119
|
const r = c.git
|
|
120
120
|
? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
|
|
121
121
|
: await probe(c.url, c.proxy, c.range);
|
package/lib/http/install.js
CHANGED
|
@@ -16,7 +16,7 @@ import { lookupReleaseAge } from '../services/npm-release-age.js';
|
|
|
16
16
|
import { profileDirectory } from '../services/profile/profile.js';
|
|
17
17
|
import { readInstalled } from './catalog.js';
|
|
18
18
|
import { COMMAND_TIMEOUT_MS, readJsonBody, requireTrustedPost, sendJson } from './shared.js';
|
|
19
|
-
/** 安装/卸载 CLI
|
|
19
|
+
/** 安装/卸载 CLI 的子进程环境:原样传递宿主环境并注入 HTTP(S) 代理,
|
|
20
20
|
* 代理来源与诊断一致(设置代理 → 系统代理 → 宿主 env);registry 由任务队列按候选源逐次注入。 */
|
|
21
21
|
function mutationEnv(settings) {
|
|
22
22
|
const env = { ...process.env, CI: 'true' };
|
|
@@ -66,15 +66,15 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
66
66
|
const lang = typeof body === 'object' && body !== null && body.lang === 'en'
|
|
67
67
|
? 'en'
|
|
68
68
|
: 'zh';
|
|
69
|
-
//
|
|
69
|
+
// 代理规则与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
70
70
|
const effectiveProxy = settings.proxy !== ''
|
|
71
71
|
? settings.proxy
|
|
72
72
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
73
|
-
// 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm
|
|
73
|
+
// 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm 配置(与诊断同一套规则)。
|
|
74
74
|
// 只有公共源允许失败后换源;自定义/内网源保持单一候选,避免私有包名泄露给公共源。
|
|
75
75
|
const registries = registryCandidates(settings.npmRegistry, settings.npmRegistry === '' ? userNpmRegistry(profileDirectory(profile)) : '', settings.registryFallback);
|
|
76
76
|
// 全局 npm 安装(`npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
|
|
77
|
-
// 包列表优先取 body.globalNpm
|
|
77
|
+
// 包列表优先取 body.globalNpm,取不到时再从 rawRepo 解析,保证直接调 API 传完整命令也能识别。
|
|
78
78
|
const bodyGlobalNpm = Array.isArray(body.globalNpm)
|
|
79
79
|
? body.globalNpm.filter((x) => typeof x === 'string' && x !== '')
|
|
80
80
|
: [];
|
|
@@ -85,7 +85,7 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
85
85
|
sendJson(response, 400, { error: 'unsupported install target' });
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
-
//
|
|
88
|
+
// 通道校验与安装通道同构:全局安装本质是 npm 操作,吃「启用 NPM 安装」开关
|
|
89
89
|
if (source !== 'catalog' && !settings.enableNpmInstall) {
|
|
90
90
|
sendJson(response, 403, { error: 'npm installs are disabled by the security settings' });
|
|
91
91
|
return;
|
|
@@ -118,8 +118,8 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
const target = rawRepo;
|
|
121
|
-
//
|
|
122
|
-
//
|
|
121
|
+
// 锁定精确版本(Hub 自我更新):最新版可能仍在 pnpm 发布冷却期内,
|
|
122
|
+
// 客户端提示的是「已过冷却期」的版本,服务端必须按该版本安装而不是 @latest。
|
|
123
123
|
const pinVersion = typeof body.version === 'string'
|
|
124
124
|
? body.version.trim()
|
|
125
125
|
: '';
|
|
@@ -179,12 +179,12 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
179
179
|
}
|
|
180
180
|
ordered = [chosen, ...registries.filter((r) => r !== chosen)];
|
|
181
181
|
}
|
|
182
|
-
//
|
|
183
|
-
//
|
|
182
|
+
// 新安装锁定「已过发布冷却期」的最高稳定版:pnpm 对发布还不满 24 小时的版本会不提示地回退,
|
|
183
|
+
// 锁定后装到的就是预期版本。更新已有目标时客户端已按冷却期筛选,继续走 @latest。
|
|
184
184
|
let installSpec = pinVersion !== '' ? `${target}@${pinVersion}` : undefined;
|
|
185
185
|
if (installSpec === undefined && already === null) {
|
|
186
|
-
//
|
|
187
|
-
//
|
|
186
|
+
// 解析源与安装首选源一致,锁定的版本一定存在于真正会用的那个源;解析失败则不锁定,不阻断安装。
|
|
187
|
+
// 超时 5s:网络刚探测通过,卡住时也不能把安装请求拖到默认的 8s
|
|
188
188
|
const release = await lookupReleaseAge(target, ordered[0] ?? '', effectiveProxy, 5000);
|
|
189
189
|
if (release !== null && release.installable !== null)
|
|
190
190
|
installSpec = `${target}@${release.installable.version}`;
|
|
@@ -198,7 +198,7 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
198
198
|
// 后台启动 CLI;客户端轮询 /status 获取进度
|
|
199
199
|
const task = startPluginMutation({
|
|
200
200
|
// mode=update(已安装目标的覆盖更新)以 update 动作入队:
|
|
201
|
-
// 日志按 update
|
|
201
|
+
// 日志按 update 类别写入磁盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
|
|
202
202
|
action: mode === 'update' ? 'update' : 'add',
|
|
203
203
|
profile,
|
|
204
204
|
target,
|
|
@@ -206,7 +206,7 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
206
206
|
displayTarget: githubRepoOf(displayRepo) ?? undefined,
|
|
207
207
|
// 覆盖更新:npm 命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
|
|
208
208
|
updateNpm: mode === 'update',
|
|
209
|
-
// 运行中 loader
|
|
209
|
+
// 运行中 loader:安装成功后把新插件热加载进 loader,非桌面端 + 纯服务端插件无需重启即生效
|
|
210
210
|
loader,
|
|
211
211
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
212
212
|
env: mutationEnv(settings),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* npm 包发布时间查询路由:按已安装插件的 npm 包名按需查询最新版本的发布时间,
|
|
7
|
-
* 供客户端判定 pnpm 24h
|
|
7
|
+
* 供客户端判定 pnpm 24h 发布冷却期(发布还不满 24 小时的版本不提示更新)。
|
|
8
8
|
*/
|
|
9
9
|
import { systemProxy } from '../services/probe.js';
|
|
10
10
|
import { lookupReleaseAge, parseReleaseAgeRequest } from '../services/npm-release-age.js';
|
|
@@ -27,12 +27,12 @@ export function registerNpmReleaseAgeRoutes(webServer, profile) {
|
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
const settings = loadSettings(profile);
|
|
30
|
-
//
|
|
30
|
+
// 代理规则与安装/目录一致:设置里的代理 → 系统代理 → 环境变量 → 直连
|
|
31
31
|
const proxy = settings.proxy !== ''
|
|
32
32
|
? settings.proxy
|
|
33
33
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
34
34
|
// 并发查询:单包各自带超时且失败即 null,整体不因任一包失败而失败
|
|
35
|
-
//
|
|
35
|
+
// 查询源与安装首选源同一套规则(设置 → 本机 npm 配置):安装锁定的版本必须来自真正要用的那个源
|
|
36
36
|
const registry = settings.npmRegistry !== ''
|
|
37
37
|
? settings.npmRegistry
|
|
38
38
|
: userNpmRegistry(profileDirectory(profile));
|
package/lib/http/shared.js
CHANGED
|
@@ -32,7 +32,7 @@ function findPids() {
|
|
|
32
32
|
|
|
33
33
|
const targets = findPids()
|
|
34
34
|
// 一个监听进程都没找到 = 端口探测本身就失败了(netstat 输出格式 / 本地化差异),
|
|
35
|
-
// 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE
|
|
35
|
+
// 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 无提示地退出,
|
|
36
36
|
// 用户却看到「重启完成」——不制造这种假象,留一行可诊断的记录后退出。
|
|
37
37
|
if (targets.length === 0) {
|
|
38
38
|
logLine('restart aborted: no listener found on port ' + port)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* 宿主与命令行工具的版本探测:dsh 版本同时用于诊断快照和「插件要求更高 DSH」判定,
|
|
7
|
-
* pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms
|
|
7
|
+
* pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms 超时保护,拿不到一律 null,绝不挂起。
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
10
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -38,7 +38,7 @@ export function toolVersion(cmd, args) {
|
|
|
38
38
|
}
|
|
39
39
|
let cachedDshVersion = null;
|
|
40
40
|
/** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
|
|
41
|
-
* (dsh 不在 PATH
|
|
41
|
+
* (dsh 不在 PATH 时使用)。探测成功即缓存,失败不缓存以便下次重试。 */
|
|
42
42
|
export function hostDshVersion() {
|
|
43
43
|
if (cachedDshVersion !== null)
|
|
44
44
|
return cachedDshVersion;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 不再把这类失败归成「未分类」丢给用户或插件作者。
|
|
10
10
|
*
|
|
11
11
|
* 只在失败路径调用:peer 不满足本身通常只是警告,成功安装时不该出现这个提示;
|
|
12
|
-
*
|
|
12
|
+
* 任何网络/解析异常都悄悄返回 null,不改变原有失败处理。
|
|
13
13
|
*/
|
|
14
14
|
import { hostDshVersion } from '../host-version.js';
|
|
15
15
|
/** 从安装目标里取 npm 包名:`@scope/pkg@1.2.3` → `@scope/pkg`。
|
|
@@ -75,7 +75,7 @@ export function isDshNpmPackageForRepo(packageName, repo, registry = '', proxy =
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* 解析仓库改名后的规范 `owner/repo`:GitHub 对已改名仓库返回 301,跟随跳转取最终地址。
|
|
78
|
-
* 与其它 GitHub
|
|
78
|
+
* 与其它 GitHub 访问同一套规则走 curl + 代理 env(Node 内置 https 读不到系统代理)。
|
|
79
79
|
* 未改名、仓库不存在、网络失败或 curl 不可用都返回 '',调用方按原名继续。
|
|
80
80
|
*/
|
|
81
81
|
async function canonicalRepo(repo, proxy) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 回显到控制台,真正的 pnpm 报错写进该路径的 operation 日志。这里从输出里解析出该路径
|
|
8
8
|
* 并读取其尾部,补进失败信息 —— 否则前端拿不到任何错误特征,只能把失败归为「未知」。
|
|
9
9
|
* 只允许读 profile 目录下的 pnpm.log(dsh 自己的 operation 日志),
|
|
10
|
-
*
|
|
10
|
+
* 防止插件把任意路径打印进日志造成越权读取;任何异常都降级为 null,
|
|
11
11
|
* 不改变原有失败处理。
|
|
12
12
|
*/
|
|
13
13
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -55,7 +55,7 @@ export function operationLogPathOf(output) {
|
|
|
55
55
|
/**
|
|
56
56
|
* 只允许读 profile 目录下的 pnpm.log(dsh 自己的操作日志):
|
|
57
57
|
* 绝对路径必须落在该目录内,相对路径按该目录解析。
|
|
58
|
-
* 未提供 profileDir
|
|
58
|
+
* 未提供 profileDir 时退回旧规则(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
|
|
59
59
|
*/
|
|
60
60
|
function allowedLogPath(raw, profileDir) {
|
|
61
61
|
const path = resolve(profileDir ?? '', raw);
|
|
@@ -101,7 +101,7 @@ export function hasSubstantiveOutput(output) {
|
|
|
101
101
|
/**
|
|
102
102
|
* 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
|
|
103
103
|
* 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
|
|
104
|
-
* `.plugin-manager/logs/<operation>/pnpm.log
|
|
104
|
+
* `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容补全。
|
|
105
105
|
* 只认本次运行产生的新文件(mtime 晚于启动基线),避免复述上一次安装的日志。
|
|
106
106
|
*/
|
|
107
107
|
export function createOperationLogTap(profileDir) {
|
|
@@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
8
8
|
/** 缺 git 的失败标记:前端按 `[git-missing]` 归类并给「安装 git」指引(乱码免疫)。 */
|
|
9
9
|
export 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)';
|
|
10
10
|
/** 命令存在性探测:which/where 找 PATH 里的命令。
|
|
11
|
-
* 探测自身异常(which/where 不可用等)返回 true
|
|
11
|
+
* 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验处理。 */
|
|
12
12
|
export function commandOnPath(cmd, env) {
|
|
13
13
|
try {
|
|
14
14
|
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* npm
|
|
3
|
-
*
|
|
2
|
+
* npm 换源:当前源是公共镜像时,传输层失败后按「当前源 → 官方 → npmmirror」
|
|
3
|
+
* 自动换源重试;自定义/内网源不换源,避免私有包名泄露给公共源。
|
|
4
4
|
*/
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
6
|
/** 允许自动切换的公共源:只放官方与大厂/高校镜像。 */
|
|
@@ -34,7 +34,7 @@ export function isPublicRegistry(value) {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
|
|
37
|
-
* 只有公共源才追加后备(最多 3
|
|
37
|
+
* 只有公共源才追加后备(最多 3 个);关闭换源或源不在白名单时只返回它自己。
|
|
38
38
|
*/
|
|
39
39
|
export function registryCandidates(configured, userRegistry, enabled) {
|
|
40
40
|
const base = normalizeRegistry(configured) || normalizeRegistry(userRegistry) || PUBLIC_REGISTRIES[0];
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* 预构建 release 直链的仓库身份解析。部分插件(如 loopx)当初以 GitHub release 的 .tgz
|
|
7
7
|
* 安装过,仓库 HEAD 反而是 monorepo 根、走 git 直装会因缺入口文件失败;老安装记录里留的
|
|
8
8
|
* 就是这种 release 直链。这里把「历史安装目标」还原成所属仓库身份,供已装态读回、队列
|
|
9
|
-
*
|
|
9
|
+
* 去重与展示复用(单一事实来源,避免两端规则漂移)。
|
|
10
10
|
*
|
|
11
11
|
* 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
|
|
12
12
|
* 输入是记录/展示数据、绝非 shell 代码,绝不执行其文本;跨仓跳转、外部主机、凭据、
|
|
13
13
|
* 移动的 latest 链接、query/fragment、多余 CLI 参数与 shell 片段一律拒绝。
|
|
14
14
|
*/
|
|
15
|
-
/** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf
|
|
15
|
+
/** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同一套规则)。 */
|
|
16
16
|
const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i;
|
|
17
17
|
/** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */
|
|
18
18
|
const RELEASE_URL_RE = /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/releases\/download\/([A-Za-z0-9._+-]+)\/([A-Za-z0-9._+-]+\.tgz)$/;
|
|
@@ -132,7 +132,7 @@ function pushLine(task, line) {
|
|
|
132
132
|
task.progress = Math.max(task.progress, estimate);
|
|
133
133
|
}
|
|
134
134
|
else if (task.status === 'running' && task.progress < 85) {
|
|
135
|
-
//
|
|
135
|
+
// 补充:行无解析格式也随输出量推进,保证进度条始终在动(不会卡在 0)
|
|
136
136
|
task.progress = Math.max(task.progress, 6 + Math.min(79, task.lines.length * 2));
|
|
137
137
|
}
|
|
138
138
|
}
|
|
@@ -202,7 +202,7 @@ export function desktopCliBootstrap(entry) {
|
|
|
202
202
|
return null;
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
|
-
* 传给 CLI
|
|
205
|
+
* 传给 CLI 的安装目标:锁定版本优先(Hub 自我更新提示的是已过发布冷却期的版本),
|
|
206
206
|
* 其次 npm 的 add/update 都显式 `@latest`;git 源、全局安装、已带版本/tag 的 spec 保持原样。
|
|
207
207
|
*/
|
|
208
208
|
export function npmInstallTargetOf(target, opts) {
|
|
@@ -215,6 +215,13 @@ export function npmInstallTargetOf(target, opts) {
|
|
|
215
215
|
return target;
|
|
216
216
|
return `${target}@latest`;
|
|
217
217
|
}
|
|
218
|
+
/** 重复卸载:目标已不在 profile 依赖里,pnpm 报「没有这个依赖」。
|
|
219
|
+
* 结果与卸载成功等价 —— 按失败上报会弹出错误并自动提一条不存在的 Issue。 */
|
|
220
|
+
export function isAlreadyRemoved(action, output) {
|
|
221
|
+
if (action !== 'remove')
|
|
222
|
+
return false;
|
|
223
|
+
return /ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS|no such dependency found/i.test(output);
|
|
224
|
+
}
|
|
218
225
|
function stopChild(child) {
|
|
219
226
|
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
220
227
|
const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
|
|
@@ -380,10 +387,10 @@ function spawnMutation(options) {
|
|
|
380
387
|
}
|
|
381
388
|
// 安装/更新 npm 包都显式 @latest:pnpm add <pkg>(无版本)对已存在的精确 spec 是幂等的,
|
|
382
389
|
// 会原样留用旧版本(更新不生效,普通安装也可能装回不支持当前宿主的旧包)。
|
|
383
|
-
// update 动作同样执行 `add <target>@latest`,CLI
|
|
390
|
+
// update 动作同样执行 `add <target>@latest`,CLI 动词原样传递 add,不与官方 update 动词分叉。
|
|
384
391
|
const isUpdate = action === 'update' || updateNpm === true;
|
|
385
392
|
const cliAction = action === 'update' ? 'add' : action;
|
|
386
|
-
//
|
|
393
|
+
// 锁定版本优先于 @latest:提示的是「已过冷却期」的版本,装 latest 会被 pnpm 拒绝
|
|
387
394
|
const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec, add: cliAction === 'add' });
|
|
388
395
|
const args = isGlobalNpm
|
|
389
396
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
@@ -395,7 +402,7 @@ function spawnMutation(options) {
|
|
|
395
402
|
if (task && !task.attempts.includes(executed)) {
|
|
396
403
|
task.attempts.push(executed);
|
|
397
404
|
}
|
|
398
|
-
// 单次 spawn 并捕获结果:流式输出打日志 +
|
|
405
|
+
// 单次 spawn 并捕获结果:流式输出打日志 + 超时、取消时收尾,close 后 resolve。
|
|
399
406
|
// 不做任何终态后处理(exitCode/status/needsRestart 由下方统一收尾),
|
|
400
407
|
// 这样全局 npm 首次失败后可以干净地换参数重试一次。
|
|
401
408
|
const spawnOnce = (attemptArgs, registry) => new Promise((resolveOnce) => {
|
|
@@ -438,7 +445,7 @@ function spawnMutation(options) {
|
|
|
438
445
|
}
|
|
439
446
|
}
|
|
440
447
|
// invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):
|
|
441
|
-
// 没有它时保持 env
|
|
448
|
+
// 没有它时保持 env 原样传递,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
|
|
442
449
|
// registry 最后写入:换源重试只改这一层,pnpm/npm 都认 npm_config_registry。
|
|
443
450
|
const childEnv = { ...(env ?? process.env), ...(invocation.env ?? {}) };
|
|
444
451
|
if (registry !== undefined) {
|
|
@@ -477,12 +484,12 @@ function spawnMutation(options) {
|
|
|
477
484
|
}
|
|
478
485
|
if (task)
|
|
479
486
|
runningChildren.set(task.id, child);
|
|
480
|
-
//
|
|
487
|
+
// 最终超时定时器句柄:整段超时 / exit 后 close 宽限 / 强制收尾
|
|
481
488
|
let timer;
|
|
482
489
|
let progressTimer;
|
|
483
490
|
let exitWatch;
|
|
484
491
|
let forceSettle;
|
|
485
|
-
// 唯一收尾入口:close / error /
|
|
492
|
+
// 唯一收尾入口:close / error / 最终超时 三者只放行一次,确保任务持续推进
|
|
486
493
|
const settleOnce = (result) => {
|
|
487
494
|
if (settled)
|
|
488
495
|
return;
|
|
@@ -503,7 +510,7 @@ function spawnMutation(options) {
|
|
|
503
510
|
timedOut = true;
|
|
504
511
|
stopChild(child);
|
|
505
512
|
}, timeoutMs);
|
|
506
|
-
//
|
|
513
|
+
// 时间补充:即使 CLI 悄悄不吐进度行,进度条也单调推进,不会卡在 0
|
|
507
514
|
progressTimer = setInterval(() => {
|
|
508
515
|
if (task && task.status === 'running' && task.progress < 85)
|
|
509
516
|
task.progress += 1;
|
|
@@ -515,7 +522,7 @@ function spawnMutation(options) {
|
|
|
515
522
|
else
|
|
516
523
|
stderr = (stderr + text).slice(-CAPTURE_LIMIT_BYTES);
|
|
517
524
|
if (task) {
|
|
518
|
-
//
|
|
525
|
+
// 原生原样传递 pnpm/npm 输出:stdout/stderr 原样入日志,不做格式化,保证报错信息完整可见
|
|
519
526
|
for (const line of text.split(/\r?\n|\r/)) {
|
|
520
527
|
if (line.trim() !== '')
|
|
521
528
|
pushLine(task, line.trimEnd());
|
|
@@ -534,7 +541,7 @@ function spawnMutation(options) {
|
|
|
534
541
|
if (task && task.status !== 'cancelled')
|
|
535
542
|
pushLine(task, '[stale pipe] force closing child');
|
|
536
543
|
stopChild(child);
|
|
537
|
-
// 清理后极端情况下 close 仍未触发(kill
|
|
544
|
+
// 清理后极端情况下 close 仍未触发(kill 失败):再强制收尾一次
|
|
538
545
|
forceSettle = setTimeout(() => {
|
|
539
546
|
if (settled)
|
|
540
547
|
return;
|
|
@@ -597,7 +604,7 @@ function spawnMutation(options) {
|
|
|
597
604
|
pushLine(task, '[npm] peer-set resolution crashed npm (known npm bug) — retrying with --legacy-peer-deps');
|
|
598
605
|
result = await spawnOnce(retryArgs, activeRegistry);
|
|
599
606
|
}
|
|
600
|
-
// 超时被强杀 ≠
|
|
607
|
+
// 超时被强杀 ≠ 没装上:命中的安装可能已经写入磁盘(日志里 pnpm 已打印 Done,只是 dsh 进程迟迟不退出
|
|
601
608
|
// 而被我们超时掐掉)。判失败之前先复核 profile —— 目标已在依赖里、且声明分发的入口文件都在,
|
|
602
609
|
// 就按成功收尾,避免「其实装好了却报安装失败」并被自动归档成未知失败。
|
|
603
610
|
if (result.timedOut && !wasInstalled && action !== 'remove' && !isGlobalNpm && task?.status !== 'cancelled') {
|
|
@@ -608,6 +615,14 @@ function spawnMutation(options) {
|
|
|
608
615
|
pushLine(task, '[timeout] the install had already completed — treating it as a success');
|
|
609
616
|
}
|
|
610
617
|
}
|
|
618
|
+
// 重复卸载:依赖已经不在 profile 里,pnpm 拒绝移除并报「没有这个依赖」。
|
|
619
|
+
// 与卸载成功同义(用户要的结果已达成),按成功收尾,避免误报失败、误提 Issue。
|
|
620
|
+
if (!result.timedOut && task?.status !== 'cancelled'
|
|
621
|
+
&& isAlreadyRemoved(action, `${result.stderr}\n${result.stdout}`)) {
|
|
622
|
+
result = { ...result, exitCode: 0, error: null };
|
|
623
|
+
if (task)
|
|
624
|
+
pushLine(task, '[already removed] the package is no longer in the profile — treating it as uninstalled');
|
|
625
|
+
}
|
|
611
626
|
// 统一终态后处理:exitCode/status/needsRestart 以最后一次尝试为准
|
|
612
627
|
if (task) {
|
|
613
628
|
task.exitCode = result.exitCode;
|
|
@@ -634,7 +649,7 @@ function spawnMutation(options) {
|
|
|
634
649
|
else if (action === 'remove' && loader) {
|
|
635
650
|
// 卸载成功 → 主动从运行中 loader 停用该包条目:
|
|
636
651
|
// - 停用成功:服务端即时卸载;带客户端 UI 的插件面板要页面刷新才摘除(无需重启宿主)
|
|
637
|
-
// - 停用失败 /
|
|
652
|
+
// - 停用失败 / 超时:回退「待重启」提醒(刷新解决不了,登记常驻提醒)
|
|
638
653
|
const removal = await removeLoadedEntry(loader, target);
|
|
639
654
|
task.needsRestart = !removal.ok;
|
|
640
655
|
task.needsReload = removal.ok && removal.live && task.hadClientUi === true;
|
|
@@ -644,10 +659,10 @@ function spawnMutation(options) {
|
|
|
644
659
|
clearPendingRestart(displayTarget ?? target);
|
|
645
660
|
}
|
|
646
661
|
else if (action !== 'remove' && !isUpdate && loader) {
|
|
647
|
-
// 安装成功 →
|
|
662
|
+
// 安装成功 → 尝试把新插件热加载进运行中 loader,让插件无需重启宿主即生效。
|
|
648
663
|
// 挂上了:服务端即时生效;带客户端 UI 的插件还需页面重新加载(宿主按请求现场
|
|
649
664
|
// 拼装 __DSH_BOOT__ 注入清单,网页端与桌面端一致,刷新即可)。
|
|
650
|
-
// 挂不上 / 非 dsh 插件 /
|
|
665
|
+
// 挂不上 / 非 dsh 插件 / 无法定位包名:回退「待重启」提醒(保证不会无声失效)。
|
|
651
666
|
const { name } = verifyInstalledEntry(profile, target);
|
|
652
667
|
if (name !== null && isDshPlugin(profile, name) && await mountLoadedEntry(loader, name)) {
|
|
653
668
|
task.needsReload = hasClientUi(profile, name);
|
|
@@ -659,7 +674,7 @@ function spawnMutation(options) {
|
|
|
659
674
|
}
|
|
660
675
|
}
|
|
661
676
|
else {
|
|
662
|
-
// 更新(ESM
|
|
677
|
+
// 更新(ESM 缓存下热加载取不到新代码)/ 无 loader 的安装或卸载 → 登记待重启:
|
|
663
678
|
// 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
|
|
664
679
|
task.needsRestart = true;
|
|
665
680
|
addPendingRestart(displayTarget ?? target, action === 'remove' ? 'uninstall' : 'install');
|
|
@@ -671,7 +686,7 @@ function spawnMutation(options) {
|
|
|
671
686
|
}
|
|
672
687
|
}
|
|
673
688
|
// 带上最终生效的源:后续注解(如 [dsh-too-old])读元数据必须与安装同源,
|
|
674
|
-
//
|
|
689
|
+
// 否则换过源之后会去另一个源查,查不到就漏报
|
|
675
690
|
return activeRegistry === undefined ? result : { ...result, registry: activeRegistry };
|
|
676
691
|
})();
|
|
677
692
|
}
|
|
@@ -815,7 +830,7 @@ async function annotateDshTooOld(result, options) {
|
|
|
815
830
|
if ((options.globalNpm ?? []).length > 0)
|
|
816
831
|
return result;
|
|
817
832
|
// 用安装**实际**使用的源读元数据:换源重试后候选表首位可能并不是真正生效的那个,
|
|
818
|
-
//
|
|
833
|
+
// 用错源会读不到包而漏报;私有源/镜像里同样能读到包声明的 engines
|
|
819
834
|
const line = await dshTooOldMarker(options.target, { registry: result.registry ?? options.registries?.[0] });
|
|
820
835
|
if (line === null)
|
|
821
836
|
return result;
|
|
@@ -836,14 +851,14 @@ export function hostSpawnMarker(invocation, error) {
|
|
|
836
851
|
return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`;
|
|
837
852
|
}
|
|
838
853
|
/**
|
|
839
|
-
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds
|
|
854
|
+
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 限制拦下构建脚本而失败 ——
|
|
840
855
|
* 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
|
|
841
856
|
* key@url 提示),或是某依赖的原生构建(`ERR_PNPM_IGNORED_BUILDS`,key = 列出的 name@version)——
|
|
842
857
|
* 就把确切的 key 写进 profile 的 pnpm-workspace.yaml 并重试一次。
|
|
843
858
|
* 其它失败一律落到单次 spawn 的结果。
|
|
844
859
|
*/
|
|
845
860
|
export async function runPluginMutation(options) {
|
|
846
|
-
// operation
|
|
861
|
+
// operation 日志只在运行期存在(宿主结束后可能清掉),先开抓取,退出后按路径读取作为后备
|
|
847
862
|
const profileDir = profileDirectory(options.profile);
|
|
848
863
|
const logTap = createOperationLogTap(profileDir);
|
|
849
864
|
let result = await spawnMutation(options);
|
package/lib/services/loader.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* 运行中 loader
|
|
7
|
-
*
|
|
6
|
+
* 运行中 loader 的读取、停用与热加载:卸载成功后对匹配条目做 live-disable、
|
|
7
|
+
* 安装成功后把新插件条目热加载进运行中的 loader,让两类操作都尽量立即生效
|
|
8
8
|
* (刷新页面即可,无需重启宿主)。
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
@@ -61,7 +61,7 @@ function nameMatches(entryName, target) {
|
|
|
61
61
|
if (e === tShort)
|
|
62
62
|
return true;
|
|
63
63
|
// 3) 反向变体:目标短名可能就是条目的完整形式(条目 `scope-widget`,
|
|
64
|
-
// 目标短名 `widget`
|
|
64
|
+
// 目标短名 `widget` 是它的尾段)——以「条目名以短名结尾」作为匹配条件,避免误伤。
|
|
65
65
|
if (tShort.length >= 2 && e.endsWith(`-${tShort}`))
|
|
66
66
|
return true;
|
|
67
67
|
// 4) 条目名是目标包内的模块路径(`aegis/extensions/dsh/index.js` 属于包 `aegis`,
|
|
@@ -82,13 +82,13 @@ function nameMatches(entryName, target) {
|
|
|
82
82
|
* - `{ ok: true, live: false }`:宿主从未加载过它(磁盘已干净),无需刷新/重启;
|
|
83
83
|
* - `{ ok: true, live: true }`:曾在 loader 中存活且已 live-disable —— 服务端即时卸载;
|
|
84
84
|
* 带客户端 UI 的插件面板要页面刷新才摘除(由调用方按 `hadClientUi` 决定 needsReload);
|
|
85
|
-
* - `{ ok: false, live: true }`:disable 失败 /
|
|
85
|
+
* - `{ ok: false, live: true }`:disable 失败 / 超时,需宿主重启后清理。
|
|
86
86
|
* 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
|
|
87
87
|
*/
|
|
88
88
|
export async function removeLoadedEntry(loader, name) {
|
|
89
89
|
if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/'))
|
|
90
90
|
return { ok: false, live: true };
|
|
91
|
-
// 5s
|
|
91
|
+
// 5s 超时保护:fiber dispose 卡住时不能让卸载任务(乃至整个队列)被拖死
|
|
92
92
|
const removal = (async () => {
|
|
93
93
|
const entries = dumpLoaderEntries(loader);
|
|
94
94
|
const match = entries.find((e) => e.name === name);
|
|
@@ -134,7 +134,7 @@ export async function removeLoadedEntry(loader, name) {
|
|
|
134
134
|
return result;
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
|
-
*
|
|
137
|
+
* 安装成功后把新插件热加载进运行中 loader(等价 profile bundle patch 的
|
|
138
138
|
* `- insert: - name: <pkg>` 行),让插件无需重启宿主即可生效。
|
|
139
139
|
* 做法:在 Loader 根组新建条目并启动 —— Loader.write() 为空实现,不写盘;
|
|
140
140
|
* 重启后由 profile 的 bundles 清单重新挂载,不会重复。
|
|
@@ -155,7 +155,7 @@ export async function mountLoadedEntry(loader, name) {
|
|
|
155
155
|
console.log(`[hub-install] hot mount ${name}: entry ${id} created but not running (import failed?)`);
|
|
156
156
|
return false;
|
|
157
157
|
})();
|
|
158
|
-
// 10s
|
|
158
|
+
// 10s 超时保护:import 卡住时不能让装完收尾(乃至整个队列)被拖死
|
|
159
159
|
return Promise.race([
|
|
160
160
|
mounting.catch((error) => {
|
|
161
161
|
console.log(`[hub-install] hot mount ${name} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -166,7 +166,7 @@ export async function mountLoadedEntry(loader, name) {
|
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
168
|
* 已安装包是否带客户端 UI(package.json 的 `dsh.client` 声明):client-modules 据它把
|
|
169
|
-
* 客户端 bundle 注入宿主页面。带 UI
|
|
169
|
+
* 客户端 bundle 注入宿主页面。带 UI 的插件热加载后需要页面重新加载才会渲染,
|
|
170
170
|
* 网页端与桌面端一致 —— `__DSH_BOOT__` 注入清单由宿主服务端按请求现场拼装,
|
|
171
171
|
* 壳不缓存,所以桌面端刷新页面同样能拿到新清单,无需重开 App。
|
|
172
172
|
*/
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* npm
|
|
7
|
-
* pnpm 对发布不满 24h(dsh profile minimumReleaseAge=1440
|
|
8
|
-
*
|
|
6
|
+
* npm 包最新版本的发布时间查询:为「已安装插件是否有更新」提供发布冷却期依据。
|
|
7
|
+
* pnpm 对发布不满 24h(dsh profile minimumReleaseAge=1440)的版本会不提示地回退到旧版本,
|
|
8
|
+
* 因此发布还不满 24 小时的版本不该提示更新(否则用户点了也装不上)。
|
|
9
9
|
* 结果按包名内存缓存,同一次会话内不重复请求;失败/超时/响应过大一律返回 null,绝不抛错、绝不阻塞。
|
|
10
10
|
*/
|
|
11
11
|
import { get as httpsGet } from 'node:https';
|
|
12
12
|
import { validPackageName } from './profile/profile.js';
|
|
13
13
|
import { fetchViaCurl } from './probe.js';
|
|
14
|
-
/** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age
|
|
14
|
+
/** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同一套规则。 */
|
|
15
15
|
export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000;
|
|
16
16
|
/** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
|
|
17
17
|
export const MAX_RELEASE_AGE_PACKAGES = 50;
|
|
@@ -22,7 +22,7 @@ const MAX_PACKUMENT_BYTES = 10 * 1024 * 1024;
|
|
|
22
22
|
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
23
23
|
/** 会话内缓存(源 + 包名 → 结果):只存成功结果,一次网络抖动不会把判断永久粘住。
|
|
24
24
|
* 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
|
|
25
|
-
* 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest /
|
|
25
|
+
* 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错冷却期。 */
|
|
26
26
|
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
27
27
|
const releaseCache = new Map();
|
|
28
28
|
/** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
|
|
@@ -41,7 +41,7 @@ export function latestReleaseOf(packument) {
|
|
|
41
41
|
return { version, publishedAt };
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* 已过发布冷却期的最高稳定版本:安装锁定它,pnpm 不会回退到旧版本。
|
|
45
45
|
* 只看纯 x.y.z(预发布版不参与,避免把用户带到不兼容预览版);时间表缺失或无满足项返回 null。
|
|
46
46
|
*/
|
|
47
47
|
export function installableReleaseOf(packument, now = Date.now()) {
|
|
@@ -126,7 +126,7 @@ function getJson(url, timeoutMs) {
|
|
|
126
126
|
req.on('error', () => finish(null));
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
-
/** 拉取一个包的完整 packument
|
|
129
|
+
/** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理规则(curl 子进程注入 proxy env)。 */
|
|
130
130
|
async function fetchPackument(url, proxy, timeoutMs) {
|
|
131
131
|
if (proxy !== '') {
|
|
132
132
|
const r = await fetchViaCurl(url, proxy, timeoutMs);
|
|
@@ -134,7 +134,7 @@ async function fetchPackument(url, proxy, timeoutMs) {
|
|
|
134
134
|
}
|
|
135
135
|
return getJson(url, timeoutMs);
|
|
136
136
|
}
|
|
137
|
-
/**
|
|
137
|
+
/** 查询一个包的最新版本、发布时间与可锁定的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
|
|
138
138
|
export async function lookupReleaseAge(packageName, registry, proxy, timeoutMs = RELEASE_AGE_TIMEOUT_MS) {
|
|
139
139
|
const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY;
|
|
140
140
|
const key = `${base}\n${packageName}`;
|