dsh-plugin-pack 1.0.13 → 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 +179 -70
  2. package/lib/import.js +107 -22
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -187,6 +187,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
187
187
  'jobs.failed': '失败',
188
188
  'jobs.clear': '清空已完成',
189
189
  'conflict.title': '这些插件已经装过了',
190
+ 'conflict.titleNew': '确认导入',
191
+ 'conflict.subtitleAll': '个插件待安装',
192
+ 'conflict.create': '新建',
190
193
  'conflict.subtitle': '个插件与已安装的重名,请选择如何处理:',
191
194
  'conflict.overwrite': '覆盖',
192
195
  'conflict.skip': '跳过',
@@ -201,6 +204,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
201
204
  'backup.rollback': '回滚',
202
205
  'backup.rolling': '正在回滚…',
203
206
  'backup.done': '回滚完成',
207
+ 'backup.surgicalHint': '只撤销了这次导入加的东西,你之后改的其它配置都保留着。',
208
+ 'backup.fullHint': '这次的备份是老格式,只能整份还原 —— 导入之后改的配置可能被覆盖。',
209
+ 'backup.preSnapshot': '回滚前的配置已另存(想反悔可从这儿取回):',
204
210
  'backup.confirm': '确定要回滚到这份备份吗?本次导入新增的插件会被删掉。',
205
211
  'backup.createdAt': '时间',
206
212
  'backup.items': '项',
@@ -380,6 +386,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
380
386
  'jobs.failed': 'Failed',
381
387
  'jobs.clear': 'Clear finished',
382
388
  'conflict.title': 'These plugins are already installed',
389
+ 'conflict.titleNew': 'Confirm import',
390
+ 'conflict.subtitleAll': 'plugins to install',
391
+ 'conflict.create': 'New',
383
392
  'conflict.subtitle': 'plugins already exist. Choose what to do:',
384
393
  'conflict.overwrite': 'Overwrite',
385
394
  'conflict.skip': 'Skip',
@@ -394,6 +403,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
394
403
  'backup.rollback': 'Roll back',
395
404
  'backup.rolling': 'Rolling back…',
396
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): ',
397
409
  'backup.confirm': 'Roll back to this snapshot? Plugins added by that import will be removed.',
398
410
  'backup.createdAt': 'Time',
399
411
  'backup.items': 'items',
@@ -546,8 +558,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
546
558
 
547
559
  /* 安装偏好:由界面上的「安装后立即启用」勾选同步过来。
548
560
  * 放这里是因为 doImport 在接口层、拿不到组件 state;
549
- * 用一个普通对象透传,避免把选项硬编码进每个调用点。 */
550
- const installPrefs = { enable: true };
561
+ * 用一个普通对象透传,避免把选项硬编码进每个调用点。
562
+ * 默认 false:装完先不启用,由用户自己决定(更安全,也避免"装了没用的插件"). */
563
+ const installPrefs = { enable: false };
551
564
 
552
565
  const getStatus = () => api('/plugin-pack/status');
553
566
  const doExport = (body) => api('/plugin-pack/export', { method: 'POST', body: JSON.stringify(body) });
@@ -1553,7 +1566,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1553
1566
  },
1554
1567
  h('input', {
1555
1568
  type: 'checkbox',
1556
- checked: enableOnInstall !== false,
1569
+ checked: Boolean(enableOnInstall),
1557
1570
  disabled: busy,
1558
1571
  onChange: (event) => onToggleEnable?.(event.target.checked),
1559
1572
  }),
@@ -1576,10 +1589,18 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1576
1589
  }
1577
1590
 
