dsh-plugin-pack 1.0.7 → 1.0.9
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.js +48 -32
- package/lib/fetch-bundle.js +147 -22
- package/lib/server.js +7 -1
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -130,6 +130,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
130
130
|
'net.warnTitle': '安装前请确认来源可信',
|
|
131
131
|
'net.warnBody': '整合包会覆盖已装插件的文件与配置,来源不明的包可能损坏 DeepSeek Harness。安装前会先列出重名插件,由你逐个决定覆盖还是跳过。',
|
|
132
132
|
'net.done': '下载完成,已导入',
|
|
133
|
+
'net.hintTitle': '可以这样处理:',
|
|
133
134
|
'import.pageTitle': '导入插件整合包',
|
|
134
135
|
'drop.title': '把 zip / tgz 或插件文件夹拖到这里',
|
|
135
136
|
'drop.release': '松手就导入',
|
|
@@ -312,6 +313,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
312
313
|
'net.warnTitle': 'Verify the source before installing',
|
|
313
314
|
'net.warnBody': 'A bundle overwrites installed plugins and their config. Untrusted sources can damage DeepSeek Harness. You will be asked per-plugin whether to overwrite or skip.',
|
|
314
315
|
'net.done': 'Downloaded and imported',
|
|
316
|
+
'net.hintTitle': 'What you can do: ',
|
|
315
317
|
'import.pageTitle': 'Import plugin bundle',
|
|
316
318
|
'drop.title': 'Drop a zip / tgz or plugin folder here',
|
|
317
319
|
'drop.release': 'Release to import',
|
|
@@ -504,6 +506,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
504
506
|
const error = new Error((data && (data.error || data.message)) || `${response.status} ${response.statusText}`);
|
|
505
507
|
// 把服务端的错误码带出来 —— 界面据此区分"拖错文件"和"真出故障"
|
|
506
508
|
if (data?.code) error.code = String(data.code);
|
|
509
|
+
// 网络类错误带处置建议(挂代理 / 换网络 / 稍后重试)
|
|
510
|
+
if (data?.hint) error.hint = String(data.hint);
|
|
507
511
|
throw error;
|
|
508
512
|
}
|
|
509
513
|
return data;
|
|
@@ -1459,7 +1463,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1459
1463
|
h('div', null, t('net.warnBody')),
|
|
1460
1464
|
),
|
|
1461
1465
|
|
|
1462
|
-
// 结果区:成功 / 不是整合包 / 其它错误
|
|
1466
|
+
// 结果区:成功 / 不是整合包 / 网络失败(带处置建议)/ 其它错误
|
|
1463
1467
|
result
|
|
1464
1468
|
? h(
|
|
1465
1469
|
'div',
|
|
@@ -1472,7 +1476,29 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1472
1476
|
h('div', { className: 'pp-not-bundle-title' }, t('net.notBundle')),
|
|
1473
1477
|
h('div', { className: 'pp-not-bundle-detail' }, String(result.error)),
|
|
1474
1478
|
)
|
|
1475
|
-
: h(
|
|
1479
|
+
: h(
|
|
1480
|
+
'div',
|
|
1481
|
+
null,
|
|
1482
|
+
h(Alert, { kind: 'err' }, String(result.error)),
|
|
1483
|
+
// 网络类错误:把"该怎么做"单独列出来,别让用户卡在报错上
|
|
1484
|
+
result.hint
|
|
1485
|
+
? h(
|
|
1486
|
+
'div',
|
|
1487
|
+
{
|
|
1488
|
+
className: 'pp-hint',
|
|
1489
|
+
style: {
|
|
1490
|
+
marginTop: 6,
|
|
1491
|
+
padding: '8px 10px',
|
|
1492
|
+
borderRadius: 8,
|
|
1493
|
+
background: 'color-mix(in srgb,var(--dsw-alias-label-secondary,#6b7280) 8%,transparent)',
|
|
1494
|
+
lineHeight: 1.7,
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
h('b', null, t('net.hintTitle')),
|
|
1498
|
+
String(result.hint),
|
|
1499
|
+
)
|
|
1500
|
+
: null,
|
|
1501
|
+
)
|
|
1476
1502
|
: h(
|
|
1477
1503
|
'div',
|
|
1478
1504
|
{ className: 'pp-hint' },
|
|
@@ -2196,7 +2222,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2196
2222
|
await refreshJobs();
|
|
2197
2223
|
} catch (err) {
|
|
2198
2224
|
const code = err?.code ? String(err.code) : null;
|
|
2199
|
-
setNetResult({ error: String(err?.message ?? err), code });
|
|
2225
|
+
setNetResult({ error: String(err?.message ?? err), code, hint: err?.hint ?? null });
|
|
2200
2226
|
} finally {
|
|
2201
2227
|
setPhase('');
|
|
2202
2228
|
setBusy(false);
|
|
@@ -2325,6 +2351,25 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2325
2351
|
hotBusy ? h(Spinner, { label: t('hotload.running') }) : t('hotload'),
|
|
2326
2352
|
),
|
|
2327
2353
|
h('button', { className: 'pp-btn sm', disabled: busy, onClick: load }, busy ? h(Spinner, { label: t('loading') }) : t('refresh')),
|
|
2354
|
+
/* 「添加整合包」放在这一行最右,对齐 DSH 自带的「添加插件」位置
|
|
2355
|
+
* (页面标题行右上角),而不是沉到「导入插件整合包」小标题旁边。
|
|
2356
|
+
* 只在导入页出现,其它子页不占位。 */
|
|
2357
|
+
onlyTab === 'import' || tab === 'import'
|
|
2358
|
+
? h(
|
|
2359
|
+
'button',
|
|
2360
|
+
{
|
|
2361
|
+
className: 'pp-btn sm',
|
|
2362
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 5 },
|
|
2363
|
+
disabled: busy,
|
|
2364
|
+
onClick: () => {
|
|
2365
|
+
setNetResult(null);
|
|
2366
|
+
setNetOpen(true);
|
|
2367
|
+
},
|
|
2368
|
+
},
|
|
2369
|
+
h('span', { style: { fontSize: 14, lineHeight: 1 } }, '+'),
|
|
2370
|
+
t('net.button'),
|
|
2371
|
+
)
|
|
2372
|
+
: null,
|
|
2328
2373
|
),
|
|
2329
2374
|
error ? h(Alert, { kind: 'err' }, `${t('failed')}: ${error}`) : null,
|
|
2330
2375
|
hotResult
|
|
@@ -2550,35 +2595,6 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2550
2595
|
const importPage = h(
|
|
2551
2596
|
'div',
|
|
2552
2597
|
null,
|
|
2553
|
-
/* 右上角「添加整合包」按钮 —— 和 DSH 自带的「添加插件」同位置同风格。
|
|
2554
|
-
* 点开是一个弹窗,在里面填地址、点安装。 */
|
|
2555
|
-
h(
|
|
2556
|
-
'div',
|
|
2557
|
-
{
|
|
2558
|
-
style: {
|
|
2559
|
-
display: 'flex',
|
|
2560
|
-
alignItems: 'center',
|
|
2561
|
-
justifyContent: 'space-between',
|
|
2562
|
-
gap: 12,
|
|
2563
|
-
marginBottom: 12,
|
|
2564
|
-
},
|
|
2565
|
-
},
|
|
2566
|
-
h('div', { style: { fontSize: 14, fontWeight: 600 } }, t('import.pageTitle')),
|
|
2567
|
-
h(
|
|
2568
|
-
'button',
|
|
2569
|
-
{
|
|
2570
|
-
className: 'pp-btn',
|
|
2571
|
-
style: { display: 'inline-flex', alignItems: 'center', gap: 6, flex: 'none' },
|
|
2572
|
-
onClick: () => {
|
|
2573
|
-
setNetResult(null);
|
|
2574
|
-
setNetOpen(true);
|
|
2575
|
-
},
|
|
2576
|
-
},
|
|
2577
|
-
h('span', { style: { fontSize: 15, lineHeight: 1 } }, '+'),
|
|
2578
|
-
t('net.button'),
|
|
2579
|
-
),
|
|
2580
|
-
),
|
|
2581
|
-
|
|
2582
2598
|
h(DropZone, { onFile: runDrop, busy, dropRef: lastDrop, phase }),
|
|
2583
2599
|
|
|
2584
2600
|
// 后台任务:切走画面回来能看到刚才那次导入的结果
|
package/lib/fetch-bundle.js
CHANGED
|
@@ -128,10 +128,15 @@ async function resolveReleaseAsset({ owner, repo, tag }) {
|
|
|
128
128
|
? `${apiBase}/repos/${owner}/${repo}/releases/latest`
|
|
129
129
|
: `${apiBase}/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`;
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
let response;
|
|
132
|
+
try {
|
|
133
|
+
response = await fetchWithTimeout(endpoint, {
|
|
134
|
+
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'dsh-plugin-pack' },
|
|
135
|
+
timeoutMs: 30000,
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw explainNetworkError(error, endpoint, '查 GitHub Release');
|
|
139
|
+
}
|
|
135
140
|
if (!response.ok) {
|
|
136
141
|
const error = new Error(
|
|
137
142
|
`查 GitHub Release 失败(${response.status})。这个仓库可能没有 Release,或者 tag「${tag}」不存在。`,
|
|
@@ -185,10 +190,15 @@ async function resolveNpmTarball(spec) {
|
|
|
185
190
|
const wantedVersion = hasVersion ? spec.slice(at + 1) : null;
|
|
186
191
|
|
|
187
192
|
const endpoint = `https://registry.npmjs.org/${pkgName.replace(/\//g, '%2F')}`;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
let response;
|
|
194
|
+
try {
|
|
195
|
+
response = await fetchWithTimeout(endpoint, {
|
|
196
|
+
headers: { Accept: 'application/json', 'User-Agent': 'dsh-plugin-pack' },
|
|
197
|
+
timeoutMs: 30000,
|
|
198
|
+
});
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw explainNetworkError(error, endpoint, '查 npm 包');
|
|
201
|
+
}
|
|
192
202
|
if (!response.ok) {
|
|
193
203
|
const error = new Error(`查 npm 包失败(${response.status}):${pkgName}`);
|
|
194
204
|
error.code = 'BAD_SOURCE';
|
|
@@ -211,32 +221,147 @@ async function resolveNpmTarball(spec) {
|
|
|
211
221
|
return { url, name: `${data.name}-${version}.tgz`, from: 'npm' };
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
/**
|
|
225
|
+
* 把底层网络错误翻译成看得懂、能动手的中文。
|
|
226
|
+
*
|
|
227
|
+
* 为什么需要:Node 的 fetch 在网络不通时只抛 `TypeError: fetch failed`,
|
|
228
|
+
* 真正的原因藏在 error.cause 里(ENOTFOUND / ECONNRESET / 证书错误…)。
|
|
229
|
+
* 直接抛出去用户只看到"fetch failed",不知道是网断了、域名错了,
|
|
230
|
+
* 还是被墙了、要挂代理。
|
|
231
|
+
*
|
|
232
|
+
* @param {unknown} error 原始错误
|
|
233
|
+
* @param {string} url 正在访问的地址
|
|
234
|
+
* @param {string} [stage] 阶段描述(下载 / 查 registry / 查 Release)
|
|
235
|
+
* @returns {Error & { code: string, hint?: string }}
|
|
236
|
+
*/
|
|
237
|
+
export function explainNetworkError(error, url, stage = '下载') {
|
|
238
|
+
let host = '';
|
|
239
|
+
try {
|
|
240
|
+
host = new URL(url).host;
|
|
241
|
+
} catch {
|
|
242
|
+
host = url;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 把 cause 链上的信息拼起来判断(message + code + errno)
|
|
246
|
+
const cause = error?.cause ?? error;
|
|
247
|
+
const raw = [cause?.code, cause?.errno, cause?.message, error?.message]
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.join(' ')
|
|
250
|
+
.toLowerCase();
|
|
251
|
+
|
|
252
|
+
const make = (message, hint) => {
|
|
253
|
+
const wrapped = new Error(message);
|
|
254
|
+
wrapped.code = 'NETWORK';
|
|
255
|
+
if (hint) wrapped.hint = hint;
|
|
256
|
+
return wrapped;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// 超时(我们自己 abort 的)
|
|
260
|
+
if (error?.name === 'AbortError' || /aborted|timeout|timed out|etimedout|und_err_connect_timeout/.test(raw)) {
|
|
261
|
+
return make(
|
|
262
|
+
`${stage}超时:${host} 在限定时间内没有响应。`,
|
|
263
|
+
'网络慢或被劫持时常见。可以稍后重试;如果一直超时,试试换网络或配代理。',
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// DNS:域名解析不出来
|
|
268
|
+
if (/enotfound|eai_again|getaddrinfo|dns/.test(raw)) {
|
|
269
|
+
return make(
|
|
270
|
+
`${stage}失败:解析不了域名 ${host}。`,
|
|
271
|
+
'检查地址有没有拼错;如果域名是对的,通常是 DNS 问题或该域名被污染。',
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 连接被重置 / 主动断开:国内访问 GitHub 最常见
|
|
276
|
+
if (/econnreset|socket hang up|epipe|ecanceled/.test(raw)) {
|
|
277
|
+
return make(
|
|
278
|
+
`${stage}失败:与 ${host} 的连接被重置。`,
|
|
279
|
+
host.includes('github')
|
|
280
|
+
? 'GitHub 在国内经常被这样重置。挂代理后重试,或者把包下载到本地,用拖拽导入。'
|
|
281
|
+
: '连接中途断开。检查网络稳定性后重试。',
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 连不上(端口不通 / 代理没起)
|
|
286
|
+
if (/econnrefused|ehostunreach|enetunreach|connect failed/.test(raw)) {
|
|
287
|
+
return make(
|
|
288
|
+
`${stage}失败:连不上 ${host}。`,
|
|
289
|
+
'目标拒绝连接,或者你的代理设置指向了一个没在运行的端口。',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 证书问题
|
|
294
|
+
if (/cert|self.signed|unable to verify|ssl|tls/.test(raw)) {
|
|
295
|
+
return make(
|
|
296
|
+
`${stage}失败:${host} 的 HTTPS 证书校验不通过。`,
|
|
297
|
+
'可能是公司网络中间人代理或系统时间不对。确认来源可信后再决定是否继续。',
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 代理相关
|
|
302
|
+
if (/proxy/.test(raw)) {
|
|
303
|
+
return make(
|
|
304
|
+
`${stage}失败:代理连接出错(访问 ${host})。`,
|
|
305
|
+
'检查 HTTP_PROXY / HTTPS_PROXY 环境变量或系统代理设置。',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 兜底:把真实原因也带上,别吞掉
|
|
310
|
+
const detail = String(cause?.message ?? error?.message ?? error).slice(0, 160);
|
|
311
|
+
return make(
|
|
312
|
+
`${stage}失败:访问 ${host} 时网络出错。`,
|
|
313
|
+
`原始信息:${detail}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
214
317
|
/** 从 URL 流式下载到 Buffer(带体积上限)。 */
|
|
215
318
|
async function download(url) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
319
|
+
let response;
|
|
320
|
+
try {
|
|
321
|
+
response = await fetchWithTimeout(url, {
|
|
322
|
+
headers: { 'User-Agent': 'dsh-plugin-pack', Accept: '*/*' },
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
// 连都没连上 —— 翻译成人话再抛
|
|
326
|
+
throw explainNetworkError(error, url, '下载');
|
|
327
|
+
}
|
|
328
|
+
|
|
219
329
|
if (!response.ok) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
330
|
+
// 按状态码给不同的处置建议,而不是一句"下载失败"
|
|
331
|
+
const hints = {
|
|
332
|
+
401: '需要登录才能下载(私有仓库)。',
|
|
333
|
+
403: '服务器拒绝访问。可能是私有仓库、触发限流,或者需要认证。',
|
|
334
|
+
404: '地址不存在。检查链接是否失效、仓库是否改名、Release 是否被删。',
|
|
335
|
+
429: '请求太频繁被限流,等几分钟再试。',
|
|
336
|
+
};
|
|
337
|
+
const base = `下载失败(HTTP ${response.status})。`;
|
|
338
|
+
const error = new Error(base + (hints[response.status] ?? '服务器返回了错误状态。'));
|
|
223
339
|
error.code = 'DOWNLOAD_FAILED';
|
|
224
340
|
throw error;
|
|
225
341
|
}
|
|
226
342
|
|
|
227
343
|
const chunks = [];
|
|
228
344
|
let size = 0;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
345
|
+
try {
|
|
346
|
+
for await (const chunk of response.body) {
|
|
347
|
+
size += chunk.length;
|
|
348
|
+
if (size > MAX_BYTES) {
|
|
349
|
+
const error = new Error(`文件超过 ${MAX_BYTES / 1048576} MiB,已中止`);
|
|
350
|
+
error.code = 'TOO_LARGE';
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
chunks.push(Buffer.from(chunk));
|
|
235
354
|
}
|
|
236
|
-
|
|
355
|
+
} catch (error) {
|
|
356
|
+
// 下到一半断了(大文件走代理时很常见)
|
|
357
|
+
if (error?.code === 'TOO_LARGE') throw error;
|
|
358
|
+
const wrapped = explainNetworkError(error, url, '下载');
|
|
359
|
+
wrapped.message = `${wrapped.message}(已收到 ${(size / 1048576).toFixed(1)} MiB,可能没下完)`;
|
|
360
|
+
throw wrapped;
|
|
237
361
|
}
|
|
362
|
+
|
|
238
363
|
if (!size) {
|
|
239
|
-
const error = new Error('
|
|
364
|
+
const error = new Error('下载到的是空文件(地址可能指向了一个空响应)。');
|
|
240
365
|
error.code = 'DOWNLOAD_FAILED';
|
|
241
366
|
throw error;
|
|
242
367
|
}
|
package/lib/server.js
CHANGED
|
@@ -423,7 +423,13 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
423
423
|
try {
|
|
424
424
|
return await spec.handler(request, response);
|
|
425
425
|
} catch (error) {
|
|
426
|
-
sendJson(response, statusOf(error), {
|
|
426
|
+
sendJson(response, statusOf(error), {
|
|
427
|
+
ok: false,
|
|
428
|
+
error: String(error?.message ?? error),
|
|
429
|
+
code: error?.code,
|
|
430
|
+
// 网络类错误带处置建议,界面会显示在错误下面
|
|
431
|
+
...(error?.hint ? { hint: String(error.hint) } : {}),
|
|
432
|
+
});
|
|
427
433
|
}
|
|
428
434
|
},
|
|
429
435
|
},
|