dsh-plugin-pack 1.0.14 → 1.0.15

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.
Files changed (3) hide show
  1. package/client.js +22 -0
  2. package/lib/import.js +107 -22
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -204,6 +204,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
204
204
  'backup.rollback': '回滚',
205
205
  'backup.rolling': '正在回滚…',
206
206
  'backup.done': '回滚完成',
207
+ 'backup.surgicalHint': '只撤销了这次导入加的东西,你之后改的其它配置都保留着。',
208
+ 'backup.fullHint': '这次的备份是老格式,只能整份还原 —— 导入之后改的配置可能被覆盖。',
209
+ 'backup.preSnapshot': '回滚前的配置已另存(想反悔可从这儿取回):',
207
210
  'backup.confirm': '确定要回滚到这份备份吗?本次导入新增的插件会被删掉。',
208
211
  'backup.createdAt': '时间',
209
212
  'backup.items': '项',
@@ -400,6 +403,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
400
403
  'backup.rollback': 'Roll back',
401
404
  'backup.rolling': 'Rolling back…',
402
405
  'backup.done': 'Rollback finished',
406
+ 'backup.surgicalHint': 'Only what this import added was reverted; your later config changes are untouched.',
407
+ 'backup.fullHint': 'This backup is an old format and can only be restored wholesale — config you changed after the import may be overwritten.',
408
+ 'backup.preSnapshot': 'Your config before rollback was saved here (recover from it if needed): ',
403
409
  'backup.confirm': 'Roll back to this snapshot? Plugins added by that import will be removed.',
404
410
  'backup.createdAt': 'Time',
405
411
  'backup.items': 'items',
@@ -2982,9 +2988,25 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2982
2988
  'div',
2983
2989
  null,
2984
2990
  h(Alert, { kind: 'ok' }, `${t('backup.done')} · ${t('import.restart')}`),
2991
+ h(
2992
+ 'div',
2993
+ { className: 'pp-hint', style: { marginTop: 6 } },
2994
+ rollbackResult.mode === 'surgical'
2995
+ ? t('backup.surgicalHint')
2996
+ : t('backup.fullHint'),
2997
+ ),
2985
2998
  rollbackResult.restored?.length
2986
2999
  ? h('div', { className: 'pp-note' }, `恢复: ${rollbackResult.restored.join(', ')}`)
2987
3000
  : null,
3001
+ // 回滚前把当时配置另存了一份,路径给出来方便反悔
3002
+ rollbackResult.preRollbackDir
3003
+ ? h(
3004
+ 'div',
3005
+ { className: 'pp-hint', style: { marginTop: 6 } },
3006
+ t('backup.preSnapshot'),
3007
+ h('code', { className: 'pp-mono', style: { fontSize: 11, wordBreak: 'break-all' } }, rollbackResult.preRollbackDir),
3008
+ )
3009
+ : null,
2988
3010
  )
2989
3011
  : null,
2990
3012
  ),
