dsh-plugin-pack 1.0.20 → 1.0.21

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
@@ -580,7 +580,12 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
580
580
  api('/plugin-pack/browse', { method: 'POST', body: JSON.stringify({ dir, create: Boolean(options.create) }) });
581
581
  const doHotload = (pkg) => api('/plugin-pack/hotload', { method: 'POST', body: JSON.stringify({ package: pkg }) });
582
582
  const getClientStatus = (pkg) => api(`/plugin-pack/client?package=${encodeURIComponent(pkg ?? 'dsh-plugin-pack')}`);
583
- const doImportPlugin = (dir) => api('/plugin-pack/import-plugin', { method: 'POST', body: JSON.stringify({ dir }) });
583
+ /** 导入一个本地插件目录(dryRun=true 时只预览冲突,不写盘)。 */
584
+ const doImportPlugin = (dir, dryRun = false) =>
585
+ api('/plugin-pack/import-plugin', {
586
+ method: 'POST',
587
+ body: JSON.stringify({ dir, dryRun, enable: installPrefs.enable }),
588
+ });
584
589
  /** 后台任务列表(进行中 + 已完成)。 */
585
590
  const getJobs = () => api('/plugin-pack/jobs?limit=20');
586
591
  const clearJobs = () => api('/plugin-pack/jobs/clear', { method: 'POST', body: '{}' });
@@ -2395,20 +2400,55 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2395
2400
  }
2396
2401
  setPhase(t('import.checking'));
2397
2402
 