1578
1591
  function ConflictDialog({ pending, onConfirm, onCancel, t }) {
1592
+ /**
1593
+ * 弹窗列的是包里的**所有**插件,不只是重名的。
1594
+ * - 重名的(exists=true)→ 可选「覆盖 / 跳过」
1595
+ * - 新的(exists=false) → 标「新建」,没有二选一
1596
+ * 这样用户在真正写盘之前,能看清这次到底要装什么。
1597
+ */
1579
1598
  const conflicts = pending?.conflicts ?? [];
1599
+ const rows = pending?.rows ?? conflicts;
1600
+ const existing = rows.filter((item) => item.exists);
1580
1601
  /** 初始全部选「覆盖」——多数人是想升级到新版本。 */
1581
1602
  const [decisions, setDecisions] = useState(() =>
1582
- Object.fromEntries(conflicts.map((item) => [item.name, 'overwrite'])),
1603
+ Object.fromEntries(existing.map((item) => [item.name, 'overwrite'])),
1583
1604
  );
1584
1605
 
1585
1606
  // 弹窗打开时挂 Esc
@@ -1593,9 +1614,10 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1593
1614
 
1594
1615
  if (!pending) return null;
1595
1616
 
1596
- const setAll = (value) => setDecisions(Object.fromEntries(conflicts.map((item) => [item.name, value])));
1617
+ const setAll = (value) => setDecisions(Object.fromEntries(existing.map((item) => [item.name, value])));
1597
1618
  const overwriteCount = Object.values(decisions).filter((v) => v === 'overwrite').length;
1598
1619
  const skipCount = Object.values(decisions).filter((v) => v === 'skip').length;
1620
+ const createCount = rows.length - existing.length;
1599
1621
 
1600
1622
  return h(
1601
1623
  'div',
@@ -1632,30 +1654,38 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1632
1654
  h(
1633
1655
  'div',
1634
1656
  { style: { padding: '12px 16px', borderBottom: '.5px solid var(--dsw-alias-border-l1,#eee)' } },
1635
- h('b', { style: { fontSize: 14, fontWeight: 600, display: 'block', marginBottom: 4 } }, t('conflict.title')),
1636
1657
  h(
1637
- 'span',
1638
- { className: 'pp-hint' },
1639
- `${conflicts.length} ${t('conflict.subtitle')}`,
1658
+ 'b',
1659
+ { style: { fontSize: 14, fontWeight: 600, display: 'block', marginBottom: 4 } },
1660
+ existing.length ? t('conflict.title') : t('conflict.titleNew'),
1640
1661
  ),
1641
- ),
1642
- // 一键按钮
1643
- h(
1644
- 'div',
1645
- { className: 'pp-bar', style: { padding: '10px 16px 0', marginBottom: 0 } },
1646
- h('button', { className: 'pp-btn sm', onClick: () => setAll('overwrite') }, t('conflict.allOverwrite')),
1647
- h('button', { className: 'pp-btn sm', onClick: () => setAll('skip') }, t('conflict.allSkip')),
1648
1662
  h(
1649
1663
  'span',
1650
- { className: 'pp-hint', style: { marginLeft: 'auto' } },
1651
- `${t('conflict.overwrite')} ${overwriteCount} · ${t('conflict.skip')} ${skipCount}`,
1664
+ { className: 'pp-hint' },
1665
+ existing.length
1666
+ ? `${rows.length} ${t('conflict.subtitleAll')} · ${existing.length} ${t('conflict.subtitle')}`
1667
+ : `${rows.length} ${t('conflict.subtitleAll')}`,
1652
1668
  ),
1653
1669
  ),
1654
- // 列表
1670
+ // 一键按钮(只有存在重名时才有意义)
1671
+ existing.length
1672
+ ? h(
1673
+ 'div',
1674
+ { className: 'pp-bar', style: { padding: '10px 16px 0', marginBottom: 0 } },
1675
+ h('button', { className: 'pp-btn sm', onClick: () => setAll('overwrite') }, t('conflict.allOverwrite')),
1676
+ h('button', { className: 'pp-btn sm', onClick: () => setAll('skip') }, t('conflict.allSkip')),
1677
+ h(
1678
+ 'span',
1679
+ { className: 'pp-hint', style: { marginLeft: 'auto' } },
1680
+ `${t('conflict.overwrite')} ${overwriteCount} · ${t('conflict.skip')} ${skipCount}`,
1681
+ ),
1682
+ )
1683
+ : null,
1684
+ // 列表:全部插件
1655
1685
  h(
1656
1686
  'div',
1657
1687
  { style: { flex: 1, minHeight: 120, overflow: 'auto', padding: '8px 16px' } },
1658
- conflicts.map((item) =>
1688
+ rows.map((item) =>
1659
1689
  h(
1660
1690
  'div',
1661
1691
  {
@@ -1670,34 +1700,49 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1670
1700
  'div',
1671
1701
  { className: 'pp-name' },
1672
1702
  item.name,
1673
- item.installedVersion
1703
+ item.exists && item.installedVersion
1674
1704
  ? h('span', { className: 'pp-ver' }, `${item.installedVersion} → ${item.version}`)
1675
1705
  : h('span', { className: 'pp-ver' }, item.version),
1676
- item.sameVersion ? h('span', { className: 'pp-ver' }, t('conflict.sameVersion')) : null,
1677
- ),
1678
- h('div', { className: 'pp-meta pp-mono', style: { fontSize: 11 } }, item.dest),
1679
- ),
1680
- // 每个插件单独选
1681
- h(
1682
- 'div',
1683
- { style: { display: 'flex', gap: 4, flex: 'none' } },
1684
- h(
1685
- 'button',
1686
- {
1687
- className: `pp-btn sm${decisions[item.name] === 'overwrite' ? ' primary' : ''}`,
1688
- onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'overwrite' })),
1689
- },
1690
- t('conflict.overwrite'),
1691
- ),
1692
- h(
1693
- 'button',
1694
- {
1695
- className: `pp-btn sm${decisions[item.name] === 'skip' ? ' primary' : ''}`,
1696
- onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'skip' })),
1697
- },
1698
- t('conflict.skip'),
1706
+ item.exists && item.sameVersion
1707
+ ? h('span', { className: 'pp-ver' }, t('conflict.sameVersion'))
1708
+ : null,
1699
1709
  ),