package/lib/import.js CHANGED
@@ -447,6 +447,11 @@ export function importBundle(options = {}) {
447
447
  pkg.dsh = pkg.dsh && typeof pkg.dsh === 'object' ? pkg.dsh : {};
448
448
  pkg.dsh.profile = pkg.dsh.profile && typeof pkg.dsh.profile === 'object' ? pkg.dsh.profile : {};
449
449
  pkg.dsh.profile.bundles = Array.isArray(pkg.dsh.profile.bundles) ? pkg.dsh.profile.bundles : [];
450
+ /* 改写前先记下"每个被碰过的字段原来是什么"。
451
+ * 回滚时只把这几个字段还原,不动用户后来改的其它配置 ——
452
+ * 早先回滚是整份覆盖,会把导入之后设的 API 密钥之类一起抹掉。 */
453
+ const depsBefore = { ...pkg.dependencies };
454
+ const bundlesBefore = [...pkg.dsh.profile.bundles];
450
455
  /* enable=false:只把文件装上、依赖写好,但不进 dsh.profile.bundles
451
456
  * —— 也就是"装好了但先不启用"。用户之后可以在界面上再打开。
452
457
  * files 与依赖照常写,所以不启用不等于没装。 */
@@ -460,13 +465,30 @@ export function importBundle(options = {}) {
460
465
  }
461
466
  writeJson(pkgFile, pkg);
462
467
 
468
+ // 记录:哪些依赖被改(含改前值)、哪些 bundle 是新加的
469
+ record.pkgDepsBefore = {};
470
+ for (const item of plan) {
471
+ if (item.action === 'skip' || item.pathPlugin) continue;
472
+ const had = Object.prototype.hasOwnProperty.call(depsBefore, item.name);
473
+ record.pkgDepsBefore[item.name] = {
474
+ before: had ? depsBefore[item.name] : null,
475
+ after: `link:./local-plugins/${item.segment}`,
476
+ };
477
+ }
478
+ record.pkgBundlesAdded = pkg.dsh.profile.bundles.filter((name) => !bundlesBefore.includes(name));
479
+
463
480
  // cordis.patch.yml
464
481
  let nextPatch = patchText;
465
482
  if (patchPlan.topRows.length) nextPatch = appendTopRows(nextPatch, patchPlan.topRows, `plugin-pack ${stampText}`);
466
483
  if (patchPlan.insertRows.length) {
467
484
  nextPatch = appendInsertBlock(nextPatch, dedentRows(patchPlan.insertRows), `plugin-pack ${stampText}`).text;
468
485
  }
469
- if (nextPatch !== patchText) writeText(patchFile, nextPatch);
486
+ if (nextPatch !== patchText) {
487
+ writeText(patchFile, nextPatch);
488
+ /* 记下这次"追加的那一段原文"。回滚时只把这几个字串摘掉,
489
+ * 保留用户后来在同一个文件里做的其它修改。 */
490
+ record.patchAppended = nextPatch.startsWith(patchText) ? nextPatch.slice(patchText.length) : null;
491
+ }
470
492
 
471
493
  // node_modules 链接
472
494
  onProgress('建立 node_modules 链接', 92);
@@ -701,15 +723,77 @@ export function rollbackImport(options = {}) {
701
723
  const removed = [];
702
724
  const relinked = [];
703
725
 
704
- const backupPkg = path.join(backupRoot, 'package.json');
705
- const backupPatch = path.join(backupRoot, 'cordis.patch.yml');
706
- if (isFile(backupPkg)) {
707
- fs.copyFileSync(backupPkg, path.join(profile, 'package.json'));
708
- restored.push('package.json');
726
+ const pkgFile = path.join(profile, 'package.json');
727
+ const patchFile = path.join(profile, 'cordis.patch.yml');
728
+
729
+ /* 第一步:先把**当前**的配置文件另存一份。
730
+ *
731
+ * 回滚会改 package.json / cordis.patch.yml,早先直接整份覆盖,
732
+ * 用户在这之后设的 API 密钥、改过的 provider 全没了且找不回来。
733
+ * 现在无论走精准回滚还是整份还原,都先留一份可反悔的副本。 */
734
+ const preRollbackDir = path.join(backupRoot, `pre-rollback-${stamp()}`);
735
+ let preRollbackFiles = [];
736
+ try {
737
+ ensureDir(preRollbackDir);
738
+ for (const [src, name] of [
739
+ [pkgFile, 'package.json'],
740
+ [patchFile, 'cordis.patch.yml'],
741
+ ]) {
742
+ if (isFile(src)) {
743
+ fs.copyFileSync(src, path.join(preRollbackDir, name));
744
+ preRollbackFiles.push(name);
745
+ }
746
+ }
747
+ } catch {
748
+ /* 存不了也继续,不阻断回滚 */
749
+ }
750
+
751
+ /* 第二步:优先精准回滚 —— 只撤销这次导入加的东西,保留用户后续改动。
752
+ * 老备份没有这些字段,才退回整份还原(那种情况确实是快照替换)。 */
753
+ let mode = 'full';
754
+
755
+ if (record.pkgDepsBefore || Array.isArray(record.pkgBundlesAdded)) {
756
+ mode = 'surgical';
757
+ const pkg = readJson(pkgFile, null);
758
+ if (pkg && typeof pkg === 'object') {
759
+ pkg.dependencies = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {};
760
+ // 依赖:只在"当前值还是我们写进去的那个"时才还原,避免覆盖用户后来的改动
761
+ for (const [name, info] of Object.entries(record.pkgDepsBefore ?? {})) {
762
+ if (pkg.dependencies[name] !== info.after) continue;
763
+ if (info.before === null) delete pkg.dependencies[name];
764
+ else pkg.dependencies[name] = info.before;
765
+ }
766
+ // bundles:只移除这次加的,且现在还在的
767
+ const addedSet = new Set(record.pkgBundlesAdded ?? []);
768
+ if (Array.isArray(pkg.dsh?.profile?.bundles)) {
769
+ pkg.dsh.profile.bundles = pkg.dsh.profile.bundles.filter((name) => !addedSet.has(name));
770
+ }
771
+ writeJson(pkgFile, pkg);
772
+ restored.push('package.json(仅撤销本次导入的依赖与 bundles)');
773
+ }
774
+ }
775
+
776
+ if (typeof record.patchAppended === 'string') {
777
+ mode = mode === 'surgical' ? 'surgical' : 'surgical';
778
+ const currentPatch = readText(patchFile, null);
779
+ if (typeof currentPatch === 'string' && record.patchAppended && currentPatch.includes(record.patchAppended)) {
780
+ // 只摘掉当初追加的那一段,其它内容(含用户后加的)原样保留
781
+ const nextText = currentPatch.replace(record.patchAppended, '');
782
+ writeText(patchFile, nextText);
783
+ restored.push('cordis.patch.yml(仅移除本次导入追加的行)');
784
+ }
785
+ }
786
+
787
+ // 精准回滚没覆盖到的老备份,才退回整份还原
788
+ const pkgHandled = restored.some((item) => item.startsWith('package.json'));
789
+ const patchHandled = restored.some((item) => item.startsWith('cordis.patch.yml'));
790
+ if (!pkgHandled && isFile(path.join(backupRoot, 'package.json'))) {
791
+ fs.copyFileSync(path.join(backupRoot, 'package.json'), pkgFile);
792
+ restored.push('package.json(整份还原:老备份没有精准记录)');
709
793
  }
710
- if (isFile(backupPatch)) {
711
- fs.copyFileSync(backupPatch, path.join(profile, 'cordis.patch.yml'));
712
- restored.push('cordis.patch.yml');
794
+ if (!patchHandled && isFile(path.join(backupRoot, 'cordis.patch.yml'))) {
795
+ fs.copyFileSync(path.join(backupRoot, 'cordis.patch.yml'), patchFile);
796
+ restored.push('cordis.patch.yml(整份还原:老备份没有精准记录)');
713
797
  }
714
798
 
715
799
  for (const dest of record.addedDirs ?? []) {
@@ -748,32 +832,33 @@ export function rollbackImport(options = {}) {
748
832
  removed.push(file);
749
833
  }
750
834
 
751
- // 标记该备份已消费:重命名加 .done,之后「最新备份」指向更早的、还没回滚过的那次。
835
+ // 回滚改了配置和插件目录,扫描缓存必须失效
836
+ // (早先这行写在 return 之后,成功路径根本执行不到)
837
+ clearScanCache();
838
+
839
+ /* 标记该备份已消费:重命名加 .done,之后「最新备份」指向更早的、还没回滚过的那次。
840
+ * 注意:pre-rollback 快照在备份目录里,重命名后它跟着一起搬到 .done 里,
841
+ * 仍然可以取出来反悔。 */
842
+ let backupArchived = null;
752
843
  try {
753
844
  const done = `${backupRoot}.done`;
754
845
  fs.renameSync(backupRoot, done);
755
- return {
756
- ok: true,
757
- backup: backupRoot,
758
- backupArchived: done,
759
- restored,
760
- removed,
761
- unlinked: relinked,
762
- restartRequired: true,
763
- };
846
+ backupArchived = done;
764
847
  } catch {
765
848
  /* 重命名失败不影响回滚结果 */
766
849
  }
767
850
 
768
- // 回滚改了配置和插件目录,扫描缓存失效
769
- clearScanCache();
770
-
771
851
  return {
772
852
  ok: true,
773
853
  backup: backupRoot,
854
+ backupArchived,
855
+ mode,
774
856
  restored,
775
857
  removed,
776
858
  unlinked: relinked,
859
+ // 回滚前把当时的配置另存了一份,路径给出来方便反悔
860
+ preRollbackDir: preRollbackFiles.length ? preRollbackDir : null,
861
+ preRollbackFiles,
777
862
  restartRequired: true,
778
863
  };
779
864
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",