2398
- /* 下载到的可能是两种:
2399
- * kind='plugin' → npm 包 / 单插件 tgz,走 tgz 导入
2400
- * kind='bundle' → 整合包 zip,走整合包导入(可能要先问冲突) */
2403
+ /* 下载到的可能是三种:
2404
+ * kind='plugin' + file → npm 包 / 单插件 tgz
2405
+ * kind='plugin' + dir → 单个插件的原仓库(GitHub 归档解出来的目录)
2406
+ * kind='bundle' → 整合包 zip,可能要先问冲突 */
2401
2407
  if (fetched.kind === 'plugin') {
2402
- const result = await doImport({
2403
- file: fetched.file,
2404
- dryRun: false,
2405
- mode: 'merge',
2406
- linkNodeModules: true,
2407
- });
2408
+ // 仓库来的单插件:先 dryRun 看有没有同名已装,有就弹确认框
2409
+ if (fetched.dir) {
2410
+ const preview = await doImportPlugin(fetched.dir, true);
2411
+ const existingRow = preview?.conflicts?.[0] ?? null;
2412
+ if (existingRow || preview?.plan) {
2413
+ const row = {
2414
+ name: fetched.plugins?.[0]?.name ?? preview?.name ?? '(未知插件)',
2415
+ version: fetched.plugins?.[0]?.version ?? preview?.version ?? null,
2416
+ installedVersion: existingRow?.installedVersion ?? null,
2417
+ dest: existingRow?.dest ?? null,
2418
+ sameVersion: Boolean(existingRow?.sameVersion),
2419
+ older: Boolean(existingRow?.older),
2420
+ installSource: existingRow?.installSource ?? null,
2421
+ exists: Boolean(existingRow),
2422
+ };
2423
+ setPendingImport({
2424
+ kind: 'plugin-dir',
2425
+ pendingDir: fetched.dir,
2426
+ file: null,
2427
+ conflicts: row.exists ? [row] : [],
2428
+ rows: [row],
2429
+ plugins: fetched.plugins ?? [],
2430
+ from: fetched.from,
2431
+ });
2432
+ setNetOpen(false);
2433
+ setBusy(false);
2434
+ setPhase('');
2435
+ return;
2436
+ }
2437
+ }
2438
+
2439
+ const result = fetched.dir
2440
+ ? await doImportPlugin(fetched.dir)
2441
+ : await doImport({
2442
+ file: fetched.file,
2443
+ dryRun: false,
2444
+ mode: 'merge',
2445
+ linkNodeModules: true,
2446
+ });
2408
2447
  setNetResult({
2409
2448
  ok: true,
2410
2449
  kind: 'plugin',
2411
- file: fetched.file,
2450
+ file: fetched.file ?? null,
2451
+ dir: fetched.dir ?? null,
2412
2452
  from: fetched.from,
2413
2453
  name: fetched.name,
2414
2454
  bytes: fetched.bytes,
@@ -2493,6 +2533,26 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2493
2533
  setBusy(true);
2494
2534
  setImportResult(null);
2495
2535
  try {
2536
+ /* 单插件仓库(GitHub 归档解出来的目录):直接按目录导入。
2537
+ * 目录导入没有 decisions 的概念(一个插件一个决定),
2538
+ * 用户点了「开始导入」就等于选覆盖。 */
2539
+ if (pending.kind === 'plugin-dir' && pending.pendingDir) {
2540
+ setPhase(t('import.checking'));
2541
+ const dirResult = await doImportPlugin(pending.pendingDir);
2542
+ setNetResult({
2543
+ ok: true,
2544
+ kind: 'plugin',
2545
+ dir: pending.pendingDir,
2546
+ from: pending.from ?? null,
2547
+ plugins: pending.plugins ?? [],
2548
+ });
2549
+ setImportResult(dirResult);
2550
+ rememberEnableState(dirResult);
2551
+ await load();
2552
+ await refreshJobs();
2553
+ return;
2554
+ }
2555
+
2496
2556
  /* quick 模式:拖进来时只读了 zip 尾部就弹了冲突窗,文件还没上传。
2497
2557
  * 现在用户确认了,才真正把文件传上去。 */
2498
2558
  let file = pending.file;
@@ -20,6 +20,7 @@ import path from 'node:path';
20
20
  import { inspectBundle } from './import.js';
21
21
  import { saveIncomingZip } from './inbox.js';
22
22
  import { ensureDir } from './util.js';
23
+ import { readZip } from './zip.js';
23
24
 
24
25
  /** 单次下载的上限(与上传一致,1 GiB)。 */
25
26
  const MAX_BYTES = 1024 * 1024 * 1024;
@@ -422,11 +423,14 @@ export async function fetchBundleFrom({ source, home, onProgress = () => {} }) {
422
423
  const saved = saveIncomingZip({ home, filename: name, data, origin: 'network' });
423
424
  report(`已保存到 ${saved.file}`);
424
425
 
425
- /* 校验阶段分两种:
426
- * .tgz → 这是单个插件的 npm tarball。要求 package/package.json 存在,
427
- * 不需要"整合包",因为它本来就不是。
428
- * .zip → 必须是 dsh-plugin-pack 的整合包,否则报 NOT_A_BUNDLE。
429
- * 两类都用现实存在的解析器校验,不靠扩展名猜。 */
426
+ /* 关键:校验它到底是什么。
427
+ *
428
+ * 三种可能,靠内容判断而不是靠地址猜:
429
+ * .tgz → 单个插件的 npm tarball
430
+ * .zip 带 manifest.json → dsh-plugin-pack 整合包
431
+ * .zip 带 package.json → 单个插件的原仓库(GitHub 归档常见)
432
+ * 都不是 → 报「不是整合包也不是插件」
433
+ */
430
434
  const isTgz = /\.(tgz|tar\.gz)$/i.test(saved.file);
431
435
 
432
436
  if (isTgz) {
@@ -462,23 +466,93 @@ export async function fetchBundleFrom({ source, home, onProgress = () => {} }) {
462
466
  };
463
467
  }
464
468
 
465
- /* 关键:校验它真的是整合包。
466
- * 这一步不过就报明确错误,不让用户以为"下载成功了却导不进去"。 */
467
- const info = inspectBundle(saved.file);
468
- report(`校验通过:整合包内含 ${info.plugins.length} 个插件`);
469
-
470
- return {
471
- file: saved.file,
472
- bytes: saved.bytes,
473
- name,
474
- from,
475
- kind: 'bundle',
476
- manifest: info.manifest,
477
- plugins: info.plugins.map((plugin) => ({
478
- name: plugin.name,
479
- version: plugin.version,
480
- })),
481
- };
469
+ // .zip:先按整合包认;不是整合包再按"单插件仓库"认
470
+ try {
471
+ const info = inspectBundle(saved.file);
472
+ report(`校验通过:整合包内含 ${info.plugins.length} 个插件`);
473
+ return {
474
+ file: saved.file,
475
+ bytes: saved.bytes,
476
+ name,
477
+ from,
478
+ kind: 'bundle',
479
+ manifest: info.manifest,
480
+ plugins: info.plugins.map((plugin) => ({
481
+ name: plugin.name,
482
+ version: plugin.version,
483
+ })),
484
+ };
485
+ } catch (bundleError) {
486
+ /* 不是整合包 → 看看是不是某个插件的原仓库。
487
+ * 判断依据是解出来的目录里有合法 package.json(有 name),
488
+ * 而不是"地址长得像 github"。 */
489
+ report('不是整合包,检查是否为单个插件的原仓库…');
490
+ const detected = await detectPluginInZip(data, path.dirname(saved.file));
491
+ if (detected) {
492
+ report(`校验通过:单个插件 ${detected.info.name}@${detected.info.version ?? '?'}(来自仓库)`);
493
+ return {
494
+ file: saved.file,
495
+ bytes: saved.bytes,
496
+ name,
497
+ from,
498
+ kind: 'plugin',
499
+ dir: detected.dir,
500
+ manifest: null,
501
+ plugins: [{ name: detected.info.name, version: detected.info.version ?? null }],
502
+ };
503
+ }
504
+ throw bundleError;
505
+ }
506
+ }
507
+
508
+ /**
509
+ * 在一个 zip 里找"单个插件"的根目录。
510
+ *
511
+ * GitHub 的仓库归档会多包一层 `<repo>-<ref>/`,所以要判断:
512
+ * - 根目录就有 package.json → 就是它
513
+ * - 只有一个顶层目录,且该目录里有 package.json → 用它
514
+ * 其它情况(散落多目录、没有 package.json)返回 null。
515
+ *
516
+ * @param {Buffer} buffer zip 内容
517
+ * @param {string} parentDir 解压到哪儿(会在其下建 extracted/)
518
+ * @returns {Promise<{ dir: string, info: object } | null>}
519
+ */
520
+ async function detectPluginInZip(buffer, parentDir) {
521
+ try {
522
+ const entries = readZip(buffer);
523
+ if (!entries.length) return null;
524
+
525
+ // 先看有没有 package.json,以及它的层级
526
+ const pkgEntry = entries.find((entry) => entry.name === 'package.json' || /^[^/]+\/package\.json$/.test(entry.name));
527
+ if (!pkgEntry) return null;
528
+
529
+ const dest = path.join(parentDir, 'extracted');
530
+ fs.rmSync(dest, { recursive: true, force: true });
531
+ fs.mkdirSync(dest, { recursive: true });
532
+
533
+ for (const entry of entries) {
534
+ if (entry.name.endsWith('/')) continue;
535
+ const target = path.join(dest, entry.name);
536
+ // 防路径穿越
537
+ if (!path.resolve(target).startsWith(path.resolve(dest))) continue;
538
+ fs.mkdirSync(path.dirname(target), { recursive: true });
539
+ fs.writeFileSync(target, entry.data);
540
+ }
541
+
542
+ // package.json 在根还是在唯一子目录里
543
+ const rootPkg = path.join(dest, 'package.json');
544
+ let pluginDir = dest;
545
+ if (!fs.existsSync(rootPkg)) {
546
+ pluginDir = path.join(dest, path.dirname(pkgEntry.name));
547
+ }
548
+
549
+ const { describeLocalPlugin } = await import('./import-plugin.js');
550
+ const info = describeLocalPlugin(pluginDir);
551
+ if (!info?.name) return null;
552
+ return { dir: pluginDir, info };
553
+ } catch {
554
+ return null;
555
+ }
482
556
  }
483
557
 
484
558
  /** 归档分支名里的斜杠要转义,否则 codeload 会 404。 */
package/lib/server.js CHANGED
@@ -1150,6 +1150,8 @@ export function mountRoutes(ctx, config = {}) {
1150
1150
  mode: body.mode === 'skip' ? 'skip' : 'merge',
1151
1151
  linkNodeModules: body.linkNodeModules !== false,
1152
1152
  decisions: body.decisions && typeof body.decisions === 'object' ? body.decisions : {},
1153
+ // enable=false → 只装不启用(界面上「安装后立即启用」的勾选)
1154
+ enable: body.enable !== false,
1153
1155
  onProgress: report,
1154
1156
  });
1155
1157
  if (dryRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",