dsh-plugin-pack 1.0.8 → 1.0.10

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 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(Alert, { kind: 'err' }, String(result.error))
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);
@@ -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
- const response = await fetchWithTimeout(endpoint, {
132
- headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'dsh-plugin-pack' },
133
- timeoutMs: 30000,
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
- const response = await fetchWithTimeout(endpoint, {
189
- headers: { Accept: 'application/json', 'User-Agent': 'dsh-plugin-pack' },
190
- timeoutMs: 30000,
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
- const response = await fetchWithTimeout(url, {
217
- headers: { 'User-Agent': 'dsh-plugin-pack', Accept: '*/*' },
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
- const error = new Error(
221
- `下载失败(HTTP ${response.status})。地址可能失效、需要登录,或者是私有仓库。`,
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
- for await (const chunk of response.body) {
230
- size += chunk.length;
231
- if (size > MAX_BYTES) {
232
- const error = new Error(`文件超过 ${MAX_BYTES / 1048576} MiB,已中止`);
233
- error.code = 'TOO_LARGE';
234
- throw error;
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
- chunks.push(Buffer.from(chunk));
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
  }
@@ -292,8 +417,9 @@ export async function fetchBundleFrom({ source, home, onProgress = () => {} }) {
292
417
  report(`下载完成:${(data.length / 1048576).toFixed(1)} MiB`);
293
418
 
294
419
  // 落到 inbox(保留扩展名,后续按 .zip/.tgz 分流)
420
+ // origin='network':安装成功后可以自动删掉这份副本,不占用户磁盘
295
421
  pruneSafe(home);
296
- const saved = saveIncomingZip({ home, filename: name, data });
422
+ const saved = saveIncomingZip({ home, filename: name, data, origin: 'network' });
297
423
  report(`已保存到 ${saved.file}`);
298
424
 
299
425
  /* 校验阶段分两种:
package/lib/inbox.js CHANGED
@@ -20,6 +20,32 @@ import { ensureDir, removeTree } from './util.js';
20
20
  /** 单次落盘上限(默认 1 GiB,够大包也够防呆)。 */
21
21
  const MAX_BYTES = 1024 * 1024 * 1024;
22
22
 
23
+ /** 记录这份 inbox 副本来源的标记文件名。 */
24
+ const ORIGIN_MARK = '.origin';
25
+
26
+ /**
27
+ * 在 root 下建一个唯一的子目录:`<时间戳>-<4位随机>`。
28
+ *
29
+ * stamp() 只到秒,同一秒内多次落盘会撞名;加随机后缀既保持可读
30
+ * (按时间排序仍然有效),又能避免互相覆盖。
31
+ *
32
+ * @param {string} root
33
+ * @returns {string} 已创建的目录绝对路径
34
+ */
35
+ function uniqueDir(root) {
36
+ for (let attempt = 0; attempt < 20; attempt += 1) {
37
+ const suffix = Math.random().toString(16).slice(2, 6).padEnd(4, '0');
38
+ const dir = path.join(root, `${stamp()}-${suffix}`);
39
+ if (fs.existsSync(dir)) continue;
40
+ ensureDir(dir);
41
+ return dir;
42
+ }
43
+ // 极端情况下兜底:用纳秒精度的时间戳
44
+ const dir = path.join(root, `${stamp()}-${Date.now() % 100000}`);
45
+ ensureDir(dir);
46
+ return dir;
47
+ }
48
+
23
49
  /**
24
50
  * 从 multipart/form-data 里解析出文件条目。
25
51
  *
@@ -27,8 +53,7 @@ const MAX_BYTES = 1024 * 1024 * 1024;
27
53
  * 不引第三方库。
28
54
  *
29
55
  * @param {Buffer} body 整个请求体
30
- * @param {string} boundary boundary 字符串(不含前置 --)
31
- * @returns {Array<{ name: string, filename: string|null, data: Buffer }>}
56
+ * @param {string} boundary boundary 字符串(不含前置 --) * @returns {Array<{ name: string, filename: string|null, data: Buffer }>}
32
57
  */
33
58
  export function parseMultipart(body, boundary) {
34
59
  const delimiter = Buffer.from(`--${boundary}`);
@@ -87,9 +112,12 @@ export function boundaryOf(contentType) {
87
112
  * @param {string} options.home $DSH_HOME
88
113
  * @param {string} options.filename 原始文件名
89
114
  * @param {Buffer} options.data 内容
115
+ * @param {'upload'|'network'} [options.origin]
116
+ * 来源。`network` = 从 GitHub/npm 下载来的,装完可以自动删;
117
+ * `upload` = 用户从本地拖/选的,装完要留着(用户自己的文件)。
90
118
  * @returns {{ file: string, bytes: number }}
91
119
  */
92
- export function saveIncomingZip({ home, filename, data }) {
120
+ export function saveIncomingZip({ home, filename, data, origin = 'upload' }) {
93
121
  if (!data?.length) {
94
122
  const error = new Error('上传内容为空');
95
123
  error.code = 'BAD_UPLOAD';
@@ -101,8 +129,16 @@ export function saveIncomingZip({ home, filename, data }) {
101
129
  throw error;
102
130
  }
103
131
  const base = safeName(String(filename ?? '').trim() || `bundle-${stamp()}.zip`);
104
- const dir = path.join(home, 'plugin-pack', 'inbox', stamp());
105
- ensureDir(dir);
132
+ /* 目录名要唯一:stamp() 只到秒,同一秒内连拖两个包会落到同一个目录,
133
+ * 后一个会覆盖前一个的 .origin 标记(甚至同名时覆盖文件本身)。
134
+ * 加个短随机后缀避开。 */
135
+ const dir = uniqueDir(path.join(home, 'plugin-pack', 'inbox'));
136
+ // 记下来源:安装成功后据此决定要不要删掉这份落盘副本
137
+ try {
138
+ fs.writeFileSync(path.join(dir, ORIGIN_MARK), origin === 'network' ? 'network' : 'upload');
139
+ } catch {
140
+ /* 标记写不进去不影响导入 */
141
+ }
106
142
  // 保留原始扩展名:.zip = 整合包,.tgz/.tar.gz = 单个插件的 npm tarball
107
143
  const lower = base.toLowerCase();
108
144
  const hasKnownExt = lower.endsWith('.zip') || lower.endsWith('.tgz') || lower.endsWith('.tar.gz');
@@ -192,3 +228,47 @@ export function pruneInbox(home, days = 7) {
192
228
  }
193
229
  return removed;
194
230
  }
231
+
232
+ /**
233
+ * 读这份 inbox 副本的来源。
234
+ *
235
+ * @param {string} file inbox 里的文件绝对路径
236
+ * @returns {'upload'|'network'|null} 读不到标记时返回 null(老版本落盘的文件)
237
+ */
238
+ export function inboxOrigin(file) {
239
+ try {
240
+ const mark = path.join(path.dirname(String(file)), ORIGIN_MARK);
241
+ const text = fs.readFileSync(mark, 'utf8').trim();
242
+ return text === 'network' ? 'network' : 'upload';
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * 删掉 inbox 里的一份落盘副本(连它所在的时间戳目录一起删)。
250
+ *
251
+ * 只删 inbox 内部的目录:传进来的路径必须在 `<home>/plugin-pack/inbox` 之下,
252
+ * 否则直接拒绝 —— 免得哪天参数传错把用户自己的文件删了。
253
+ *
254
+ * @param {string} home $DSH_HOME
255
+ * @param {string} file inbox 里的文件路径
256
+ * @returns {boolean} 是否真的删了
257
+ */
258
+ export function removeInboxEntry(home, file) {
259
+ try {
260
+ const root = path.resolve(home, 'plugin-pack', 'inbox');
261
+ const target = path.resolve(String(file));
262
+ // 必须严格在 inbox 之下,且不能就是 inbox 本身
263
+ if (target === root || !target.startsWith(root + path.sep)) return false;
264
+ const dir = path.dirname(target);
265
+ // 目录必须是 inbox 的直接子目录(时间戳目录),不能是更外层
266
+ if (path.dirname(dir) !== root) return false;
267
+ // 目录本身要存在,否则不算"删掉了"
268
+ if (!fs.existsSync(dir)) return false;
269
+ removeTree(dir);
270
+ return true;
271
+ } catch {
272
+ return false;
273
+ }
274
+ }
package/lib/server.js CHANGED
@@ -28,7 +28,15 @@ import { importBundle, inspectBundle, rollbackImport, listBackups } from './impo
28
28
  import { reloadClientPackage, clientStatus, localVerdict } from '../hotload.js';
29
29
  import { importLocalPlugin } from './import-plugin.js';
30
30
  import { importTarball, isTarball } from './import-tgz.js';
31
- import { parseMultipart, boundaryOf, saveIncomingZip, saveIncomingDir, pruneInbox } from './inbox.js';
31
+ import {
32
+ parseMultipart,
33
+ boundaryOf,
34
+ saveIncomingZip,
35
+ saveIncomingDir,
36
+ pruneInbox,
37
+ inboxOrigin,
38
+ removeInboxEntry,
39
+ } from './inbox.js';
32
40
  import { runJob, listJobs, getJob, runningJobs, clearFinished, openWaiting, getWaiting, listWaiting, cancelWaiting } from './jobs.js';
33
41
  import { fetchBundleFrom, parseSource } from './fetch-bundle.js';
34
42
  import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, humanBytes } from './home.js';
@@ -80,6 +88,28 @@ function sendJson(response, status, value) {
80
88
  }
81
89
 
82
90
  /** 把异常翻译成 HTTP 状态;其余仍是 500。 */
91
+ /**
92
+ * 安装成功后清理 inbox 里的落盘副本。
93
+ *
94
+ * 只删「从网络下载」的那份;用户自己拖/选的本地文件永远保留 ——
95
+ * 那是他自己的文件,工具不该动它。
96
+ *
97
+ * @param {object} config
98
+ * @param {string} file 本次导入用的文件路径
99
+ * @returns {boolean} 是否真的删掉了
100
+ */
101
+ function autoCleanInbox(config, file) {
102
+ try {
103
+ if (!file) return false;
104
+ const home = contextOf(config).home;
105
+ // 只有网络下载的来源才删
106
+ if (inboxOrigin(file) !== 'network') return false;
107
+ return removeInboxEntry(home, file);
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
83
113
  function statusOf(error) {
84
114
  const explicit = Number(error?.status ?? error?.statusCode);
85
115
  if (Number.isFinite(explicit) && explicit >= 400 && explicit < 600) return explicit;
@@ -423,7 +453,13 @@ export function mountRoutes(ctx, config = {}) {
423
453
  try {
424
454
  return await spec.handler(request, response);
425
455
  } catch (error) {
426
- sendJson(response, statusOf(error), { ok: false, error: String(error?.message ?? error), code: error?.code });
456
+ sendJson(response, statusOf(error), {
457
+ ok: false,
458
+ error: String(error?.message ?? error),
459
+ code: error?.code,
460
+ // 网络类错误带处置建议,界面会显示在错误下面
461
+ ...(error?.hint ? { hint: String(error.hint) } : {}),
462
+ });
427
463
  }
428
464
  },
429
465
  },
@@ -584,7 +620,9 @@ export function mountRoutes(ctx, config = {}) {
584
620
  meta: { file: body.file },
585
621
  task: (report) => doImport(report),
586
622
  });
587
- sendJson(response, 200, { ...result, jobId: id });
623
+ // 网络下载来的 tgz,装完同样清掉副本
624
+ const cleanedTgz = autoCleanInbox(config, body.file);
625
+ sendJson(response, 200, { ...result, jobId: id, ...(cleanedTgz ? { inboxCleaned: true } : {}) });
588
626
  return;
589
627
  }
590
628
  const dryRun = Boolean(body.dryRun);
@@ -635,7 +673,11 @@ export function mountRoutes(ctx, config = {}) {
635
673
  jobId: body.jobId ? String(body.jobId) : undefined,
636
674
  task: (report) => doImportBundle(report),
637
675
  });
638
- sendJson(response, 200, { ...result, jobId: id });
676
+ /* 装完清掉网络下载的副本:
677
+ * 从 GitHub/npm 下下来的包,装好后原件就没用了,留着白占磁盘。
678
+ * 但用户自己拖/选的本地文件(origin='upload')一律保留 —— 那是他的东西。 */
679
+ const cleaned = autoCleanInbox(config, body.file);
680
+ sendJson(response, 200, { ...result, jobId: id, ...(cleaned ? { inboxCleaned: true } : {}) });
639
681
  },
640
682
  },
641
683
  'plugin-pack: import',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",