dsh-plugin-pack 1.0.9 → 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.
@@ -417,8 +417,9 @@ export async function fetchBundleFrom({ source, home, onProgress = () => {} }) {
417
417
  report(`下载完成:${(data.length / 1048576).toFixed(1)} MiB`);
418
418
 
419
419
  // 落到 inbox(保留扩展名,后续按 .zip/.tgz 分流)
420
+ // origin='network':安装成功后可以自动删掉这份副本,不占用户磁盘
420
421
  pruneSafe(home);
421
- const saved = saveIncomingZip({ home, filename: name, data });
422
+ const saved = saveIncomingZip({ home, filename: name, data, origin: 'network' });
422
423
  report(`已保存到 ${saved.file}`);
423
424
 
424
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;
@@ -590,7 +620,9 @@ export function mountRoutes(ctx, config = {}) {
590
620
  meta: { file: body.file },
591
621
  task: (report) => doImport(report),
592
622
  });
593
- 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 } : {}) });
594
626
  return;
595
627
  }
596
628
  const dryRun = Boolean(body.dryRun);
@@ -641,7 +673,11 @@ export function mountRoutes(ctx, config = {}) {
641
673
  jobId: body.jobId ? String(body.jobId) : undefined,
642
674
  task: (report) => doImportBundle(report),
643
675
  });
644
- 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 } : {}) });
645
681
  },
646
682
  },
647
683
  'plugin-pack: import',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.9",
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",