1710
+ item.dest ? h('div', { className: 'pp-meta pp-mono', style: { fontSize: 11 } }, item.dest) : null,
1700
1711
  ),
1712
+ // 重名的给二选一;新的只标「新建」
1713
+ item.exists
1714
+ ? h(
1715
+ 'div',
1716
+ { style: { display: 'flex', gap: 4, flex: 'none' } },
1717
+ h(
1718
+ 'button',
1719
+ {
1720
+ className: `pp-btn sm${decisions[item.name] === 'overwrite' ? ' primary' : ''}`,
1721
+ onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'overwrite' })),
1722
+ },
1723
+ t('conflict.overwrite'),
1724
+ ),
1725
+ h(
1726
+ 'button',
1727
+ {
1728
+ className: `pp-btn sm${decisions[item.name] === 'skip' ? ' primary' : ''}`,
1729
+ onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'skip' })),
1730
+ },
1731
+ t('conflict.skip'),
1732
+ ),
1733
+ )
1734
+ : h(
1735
+ 'span',
1736
+ {
1737
+ className: 'pp-pill',
1738
+ style: {
1739
+ flex: 'none',
1740
+ background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 14%,transparent)',
1741
+ color: 'var(--dsw-alias-state-success-primary,#15803d)',
1742
+ },
1743
+ },
1744
+ t('conflict.create'),
1745
+ ),
1701
1746
  ),
1702
1747
  ),
1703
1748
  ),
@@ -1710,9 +1755,16 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1710
1755
  borderTop: '.5px solid var(--dsw-alias-border-l1,#eee)',
1711
1756
  display: 'flex',
1712
1757
  gap: 8,
1713
- justifyContent: 'flex-end',
1758
+ alignItems: 'center',
1714
1759
  },
1715
1760
  },
1761
+ h(
1762
+ 'span',
1763
+ { className: 'pp-hint', style: { flex: 1 } },
1764
+ existing.length
1765
+ ? `${t('conflict.overwrite')} ${overwriteCount} · ${t('conflict.skip')} ${skipCount} · ${t('conflict.create')} ${createCount}`
1766
+ : `${t('conflict.create')} ${createCount}`,
1767
+ ),
1716
1768
  h('button', { className: 'pp-btn', onClick: onCancel }, t('conflict.cancel')),
1717
1769
  h(
1718
1770
  'button',
@@ -1901,8 +1953,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1901
1953
  const [netResult, setNetResult] = useState(null);
1902
1954
  /** 「添加整合包」弹窗是否打开。 */
1903
1955
  const [netOpen, setNetOpen] = useState(false);
1904
- /** 安装后是否立即启用(勾选框)。关掉 → 只装文件,不进 bundles。 */
1905
- const [enableOnInstall, setEnableOnInstall] = useState(true);
1956
+ /** 安装后是否立即启用(勾选框)。默认关:先装上,用户想用再启用。 */
1957
+ const [enableOnInstall, setEnableOnInstall] = useState(false);
1906
1958
  /** 刚装完的插件及其启用状态,用于装完后再切换。 */
1907
1959
  const [enablePrompt, setEnablePrompt] = useState(null);
1908
1960
  /** 后台任务列表:切走画面回来靠它恢复 */
@@ -2151,21 +2203,35 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2151
2203
  return;
2152
2204
  }
2153
2205
 
2154
- if (conflicts.length) {
2155
- /* 先把冲突列表给出来,用户选完才上传 + 真导入。
2156
- * 这时还没上传,所以待决策任务在服务端也不存在 —— 全靠确认时再登记。 */
2157
- setPendingImport({
2158
- kind: 'bundle',
2159
- pendingFile: file, // 还没上传,先拿着 File 对象
2160
- file: null,
2161
- conflicts,
2162
- plugins: packPlugins,
2163
- quick: true,
2164
- });
2165
- setBusy(false);
2166
- setPhase('');
2167
- return;
2168
- }
2206
+ /* 不管有没有重名,都在写盘之前弹一次确认框,列出包里所有插件:
2207
+ * - 重名的 → 覆盖 / 跳过
2208
+ * - 新的 → 标「新建」
2209
+ * 这时文件还没上传,取消就什么都不发生。 */
2210
+ const rows = packPlugins.map((plugin) => {
2211
+ const hit = installed.get(plugin.name);
2212
+ return {
2213
+ name: plugin.name,
2214
+ version: plugin.version ?? null,
2215
+ installedVersion: hit?.version ?? null,
2216
+ dest: hit?.dir ?? null,
2217
+ bytes: plugin.bytes ?? null,
2218
+ sameVersion: (hit?.version ?? null) === (plugin.version ?? null),
2219
+ exists: Boolean(hit),
2220
+ };
2221
+ });
2222
+
2223
+ setPendingImport({
2224
+ kind: 'bundle',
2225
+ pendingFile: file, // 还没上传,先拿着 File 对象
2226
+ file: null,
2227
+ conflicts: rows.filter((row) => row.exists),
2228
+ rows,
2229
+ plugins: packPlugins,
2230
+ quick: true,
2231
+ });
2232
+ setBusy(false);
2233
+ setPhase('');
2234
+ return;
2169
2235
  }
