dsh-plugin 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/client/client.js +388 -335
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +22 -3
- package/lib/http/diagnostics.js +36 -9
- package/lib/services/install/task-queue.js +6 -1
- package/lib/services/probe.js +10 -5
- package/lib/services/profile/profile.js +14 -0
- package/lib/types/services/install/install-types.d.ts +3 -1
- package/lib/types/services/probe.d.ts +2 -1
- package/lib/types/services/profile/profile.d.ts +3 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +6 -1
- package/src/client/components/catalog/CatalogList.tsx +5 -1
- package/src/client/components/layout/SectionTabs.tsx +2 -1
- package/src/client/components/modals/NotificationsModal.tsx +4 -0
- package/src/client/components/modals/modals.tsx +11 -2
- package/src/client/components/views/MarketView.tsx +1 -0
- package/src/client/data/catalog.ts +9 -7
- package/src/client/hooks/useCatalog.ts +10 -4
- package/src/client/hooks/useTaskQueue.ts +14 -7
- package/src/client/locales.ts +18 -4
- package/src/client/logic/failures.ts +18 -4
- package/src/client/styles/Modal.module.css +9 -0
- package/src/client/styles/SectionTabs.module.css +18 -0
- package/src/server/http/catalog.ts +24 -3
- package/src/server/http/diagnostics.ts +36 -10
- package/src/server/services/install/install-types.ts +3 -1
- package/src/server/services/install/task-queue.ts +6 -1
- package/src/server/services/probe.ts +10 -5
- package/src/server/services/profile/profile.ts +15 -0
package/lib/http/catalog.js
CHANGED
|
@@ -15,6 +15,22 @@ import { CATALOG_CACHE_TTL_MS, requireMethod, sendJson } from './shared.js';
|
|
|
15
15
|
function catalogCacheFile(profile, key) {
|
|
16
16
|
return join(profileDirectory(profile), 'cache', `catalog-${key}.json`);
|
|
17
17
|
}
|
|
18
|
+
/** 目录拉取超时:目录 JSON 约 3.6MB,20s 在弱网/代理链路上不够用;
|
|
19
|
+
* 客户端会在超时后重试,重试必须共享同一次拉取(见 fetchCatalogOnce)。 */
|
|
20
|
+
const CATALOG_FETCH_TIMEOUT_MS = 60_000;
|
|
21
|
+
/** 并发拉取去重:客户端超时重试会并发打进来,共享同一次 curl,
|
|
22
|
+
* 避免多份 3.6MB 下载互相抢带宽、越重试越慢。 */
|
|
23
|
+
const inflightCatalog = new Map();
|
|
24
|
+
function fetchCatalogOnce(target, proxy) {
|
|
25
|
+
const key = `${target}\n${proxy}`;
|
|
26
|
+
const running = inflightCatalog.get(key);
|
|
27
|
+
if (running !== undefined)
|
|
28
|
+
return running;
|
|
29
|
+
const task = fetchViaCurl(target, proxy, CATALOG_FETCH_TIMEOUT_MS)
|
|
30
|
+
.finally(() => { inflightCatalog.delete(key); });
|
|
31
|
+
inflightCatalog.set(key, task);
|
|
32
|
+
return task;
|
|
33
|
+
}
|
|
18
34
|
/** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
|
|
19
35
|
* maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时兜底使用。 */
|
|
20
36
|
function readCatalogCache(file, maxAgeMs) {
|
|
@@ -89,9 +105,12 @@ export function registerCatalogRoutes(webServer, profile) {
|
|
|
89
105
|
const target = isStats
|
|
90
106
|
? 'https://api.dsh-plugin.org/stats.json'
|
|
91
107
|
: `https://api.dsh-plugin.org/plugins.${lang}.json`;
|
|
92
|
-
// 1 小时本地缓存:命中直接返回(跨重启生效,插件市场秒开),未命中才走 curl
|
|
108
|
+
// 1 小时本地缓存:命中直接返回(跨重启生效,插件市场秒开),未命中才走 curl 拉远程。
|
|
109
|
+
// refresh=1 跳过 TTL 缓存直接回源(「强制刷新」用);成功后照样覆盖缓存,
|
|
110
|
+
// 失败仍回退旧缓存,所以强制刷新不会把可用的旧数据弄丢。
|
|
93
111
|
const cacheFile = catalogCacheFile(profile, isStats ? 'stats' : `plugins-${lang}`);
|
|
94
|
-
const
|
|
112
|
+
const forceRefresh = url.searchParams.get('refresh') === '1';
|
|
113
|
+
const cached = forceRefresh ? null : readCatalogCache(cacheFile, CATALOG_CACHE_TTL_MS);
|
|
95
114
|
if (cached !== null) {
|
|
96
115
|
try {
|
|
97
116
|
sendJson(response, 200, JSON.parse(cached));
|
|
@@ -101,7 +120,7 @@ export function registerCatalogRoutes(webServer, profile) {
|
|
|
101
120
|
// 缓存正文损坏:忽略,走重新拉取并覆盖写盘
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
|
-
const r = await
|
|
123
|
+
const r = await fetchCatalogOnce(target, proxy);
|
|
105
124
|
if (!r.ok || r.body === '') {
|
|
106
125
|
// 实时拉取失败:退回任意年龄的本地缓存(上次成功数据),
|
|
107
126
|
// 避免证书吊销受阻 / 网络抖动时整个插件市场打不开 —— 数据可能过期,但好过空白。
|
package/lib/http/diagnostics.js
CHANGED
|
@@ -74,6 +74,30 @@ function toolVersion(cmd, args) {
|
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
/** 本机实际生效的 npm registry:诊断与安装同口径 —— 设置里留空时读用户 npm 配置
|
|
78
|
+
* (含 profile 目录的 .npmrc),而不是硬探官方源,避免「能装但诊断说源不通」。
|
|
79
|
+
* 读不到(npm 缺失/超时/输出异常)时回退官方源。 */
|
|
80
|
+
function configuredNpmRegistry(profile) {
|
|
81
|
+
const fromEnv = process.env.npm_config_registry ?? process.env.NPM_CONFIG_REGISTRY ?? '';
|
|
82
|
+
const valid = (v) => (/^https?:\/\//.test(v) ? v.replace(/\/+$/, '') : null);
|
|
83
|
+
const viaEnv = valid(fromEnv);
|
|
84
|
+
if (viaEnv !== null)
|
|
85
|
+
return viaEnv;
|
|
86
|
+
try {
|
|
87
|
+
const cwd = join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile);
|
|
88
|
+
const r = spawnSync('npm', ['config', 'get', 'registry'], {
|
|
89
|
+
timeout: 3000,
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
cwd,
|
|
92
|
+
shell: process.platform === 'win32',
|
|
93
|
+
});
|
|
94
|
+
const out = (r.stdout ?? '').trim().split(/\r?\n/).pop() ?? '';
|
|
95
|
+
return valid(out) ?? 'https://registry.npmjs.org';
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return 'https://registry.npmjs.org';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
77
101
|
/** 宿主环境快照:提交 bug 时随 issue 附上,便于作者复现(前端 /env 端点返回)。 */
|
|
78
102
|
export function hostEnv(profile) {
|
|
79
103
|
return {
|
|
@@ -113,17 +137,20 @@ export function registerDiagnosticsRoutes(webServer, profile) {
|
|
|
113
137
|
}
|
|
114
138
|
const settings = loadSettings(profile);
|
|
115
139
|
// npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
|
|
116
|
-
//
|
|
117
|
-
const registry = settings.npmRegistry.replace(/\/+$/, '') ||
|
|
118
|
-
// 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub
|
|
119
|
-
//
|
|
120
|
-
// npm 行 display
|
|
140
|
+
// 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同口径。
|
|
141
|
+
const registry = settings.npmRegistry.replace(/\/+$/, '') || configuredNpmRegistry(profile);
|
|
142
|
+
// 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub 行探小仓库、
|
|
143
|
+
// 目录站按 Range 取 1KB(探市场真实下载的 plugins.zh.json,但不整份下载)。
|
|
144
|
+
// npm 行 display:显式配置、或本机 npm 源不是官方源时显示实际地址,
|
|
145
|
+
// 让用户看到「诊断的到底是哪个源」;官方源且未配置时留空(显示「跟随本机 npm 配置」)。
|
|
121
146
|
const allChecks = [
|
|
122
|
-
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
147
|
+
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' || registry !== 'https://registry.npmjs.org' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
123
148
|
// GitHub 行用真实 git 克隆握手(git ls-remote)打 dshplugin/hello-dsh 小仓库:
|
|
124
149
|
// 「网页能打开」和「git 能克隆」是两码事,握手成功才算通道通;小仓库秒级完成,不打 17MB 的 dsh-plugin-hub。
|
|
125
150
|
{ key: 'github', url: 'https://github.com/dshplugin/hello-dsh', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/hello-dsh', git: true },
|
|
126
|
-
|
|
151
|
+
// 目录行探市场真正下载的那个文件(plugins.zh.json),只取前 1KB:
|
|
152
|
+
// 只探 stats.json 会出现「诊断绿、市场加载不出来」—— 两者端点与体积完全不同。
|
|
153
|
+
{ 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' },
|
|
127
154
|
];
|
|
128
155
|
// 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
|
|
129
156
|
// 验证「代理能不能把请求带出去」。与安装同口径:curl 子进程注入该代理 env。
|
|
@@ -144,7 +171,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
|
|
|
144
171
|
const effectiveProxy = settings.proxy !== ''
|
|
145
172
|
? settings.proxy
|
|
146
173
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
147
|
-
const probe = (url, proxy) => probeUrl(url, proxy ?? effectiveProxy, 6000);
|
|
174
|
+
const probe = (url, proxy, range) => probeUrl(url, proxy ?? effectiveProxy, 6000, range);
|
|
148
175
|
// 逐路串行:一条命令一条结果地推送给终端,保持「程序在跑」的真实感
|
|
149
176
|
const results = [];
|
|
150
177
|
for (const c of checks) {
|
|
@@ -152,7 +179,7 @@ export function registerDiagnosticsRoutes(webServer, profile) {
|
|
|
152
179
|
// git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时兜底
|
|
153
180
|
const r = c.git
|
|
154
181
|
? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
|
|
155
|
-
: await probe(c.url, c.proxy);
|
|
182
|
+
: await probe(c.url, c.proxy, c.range);
|
|
156
183
|
results.push({ key: c.key, ok: r.ok });
|
|
157
184
|
// git 行的 status 是退出码,客户端 git 行只显示 OK / 不可达,不显示成 HTTP 码。
|
|
158
185
|
// reason 透出失败细分原因(如 Windows 证书吊销受阻),客户端据此给对症提示而非笼统「不可达」。
|
|
@@ -18,7 +18,7 @@ import { backfillOperationLog, createOperationLogTap, operationLogPathOf } from
|
|
|
18
18
|
import { cleanLine, estimateProgress } from '../profile/progress.js';
|
|
19
19
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
|
|
20
20
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.js';
|
|
21
|
-
import { addAllowBuildsKey, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.js';
|
|
21
|
+
import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.js';
|
|
22
22
|
import { appendLog } from '../log.js';
|
|
23
23
|
/** 内存任务注册表,按任务 id 索引。 */
|
|
24
24
|
const tasks = new Map();
|
|
@@ -568,6 +568,11 @@ function spawnMutation(options) {
|
|
|
568
568
|
task.status = result.timedOut || result.exitCode !== 0 ? 'failed' : 'done';
|
|
569
569
|
if (task.status === 'done') {
|
|
570
570
|
task.progress = 100;
|
|
571
|
+
// 市场自更新:若 bundle 已被取消勾选,重启后市场不会出现,结果页要明确提示
|
|
572
|
+
if ((action === 'add' || action === 'update') && (target === 'dsh-plugin' || target.startsWith('dsh-plugin@'))
|
|
573
|
+
&& !bundleIncludes(profile, 'dsh-plugin')) {
|
|
574
|
+
task.bundleDeselected = true;
|
|
575
|
+
}
|
|
571
576
|
if (isGlobalNpm) {
|
|
572
577
|
// 全局 npm 安装:装的是本机 CLI 工具(如 dsh-tui),不进任何 profile,无需宿主重启
|
|
573
578
|
task.needsRestart = false;
|
package/lib/services/probe.js
CHANGED
|
@@ -162,13 +162,16 @@ async function curlWithTlsFallback(args, env, timeoutMs) {
|
|
|
162
162
|
* 失败(curl 不存在 / 连接失败 / 超时)都归为不可达,全程不抛错。
|
|
163
163
|
* 证书吊销检查受阻(Windows Schannel)另记 reason,供诊断给出对症提示。
|
|
164
164
|
*/
|
|
165
|
-
async function curlProbe(url, proxy, timeoutMs) {
|
|
165
|
+
async function curlProbe(url, proxy, timeoutMs, range) {
|
|
166
166
|
const started = Date.now();
|
|
167
167
|
const discard = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
168
168
|
// -sS(而非仅 -s):silent 会把连接/TLS 错误一并吞掉,而识别证书吊销受阻正需要 stderr;
|
|
169
169
|
// 子进程 stderr 由我们收集、不会回显给用户,所以补 -S 不产生噪音。
|
|
170
|
+
// range:目录通道路由探真实的 plugins.zh.json,但只取前 1KB(-r 0-1023,返回 206 即视为可达),
|
|
171
|
+
// 避免诊断把 3.6MB 全量目录下载一遍。
|
|
170
172
|
const r = await curlWithTlsFallback([
|
|
171
173
|
'-sS', '-o', discard, '-w', '%{http_code}',
|
|
174
|
+
...(range !== undefined ? ['-r', range] : []),
|
|
172
175
|
'--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))),
|
|
173
176
|
url,
|
|
174
177
|
], proxyEnv(proxy), timeoutMs);
|
|
@@ -187,8 +190,9 @@ async function curlProbe(url, proxy, timeoutMs) {
|
|
|
187
190
|
/**
|
|
188
191
|
* 探测一个 HTTPS 目标:proxy 非空注入给 curl 子进程走代理,否则直连。
|
|
189
192
|
* 供系统诊断 /diagnostics 使用;目标 URL 非法返回不可达。
|
|
193
|
+
* range(如 '0-1023')只取前 N 字节,用于探大文件端点而不下载全量。
|
|
190
194
|
*/
|
|
191
|
-
export function probeUrl(url, proxy, timeoutMs) {
|
|
195
|
+
export function probeUrl(url, proxy, timeoutMs, range) {
|
|
192
196
|
let target;
|
|
193
197
|
try {
|
|
194
198
|
target = new URL(url);
|
|
@@ -196,7 +200,7 @@ export function probeUrl(url, proxy, timeoutMs) {
|
|
|
196
200
|
catch {
|
|
197
201
|
return Promise.resolve(probeFail());
|
|
198
202
|
}
|
|
199
|
-
return curlProbe(target.href, proxy, timeoutMs);
|
|
203
|
+
return curlProbe(target.href, proxy, timeoutMs, range);
|
|
200
204
|
}
|
|
201
205
|
/**
|
|
202
206
|
* git 真实克隆握手探测:spawn git ls-remote(https 传输,与 pnpm 克隆前的
|
|
@@ -260,8 +264,9 @@ export async function finalUrl(url, proxy, timeoutMs) {
|
|
|
260
264
|
* reason 区分「证书吊销受阻」这类环境 TLS 问题,供调用方决定兜底策略。
|
|
261
265
|
*/
|
|
262
266
|
export function fetchViaCurl(url, proxy, timeoutMs) {
|
|
263
|
-
// 同样用 -sS:正文之外还要 stderr 里的 TLS 失败线索(被 stderr
|
|
264
|
-
|
|
267
|
+
// 同样用 -sS:正文之外还要 stderr 里的 TLS 失败线索(被 stderr 收集,不外泄给用户)。
|
|
268
|
+
// --compressed:目录 JSON 约 3.6MB,源站支持 gzip,压缩后弱网下也能在超时内拉完。
|
|
269
|
+
const args = ['-sS', '--compressed', '--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))), url];
|
|
265
270
|
return curlWithTlsFallback(args, proxyEnv(proxy), timeoutMs).then((r) => ({
|
|
266
271
|
ok: r.code === 0,
|
|
267
272
|
body: r.stdout,
|
|
@@ -50,6 +50,20 @@ export function profileFromArgv(argv, fallback = 'web') {
|
|
|
50
50
|
export function profileDirectory(profile) {
|
|
51
51
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile);
|
|
52
52
|
}
|
|
53
|
+
/** profile 的 dsh.profile.bundles 是否仍包含指定包:取消勾选后 bundle patch 不再执行。
|
|
54
|
+
* 没有该清单或读取失败时返回 true —— 不归我们判断的情况不报警。 */
|
|
55
|
+
export function bundleIncludes(profile, name) {
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'package.json'), 'utf8'));
|
|
58
|
+
const bundles = pkg.dsh?.profile?.bundles;
|
|
59
|
+
if (!Array.isArray(bundles))
|
|
60
|
+
return true;
|
|
61
|
+
return bundles.includes(name);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
53
67
|
/** 把仓库身份转成规范的 GitHub 目标(git+https),仓库不合法时返回 null。
|
|
54
68
|
* 带 ref 时按 Git ref(`#tag` / `#commit`)拼接;安装通道已不再使用 git 目标,此函数仅做身份规范化。 */
|
|
55
69
|
export function githubTarget(repo, ref) {
|
|
@@ -39,9 +39,11 @@ export interface InstallTask {
|
|
|
39
39
|
/** 完成后是否仍需宿主重启才生效:仅「热挂载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
|
|
40
40
|
* 无 loader」等刷新解决不了的兜底场景为 true(弹窗给「立即重启」);其余一律 false。 */
|
|
41
41
|
needsRestart: boolean;
|
|
42
|
-
/**
|
|
42
|
+
/** 完成结果是否只需刷新页面即生效(插件已热挂进运行中 loader,仅剩客户端 UI 待页面重新加载,
|
|
43
43
|
* 网页端与桌面端一致):true 时关闭结果弹窗即自动刷新页面,不给手动刷新按钮。 */
|
|
44
44
|
needsReload?: boolean;
|
|
45
|
+
/** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
|
|
46
|
+
bundleDeselected?: boolean;
|
|
45
47
|
/** 卸载入队时该包是否带客户端 UI(`dsh.client`):卸载后包已从磁盘删除,只能入队前快照,
|
|
46
48
|
* 卸载成功且曾存活时据此决定是否需要刷新页面摘除插件面板。 */
|
|
47
49
|
hadClientUi?: boolean;
|
|
@@ -46,8 +46,9 @@ export declare function systemProxy(): string | null;
|
|
|
46
46
|
/**
|
|
47
47
|
* 探测一个 HTTPS 目标:proxy 非空注入给 curl 子进程走代理,否则直连。
|
|
48
48
|
* 供系统诊断 /diagnostics 使用;目标 URL 非法返回不可达。
|
|
49
|
+
* range(如 '0-1023')只取前 N 字节,用于探大文件端点而不下载全量。
|
|
49
50
|
*/
|
|
50
|
-
export declare function probeUrl(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult>;
|
|
51
|
+
export declare function probeUrl(url: string, proxy: string, timeoutMs: number, range?: string): Promise<ProbeResult>;
|
|
51
52
|
/**
|
|
52
53
|
* git 真实克隆握手探测:spawn git ls-remote(https 传输,与 pnpm 克隆前的
|
|
53
54
|
* ref 握手一致),注入代理 env,GIT_TERMINAL_PROMPT=0 防凭据提示挂起。
|
|
@@ -12,6 +12,9 @@ export declare function readProfileArg(fallback?: string): string;
|
|
|
12
12
|
export declare function profileFromArgv(argv: readonly string[], fallback?: string): string;
|
|
13
13
|
/** 解析 profile 目录(`DSH_HOME` 或 `~/.dsh`)。 */
|
|
14
14
|
export declare function profileDirectory(profile: string): string;
|
|
15
|
+
/** profile 的 dsh.profile.bundles 是否仍包含指定包:取消勾选后 bundle patch 不再执行。
|
|
16
|
+
* 没有该清单或读取失败时返回 true —— 不归我们判断的情况不报警。 */
|
|
17
|
+
export declare function bundleIncludes(profile: string, name: string): boolean;
|
|
15
18
|
/** 把仓库身份转成规范的 GitHub 目标(git+https),仓库不合法时返回 null。
|
|
16
19
|
* 带 ref 时按 Git ref(`#tag` / `#commit`)拼接;安装通道已不再使用 git 目标,此函数仅做身份规范化。 */
|
|
17
20
|
export declare function githubTarget(repo: string, ref?: string | null): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
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",
|
|
@@ -109,6 +109,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
109
109
|
/** 结果视图是否显示「关闭后自动刷新」说明:插件已热挂进运行中 loader(安装)、或卸载后
|
|
110
110
|
* 插件面板仍残留在当前页面(卸载)。关闭弹窗即整页刷新,不给手动刷新按钮。 */
|
|
111
111
|
const [installNeedsReload, setInstallNeedsReload] = useState(false)
|
|
112
|
+
/** 市场自更新成功、但 profile 的 bundle 列表里已不含 dsh-plugin:结果页提示重新勾选 */
|
|
113
|
+
const [installBundleDeselected, setInstallBundleDeselected] = useState(false)
|
|
112
114
|
const [uninstallNeedsReload, setUninstallNeedsReload] = useState(false)
|
|
113
115
|
/** 结果视图「立即重启」:请求宿主重启后进入等待,服务回来后整页刷新 */
|
|
114
116
|
const [restarting, setRestarting] = useState(false)
|
|
@@ -143,10 +145,11 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
143
145
|
// 界面语言带给服务端:安装前网络探测等错误消息按用户语言提示(中文界面给中文,英文界面给英文)
|
|
144
146
|
langKey,
|
|
145
147
|
refreshInstalled: catalog.refreshInstalled,
|
|
146
|
-
onInstallDone: (viaModal, repo, needsRestart, update, needsReload) => {
|
|
148
|
+
onInstallDone: (viaModal, repo, needsRestart, update, needsReload, bundleDeselected) => {
|
|
147
149
|
const reload = needsReload === true
|
|
148
150
|
setInstallNeedsRestart(needsRestart)
|
|
149
151
|
setInstallNeedsReload(reload)
|
|
152
|
+
setInstallBundleDeselected(bundleDeselected === true)
|
|
150
153
|
if (viaModal) setInstallDone(true)
|
|
151
154
|
// 弹窗外的后台安装:给结果 toast;需要页面重载时(热挂载的客户端 UI)读完后自动刷新
|
|
152
155
|
else setToast({
|
|
@@ -661,10 +664,12 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
661
664
|
submitting: queue.submitting,
|
|
662
665
|
needsRestart: installNeedsRestart,
|
|
663
666
|
needsReload: installNeedsReload,
|
|
667
|
+
bundleDeselected: installBundleDeselected,
|
|
664
668
|
// 结果弹窗关闭即生效:安装热挂载的插件要页面重载才渲染客户端 UI → 自动刷新,无需用户点按钮
|
|
665
669
|
onClose: () => {
|
|
666
670
|
const reload = installDone && installNeedsReload
|
|
667
671
|
setConfirmPlugin(null)
|
|
672
|
+
setInstallBundleDeselected(false)
|
|
668
673
|
if (reload) window.location.reload()
|
|
669
674
|
},
|
|
670
675
|
onCopy: () => copyCommand(confirmPlugin),
|
|
@@ -13,7 +13,7 @@ import { siteUrl } from '../../logic/urls.ts'
|
|
|
13
13
|
import { useIncrementalList } from '../../hooks/useIncrementalList.ts'
|
|
14
14
|
import { PluginCard } from './PluginCard.tsx'
|
|
15
15
|
|
|
16
|
-
export function CatalogList({ plugins, failed, visible, total, t, langPath, reload, category, copied, installedName, installedVersion, hasUpdate, langKey, onInstall, onUninstall, hasProxy, onOpenDiagnostics }: {
|
|
16
|
+
export function CatalogList({ plugins, failed, visible, total, t, langPath, reload, forceReload, category, copied, installedName, installedVersion, hasUpdate, langKey, onInstall, onUninstall, hasProxy, onOpenDiagnostics }: {
|
|
17
17
|
plugins: HubPlugin[] | null
|
|
18
18
|
failed: boolean
|
|
19
19
|
visible: HubPlugin[]
|
|
@@ -21,6 +21,8 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
|
|
|
21
21
|
t: Translate
|
|
22
22
|
langPath: string
|
|
23
23
|
reload: () => void
|
|
24
|
+
/** 跳过服务端缓存强制回源:缓存数据异常或弱网重试时用 */
|
|
25
|
+
forceReload: () => void
|
|
24
26
|
category: string
|
|
25
27
|
copied: string | null
|
|
26
28
|
installedName: (p: HubPlugin) => string | null
|
|
@@ -52,6 +54,8 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
|
|
|
52
54
|
h('div', { className: styles.stateDesc }, t(hasProxy ? 'failedDescProxy' : 'failedDescNoProxy')),
|
|
53
55
|
h('div', { className: styles.stateActions },
|
|
54
56
|
h('button', { className: styles.retryBtn, onClick: () => reload() }, t('retry')),
|
|
57
|
+
// 强制刷新:跳过服务端缓存直接回源,缓存异常/弱网时可绕过旧数据重新拉取
|
|
58
|
+
h('button', { className: styles.retryBtn, onClick: () => forceReload(), title: t('forceReloadHint') }, t('forceReload')),
|
|
55
59
|
h('button', { className: styles.diagBtn, onClick: onOpenDiagnostics }, t('failNetworkRunDiag')),
|
|
56
60
|
),
|
|
57
61
|
),
|
|
@@ -36,11 +36,12 @@ export function SectionTabs({ view, setView, installedCount, t, noticeCount, onO
|
|
|
36
36
|
role: 'tab',
|
|
37
37
|
'aria-selected': view === id,
|
|
38
38
|
title: t(hintKey),
|
|
39
|
+
'aria-label': t(labelKey),
|
|
39
40
|
className: view === id ? styles.tabActive : styles.tab,
|
|
40
41
|
onClick: () => setView(id),
|
|
41
42
|
},
|
|
42
43
|
h('span', { className: styles.tabIcon }, h(Icon)),
|
|
43
|
-
t(labelKey),
|
|
44
|
+
h('span', { className: styles.tabLabel }, t(labelKey)),
|
|
44
45
|
id === 'installed' && installedCount > 0
|
|
45
46
|
? h('span', { className: view === id ? styles.tabCountActive : styles.tabCount }, installedCount)
|
|
46
47
|
: null)),
|
|
@@ -296,6 +296,10 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
296
296
|
// 给安装指引,不引导提 Issue
|
|
297
297
|
return h('div', { className: styles.failPrepareHint }, t('failNpmMissingHint'))
|
|
298
298
|
}
|
|
299
|
+
if (kind === 'buildToolchainMissing') {
|
|
300
|
+
// 原生模块要本机编译,缺编译器属于用户环境,不引导提 Issue
|
|
301
|
+
return h('div', { className: styles.failPrepareHint }, t('failBuildToolchainHint'))
|
|
302
|
+
}
|
|
299
303
|
if (kind === 'pnpmWorkspace') {
|
|
300
304
|
// pnpm 拒绝把依赖装到 workspace 根目录(ERR_PNPM_ADDING_TO_ROOT):宿主在 profile
|
|
301
305
|
// 目录调 pnpm add 未声明在根操作,任何插件都会失败 ——
|
|
@@ -25,7 +25,7 @@ import type { FailureAction } from '../../logic/failures.ts'
|
|
|
25
25
|
* - 其余(含 needsReload=true 的「已热挂载、仅剩客户端 UI」)→ 仅「完成」按钮:关闭结果弹窗时
|
|
26
26
|
* 上层自动刷新页面让插件生效,不再给「稍后/立即刷新」手动选择。 */
|
|
27
27
|
function ResultView({
|
|
28
|
-
title, desc, t, restarting, needsRestart, needsReload, onRestart, onClose,
|
|
28
|
+
title, desc, t, restarting, needsRestart, needsReload, bundleDeselected, onRestart, onClose,
|
|
29
29
|
}: {
|
|
30
30
|
title: string
|
|
31
31
|
desc: string
|
|
@@ -34,6 +34,8 @@ function ResultView({
|
|
|
34
34
|
needsRestart: boolean
|
|
35
35
|
/** 仅刷新页面即生效(插件已热挂载):给一句自动刷新说明,按钮仍是「完成」 */
|
|
36
36
|
needsReload?: boolean
|
|
37
|
+
/** 市场自更新成功、但 bundle 被取消勾选:重启后市场不会出现,提示重新勾选 */
|
|
38
|
+
bundleDeselected?: boolean
|
|
37
39
|
onRestart: () => void
|
|
38
40
|
onClose: () => void
|
|
39
41
|
}) {
|
|
@@ -56,6 +58,7 @@ function ResultView({
|
|
|
56
58
|
),
|
|
57
59
|
h('div', { className: styles.resultTitle }, title),
|
|
58
60
|
desc ? h('div', { className: styles.resultDesc }, desc) : null,
|
|
61
|
+
bundleDeselected ? h('div', { className: styles.bundleDeselectedHint }, t('bundleDeselectedHint')) : null,
|
|
59
62
|
needsRestart
|
|
60
63
|
? h('div', null, [
|
|
61
64
|
h('div', { className: styles.resultRestarting }, restarting ? t('restarting') : t('restartHint')),
|
|
@@ -95,6 +98,8 @@ export interface InstallModalProps {
|
|
|
95
98
|
needsRestart: boolean
|
|
96
99
|
/** 完成结果是否只需刷新页面即生效(插件已热挂载):true → 结果视图显示自动刷新说明 */
|
|
97
100
|
needsReload: boolean
|
|
101
|
+
/** 市场自更新成功、但 profile 的 bundle 列表已不含 dsh-plugin:结果页提示重新勾选 */
|
|
102
|
+
bundleDeselected?: boolean
|
|
98
103
|
onClose: () => void
|
|
99
104
|
onCopy: () => void
|
|
100
105
|
onInstall: () => void
|
|
@@ -107,7 +112,7 @@ export interface InstallModalProps {
|
|
|
107
112
|
* 目录插件与命令行安装(customTarget 模式)共用同一套确认/进度/结果流程。
|
|
108
113
|
*/
|
|
109
114
|
export function InstallModal(props: InstallModalProps) {
|
|
110
|
-
const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, needsReload, onClose, onCopy, onInstall, onRestart } = props
|
|
115
|
+
const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, needsReload, bundleDeselected, onClose, onCopy, onInstall, onRestart } = props
|
|
111
116
|
const busy = submitting || (task !== null && (task.status === 'pending' || task.status === 'running'))
|
|
112
117
|
// 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分;
|
|
113
118
|
// 命令行安装无插件元数据 → 直接用输入的目标展示;全局 npm 安装 → 包列表空格拼接
|
|
@@ -148,6 +153,7 @@ export function InstallModal(props: InstallModalProps) {
|
|
|
148
153
|
restarting,
|
|
149
154
|
needsRestart,
|
|
150
155
|
needsReload,
|
|
156
|
+
bundleDeselected,
|
|
151
157
|
onRestart,
|
|
152
158
|
onClose,
|
|
153
159
|
})
|
|
@@ -431,6 +437,9 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
431
437
|
: failureKind === 'npmMissing'
|
|
432
438
|
// 本机缺 npm,不是插件问题
|
|
433
439
|
? h('div', { className: styles.failPrepareHint }, t('failNpmMissingHint'))
|
|
440
|
+
: failureKind === 'buildToolchainMissing'
|
|
441
|
+
// 原生模块要本机编译,缺编译器属于用户环境,不引导提 Issue
|
|
442
|
+
? h('div', { className: styles.failPrepareHint }, t('failBuildToolchainHint'))
|
|
434
443
|
: failureKind === 'pnpmWorkspace'
|
|
435
444
|
// pnpm 拒绝把依赖装到 workspace 根目录(ERR_PNPM_ADDING_TO_ROOT):宿主在 profile 目录
|
|
436
445
|
// 调 pnpm add 时未声明在根操作,任何插件都会失败,不是插件问题 ——
|
|
@@ -12,8 +12,9 @@ import { normalize } from '../logic/normalize.ts'
|
|
|
12
12
|
// 目录/统计数据经服务端 /catalog 代理路由获取(curl 子进程注入设置里的代理),
|
|
13
13
|
// 与 npm / git 安装通道同一代理口径 —— 浏览器不直连 api.dsh-plugin.org。
|
|
14
14
|
const PROXY_BASE = '/dsh-plugin-hub/catalog'
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
// refresh=1:跳过服务端 1 小时缓存直接回源(失败仍回退旧缓存),供「强制刷新」使用
|
|
16
|
+
const PLUGINS_URL = (lang: LocaleId, refresh: boolean) => `${PROXY_BASE}?lang=${lang}${refresh ? '&refresh=1' : ''}`
|
|
17
|
+
const STATS_URL = (refresh: boolean) => `${PROXY_BASE}?stats=1${refresh ? '&refresh=1' : ''}`
|
|
17
18
|
|
|
18
19
|
async function fetchJson(url: string, signal?: AbortSignal): Promise<unknown> {
|
|
19
20
|
const res = await fetch(url, { cache: 'no-store', signal })
|
|
@@ -22,15 +23,16 @@ async function fetchJson(url: string, signal?: AbortSignal): Promise<unknown> {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** 目录插件列表(在线 API 已只返回 verified;过滤与排序由上层负责)。
|
|
25
|
-
* signal 可选:宿主重启/代理抖动导致请求挂起时,上层可中止旧请求释放连接。
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
* signal 可选:宿主重启/代理抖动导致请求挂起时,上层可中止旧请求释放连接。
|
|
27
|
+
* refresh=true 跳过服务端缓存,强制回源。 */
|
|
28
|
+
export async function fetchCatalog(lang: LocaleId, signal?: AbortSignal, refresh = false): Promise<HubPlugin[]> {
|
|
29
|
+
const data = await fetchJson(PLUGINS_URL(lang, refresh), signal)
|
|
28
30
|
return (Array.isArray(data) ? data : []).map((item) => normalize(item as Record<string, unknown>))
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** 收录/精选统计(/api/stats.json);字段不完整返回 null。 */
|
|
32
|
-
export async function fetchStats(signal?: AbortSignal): Promise<{ total: number; verified: number } | null> {
|
|
33
|
-
const s = await fetchJson(STATS_URL, signal) as { total?: number; verified?: number }
|
|
34
|
+
export async function fetchStats(signal?: AbortSignal, refresh = false): Promise<{ total: number; verified: number } | null> {
|
|
35
|
+
const s = await fetchJson(STATS_URL(refresh), signal) as { total?: number; verified?: number }
|
|
34
36
|
if (s && typeof s.total === 'number' && typeof s.verified === 'number') {
|
|
35
37
|
return { total: s.total, verified: s.verified }
|
|
36
38
|
}
|
|
@@ -52,10 +52,12 @@ export function useCatalog(lang: LocaleId) {
|
|
|
52
52
|
* 界面停在「正在加载插件数据…」假死 —— 超时自动重拉,最多自动重试 2 次后转失败态;
|
|
53
53
|
* 用户手动刷新(reload)时清零重新计数。 */
|
|
54
54
|
const loadRetriesRef = useRef(0)
|
|
55
|
-
/** 目录加载超时阈值:超过即判定挂起,强制刷新一次(服务端代理 curl 上限
|
|
56
|
-
const LOAD_TIMEOUT_MS =
|
|
57
|
-
/** 挂起后最多自动重试次数:避免代理持续不通时无限循环刷新(每次重试仍
|
|
55
|
+
/** 目录加载超时阈值:超过即判定挂起,强制刷新一次(服务端代理 curl 上限 60s,客户端 30s 先兜底) */
|
|
56
|
+
const LOAD_TIMEOUT_MS = 30000
|
|
57
|
+
/** 挂起后最多自动重试次数:避免代理持续不通时无限循环刷新(每次重试仍 30s,2 次后转失败界面可手动重试) */
|
|
58
58
|
const MAX_LOAD_RETRIES = 2
|
|
59
|
+
/** 本次 reload 是否强制回源(跳过服务端 1 小时缓存):由「强制刷新」置位,读取后立即复位 */
|
|
60
|
+
const forceRefreshRef = useRef(false)
|
|
59
61
|
|
|
60
62
|
/** 点击排序按钮:同一按钮切换正/倒序,新按钮用默认方向 */
|
|
61
63
|
function toggleSort(key: SortKey) {
|
|
@@ -99,12 +101,14 @@ export function useCatalog(lang: LocaleId) {
|
|
|
99
101
|
// 中止控制器:请求挂起时(宿主重启/代理抖动)超时自动重试会换新请求,
|
|
100
102
|
// 旧请求在此中止释放连接,避免挂起堆积
|
|
101
103
|
const controller = new AbortController()
|
|
104
|
+
const refresh = forceRefreshRef.current
|
|
105
|
+
forceRefreshRef.current = false
|
|
102
106
|
setPlugins(null)
|
|
103
107
|
setHubPlugin(null)
|
|
104
108
|
setStats(null)
|
|
105
109
|
setFailed(false)
|
|
106
110
|
// 此处只拉目录与统计;hub 自身版本由独立 Worker 版本控制中心提供(见 fetchHubUpdate)。
|
|
107
|
-
const load = () => Promise.all([fetchCatalog(lang, controller.signal), fetchStats(controller.signal)])
|
|
111
|
+
const load = () => Promise.all([fetchCatalog(lang, controller.signal, refresh), fetchStats(controller.signal, refresh)])
|
|
108
112
|
load()
|
|
109
113
|
.then(([list, stats]) => {
|
|
110
114
|
if (cancelled) return
|
|
@@ -321,6 +325,8 @@ export function useCatalog(lang: LocaleId) {
|
|
|
321
325
|
stats,
|
|
322
326
|
failed,
|
|
323
327
|
reload: () => { loadRetriesRef.current = 0; setReloadKey((k) => k + 1) },
|
|
328
|
+
/** 强制刷新:跳过服务端 1 小时缓存直接回源,成功后覆盖缓存(失败仍回退旧缓存) */
|
|
329
|
+
forceReload: () => { loadRetriesRef.current = 0; forceRefreshRef.current = true; setReloadKey((k) => k + 1) },
|
|
324
330
|
installed,
|
|
325
331
|
installedItems,
|
|
326
332
|
installedName,
|
|
@@ -54,6 +54,8 @@ export interface QueueTask {
|
|
|
54
54
|
/** 完成结果是否只需刷新页面即生效(插件已热挂进运行中 loader,或卸载后插件面板仍残留在当前页面):
|
|
55
55
|
* true 时关闭结果弹窗 / Toast 消失后由上层自动刷新页面,不给手动刷新按钮。 */
|
|
56
56
|
needsReload?: boolean
|
|
57
|
+
/** 市场自更新成功、但 profile 的 bundle 列表里已不含 dsh-plugin:重启后市场不会出现 */
|
|
58
|
+
bundleDeselected?: boolean
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/** 客户端待重启项:镜像服务端内存列表 + 本地补齐的展示信息(简介/版本)。 */
|
|
@@ -76,7 +78,7 @@ export interface TaskQueueOptions {
|
|
|
76
78
|
* needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」);
|
|
77
79
|
* needsReload 表示是否只需刷新页面即生效(热挂载成功、或卸载后插件面板仍残留)。
|
|
78
80
|
* update 表示实际执行的是更新(覆盖重装)而非首次安装:通知中心据此显示「更新成功」而非「安装成功」。 */
|
|
79
|
-
onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, update?: boolean, needsReload?: boolean) => void
|
|
81
|
+
onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, update?: boolean, needsReload?: boolean, bundleDeselected?: boolean) => void
|
|
80
82
|
onUninstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, needsReload?: boolean) => void
|
|
81
83
|
/** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 实际执行/重试过的安装命令(可缺省)+ 是否更新。 */
|
|
82
84
|
onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[], update?: boolean) => void
|
|
@@ -212,7 +214,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
212
214
|
// 同时缓存展示信息,待重启列表(服务端已登记)合并时直接补齐简介/版本
|
|
213
215
|
void syncInstalledVersion(q.repo, q.version, q.updatedAt)
|
|
214
216
|
pendingInfoRef.current.set(q.target, { desc: q.desc, version: q.version })
|
|
215
|
-
onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart, q.action === 'update', q.needsReload ?? false)
|
|
217
|
+
onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart, q.action === 'update', q.needsReload ?? false, q.bundleDeselected === true)
|
|
216
218
|
}
|
|
217
219
|
} else {
|
|
218
220
|
// 失败:完整展示全部输出行(最新在前,逆序为日志阅读顺序),不裁剪
|
|
@@ -257,20 +259,22 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
257
259
|
let status = 'failed'
|
|
258
260
|
let lines: string[] = []
|
|
259
261
|
let attempts: string[] | undefined
|
|
262
|
+
let bundleDeselected: boolean | undefined
|
|
260
263
|
try {
|
|
261
264
|
const res = await fetch(`/dsh-plugin-hub/status?task=${q.id}`, { cache: 'no-store' })
|
|
262
265
|
if (res.ok) {
|
|
263
|
-
const data = await res.json() as { task?: { status?: string; lines?: string[]; attempts?: string[]; needsRestart?: unknown; needsReload?: unknown } }
|
|
266
|
+
const data = await res.json() as { task?: { status?: string; lines?: string[]; attempts?: string[]; needsRestart?: unknown; needsReload?: unknown; bundleDeselected?: unknown } }
|
|
264
267
|
const task = data.task
|
|
265
268
|
if (task !== undefined) {
|
|
266
269
|
status = task.status ?? 'failed'
|
|
267
270
|
lines = task.lines ?? []
|
|
268
271
|
attempts = Array.isArray(task.attempts) ? task.attempts : undefined
|
|
272
|
+
bundleDeselected = task.bundleDeselected === true ? true : undefined
|
|
269
273
|
// 终态带出的「是否需要重启 / 是否只需刷新」覆盖本地(服务端 done 时按 loader 热挂/即时移除情况设置)
|
|
270
274
|
const needsRestart = typeof task.needsRestart === 'boolean' ? task.needsRestart : q.needsRestart
|
|
271
275
|
const needsReload = typeof task.needsReload === 'boolean' ? task.needsReload : (q.needsReload ?? false)
|
|
272
276
|
if (status === 'done') {
|
|
273
|
-
const settled = { ...q, needsRestart, needsReload }
|
|
277
|
+
const settled = { ...q, needsRestart, needsReload, bundleDeselected: bundleDeselected === true ? true : q.bundleDeselected }
|
|
274
278
|
// 卸载成功走进度过渡动画;安装成功直接收尾
|
|
275
279
|
if (q.kind === 'uninstall') settleDone(settled, lines)
|
|
276
280
|
else finishQueueTask(true, settled, lines)
|
|
@@ -297,7 +301,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
297
301
|
const res = await fetch('/dsh-plugin-hub/active', { cache: 'no-store' })
|
|
298
302
|
if (!res.ok) throw new Error(`active ${res.status}`)
|
|
299
303
|
const data = await res.json() as {
|
|
300
|
-
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown; installChannel?: unknown }[]
|
|
304
|
+
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown; installChannel?: unknown; bundleDeselected?: unknown }[]
|
|
301
305
|
pendingRestarts?: unknown
|
|
302
306
|
}
|
|
303
307
|
const active = (data.tasks ?? []).filter((a) => typeof a.id === 'number')
|
|
@@ -327,6 +331,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
327
331
|
lines: Array.isArray(a.lines) ? (a.lines as string[]) : (prevTask?.lines ?? []),
|
|
328
332
|
attempts: Array.isArray(a.attempts) ? (a.attempts as string[]) : (prevTask?.attempts ?? []),
|
|
329
333
|
needsRestart: typeof a.needsRestart === 'boolean' ? a.needsRestart : (prevTask?.needsRestart ?? true),
|
|
334
|
+
bundleDeselected: a.bundleDeselected === true || prevTask?.bundleDeselected === true,
|
|
330
335
|
// 入口渠道:本地乐观入队已带;服务端 /active 也带回(刷新恢复用),服务端优先。
|
|
331
336
|
// 旧服务端可能回传已废弃的 'git'/'dsh' 渠道,忽略即可、回退本地值。
|
|
332
337
|
channel: a.installChannel === 'npm' ? 'npm' : prevTask?.channel,
|
|
@@ -502,8 +507,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
502
507
|
// 网络异常:移除乐观条目 + 兜底错误弹窗(附上底层原因,避免只剩一句笼统的「安装失败」)
|
|
503
508
|
applyQueue((prev) => prev.filter((x) => x.id !== tempId))
|
|
504
509
|
const aborted = (err as { name?: string } | null)?.name === 'AbortError'
|
|
510
|
+
// [request-timeout]:给分类器一个与界面语言无关的超时标记
|
|
505
511
|
const reason = aborted
|
|
506
|
-
? t('requestTimeout')
|
|
512
|
+
? `${t('requestTimeout')}\n[request-timeout]`
|
|
507
513
|
: err instanceof Error && err.message ? err.message : String(err ?? '')
|
|
508
514
|
// 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」
|
|
509
515
|
const entry = entryLineOf(t, input.channel)
|
|
@@ -601,8 +607,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
601
607
|
} catch (err) {
|
|
602
608
|
// 网络异常:附上底层原因,避免只剩一句笼统的「卸载失败」
|
|
603
609
|
const aborted = (err as { name?: string } | null)?.name === 'AbortError'
|
|
610
|
+
// [request-timeout]:给分类器一个与界面语言无关的超时标记
|
|
604
611
|
const reason = aborted
|
|
605
|
-
? t('requestTimeout')
|
|
612
|
+
? `${t('requestTimeout')}\n[request-timeout]`
|
|
606
613
|
: err instanceof Error && err.message ? err.message : String(err ?? '')
|
|
607
614
|
onError(reason ? `${t('uninstallFail')} — ${reason}` : t('uninstallFail'), repo, 'uninstall')
|
|
608
615
|
} finally {
|