2170
2236
 
2171
2237
  // 第二步:确认无冲突(或拿不到快速清单)才上传
@@ -2173,18 +2239,32 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2173
2239
  const uploaded = await uploadZip(file);
2174
2240
  setZipPath(uploaded.file);
2175
2241
 
2176
- // 先 dryRun 探一次:有重名冲突就弹窗让用户决定
2242
+ // 先 dryRun 探一次:拿到完整插件清单,写盘前弹确认
2177
2243
  setPhase(t('import.checking'));
2178
2244
  const preview = await doImport({ file: uploaded.file, dryRun: true });
2179
2245
  const conflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
2180
- if (conflicts.length) {
2181
- // 服务端在 dryRun 时已登记了"待决策"任务,带 jobId 回来
2246
+ const conflictByName = new Map(conflicts.map((item) => [item.name, item]));
2247
+ const previewRows = (preview?.plugins ?? []).map((plugin) => {
2248
+ const hit = conflictByName.get(plugin.name);
2249
+ return {
2250
+ name: plugin.name,
2251
+ version: plugin.version ?? null,
2252
+ installedVersion: hit?.installedVersion ?? null,
2253
+ dest: hit?.dest ?? null,
2254
+ sameVersion: Boolean(hit?.sameVersion),
2255
+ exists: Boolean(hit),
2256
+ };
2257
+ });
2258
+
2259
+ if (previewRows.length) {
2260
+ // 服务端在 dryRun 时已登记了"待决策"任务(仅当有重名),带 jobId 回来
2182
2261
  pendingJobId.current = preview?.jobId ?? null;
2183
2262
  setPendingImport({
2184
2263
  kind: 'bundle',
2185
2264
  jobId: preview?.jobId ?? null,
2186
2265
  file: uploaded.file,
2187
2266
  conflicts,
2267
+ rows: previewRows,
2188
2268
  plugins: preview.plugins ?? [],
2189
2269
  });
2190
2270
  setBusy(false);
@@ -2279,19 +2359,32 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2279
2359
  return;
2280
2360
  }
2281
2361
 
2282
- // 下载好的包按普通导入走一遍:先 dryRun 看有没有重名冲突
2362
+ // 下载好的包:先 dryRun 拿完整清单,写盘前弹确认
2283
2363
  const preview = await doImport({ file: fetched.file, dryRun: true });
2284
- const conflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
2285
- if (conflicts.length) {
2364
+ const netConflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
2365
+ const netConflictByName = new Map(netConflicts.map((item) => [item.name, item]));
2366
+ const netRows = (preview?.plugins ?? []).map((plugin) => {
2367
+ const hit = netConflictByName.get(plugin.name);
2368
+ return {
2369
+ name: plugin.name,
2370
+ version: plugin.version ?? null,
2371
+ installedVersion: hit?.installedVersion ?? null,
2372
+ dest: hit?.dest ?? null,
2373
+ sameVersion: Boolean(hit?.sameVersion),
2374
+ exists: Boolean(hit),
2375
+ };
2376
+ });
2377
+ if (netRows.length) {
2286
2378
  pendingJobId.current = preview?.jobId ?? null;
2287
2379
  setPendingImport({
2288
2380
  kind: 'bundle',
2289
2381
  jobId: preview?.jobId ?? null,
2290
2382
  file: fetched.file,
2291
- conflicts,
2383
+ conflicts: netConflicts,
2384
+ rows: netRows,
2292
2385
  plugins: preview.plugins ?? [],
2293
2386
  });
2294
- // 交给冲突弹窗接管,先关掉来源弹窗
2387
+ // 交给确认弹窗接管,先关掉来源弹窗
2295
2388
  setNetOpen(false);
2296
2389
  return;
2297
2390
  }
@@ -2895,9 +2988,25 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2895
2988
  'div',
2896
2989
  null,
2897
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
+ ),
2898
2998
  rollbackResult.restored?.length
2899
2999
  ? h('div', { className: 'pp-note' }, `恢复: ${rollbackResult.restored.join(', ')}`)
2900
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,
2901
3010
  )
2902
3011
  : null,
2903
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.13",
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",