dsh-plugin-pack 1.0.12 → 1.0.14

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
@@ -134,6 +134,14 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
134
134
  'net.warnBody': '整合包会覆盖已装插件的文件与配置,来源不明的包可能损坏 DeepSeek Harness。安装前会先列出重名插件,由你逐个决定覆盖还是跳过。',
135
135
  'net.done': '下载完成,已导入',
136
136
  'net.hintTitle': '可以这样处理:',
137
+ 'enable.onInstall': '安装后立即启用',
138
+ 'enable.installedTitle': '已安装',
139
+ 'enable.enabled': '已启用',
140
+ 'enable.disabled': '未启用',
141
+ 'enable.enable': '启用',
142
+ 'enable.disable': '停用',
143
+ 'enable.enabledHint': '重启 dsh 后会加载这些插件。',
144
+ 'enable.disabledHint': '文件已经装好,但不会加载。想用的时候点「启用」,重启 dsh 后生效。',
137
145
  'import.pageTitle': '导入插件整合包',
138
146
  'drop.title': '把 zip / tgz 或插件文件夹拖到这里',
139
147
  'drop.release': '松手就导入',
@@ -179,6 +187,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
179
187
  'jobs.failed': '失败',
180
188
  'jobs.clear': '清空已完成',
181
189
  'conflict.title': '这些插件已经装过了',
190
+ 'conflict.titleNew': '确认导入',
191
+ 'conflict.subtitleAll': '个插件待安装',
192
+ 'conflict.create': '新建',
182
193
  'conflict.subtitle': '个插件与已安装的重名,请选择如何处理:',
183
194
  'conflict.overwrite': '覆盖',
184
195
  'conflict.skip': '跳过',
@@ -320,6 +331,14 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
320
331
  '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.',
321
332
  'net.done': 'Downloaded and imported',
322
333
  'net.hintTitle': 'What you can do: ',
334
+ 'enable.onInstall': 'Enable right after install',
335
+ 'enable.installedTitle': 'Installed',
336
+ 'enable.enabled': 'Enabled',
337
+ 'enable.disabled': 'Not enabled',
338
+ 'enable.enable': 'Enable',
339
+ 'enable.disable': 'Disable',
340
+ 'enable.enabledHint': 'These plugins will load after you restart dsh.',
341
+ 'enable.disabledHint': 'Files are installed but will not load. Click Enable when you need them, then restart dsh.',
323
342
  'import.pageTitle': 'Import plugin bundle',
324
343
  'drop.title': 'Drop a zip / tgz or plugin folder here',
325
344
  'drop.release': 'Release to import',
@@ -364,6 +383,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
364
383
  'jobs.failed': 'Failed',
365
384
  'jobs.clear': 'Clear finished',
366
385
  'conflict.title': 'These plugins are already installed',
386
+ 'conflict.titleNew': 'Confirm import',
387
+ 'conflict.subtitleAll': 'plugins to install',
388
+ 'conflict.create': 'New',
367
389
  'conflict.subtitle': 'plugins already exist. Choose what to do:',
368
390
  'conflict.overwrite': 'Overwrite',
369
391
  'conflict.skip': 'Skip',
@@ -528,9 +550,20 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
528
550
  }
529
551
  }
530
552
 
553
+ /* 安装偏好:由界面上的「安装后立即启用」勾选同步过来。
554
+ * 放这里是因为 doImport 在接口层、拿不到组件 state;
555
+ * 用一个普通对象透传,避免把选项硬编码进每个调用点。
556
+ * 默认 false:装完先不启用,由用户自己决定(更安全,也避免"装了没用的插件"). */
557
+ const installPrefs = { enable: false };
558
+
531
559
  const getStatus = () => api('/plugin-pack/status');
532
560
  const doExport = (body) => api('/plugin-pack/export', { method: 'POST', body: JSON.stringify(body) });
533
- const doImport = (body) => api('/plugin-pack/import', { method: 'POST', body: JSON.stringify(body) });
561
+ const doImport = (body) =>
562
+ api('/plugin-pack/import', {
563
+ method: 'POST',
564
+ // 显式传 enable;body 里如果自己带了 enable 就以 body 为准
565
+ body: JSON.stringify({ enable: installPrefs.enable, ...body }),
566
+ });
534
567
  // inspect 接口保留在服务端(工具/CLI 会用),界面这层不再需要它
535
568
  const doRollback = (backup) => api('/plugin-pack/rollback', { method: 'POST', body: JSON.stringify({ backup }) });
536
569
  const doBrowse = (dir, options = {}) =>
@@ -547,6 +580,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
547
580
  api('/plugin-pack/waiting/cancel', { method: 'POST', body: JSON.stringify({ id, reason }) });
548
581
  /** 从网络来源(GitHub / npm / 直链)拉整合包。 */
549
582
  const fetchBundle = (body) => api('/plugin-pack/fetch-bundle', { method: 'POST', body: JSON.stringify(body) });
583
+ /** 启用 / 停用已装插件(装完后用户自己选的那个开关)。 */
584
+ const setEnabled = (names, enabled) =>
585
+ api('/plugin-pack/enable', { method: 'POST', body: JSON.stringify({ names, enabled }) });
550
586
 
551
587
  /* ------------------------- 浏览器端读 zip 头部 ------------------------- */
552
588
 
@@ -1343,7 +1379,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1343
1379
  * @param {() => void} props.onClose
1344
1380
  * @param {(key: string) => string} props.t
1345
1381
  */
1346
- function AddBundleDialog({ open, value, onChange, result, busy, phase, onInstall, onClose, t }) {
1382
+ function AddBundleDialog({ open, value, onChange, result, busy, phase, onInstall, onClose, t, enableOnInstall, onToggleEnable }) {
1347
1383
  const [showExamples, setShowExamples] = useState(false);
1348
1384
 
1349
1385
  // Esc 关闭
@@ -1515,6 +1551,22 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1515
1551
  )
1516
1552
  : null,
1517
1553
 
1554
+ // 安装后是否立即启用(和导入页那个勾选是同一个状态)
1555
+ h(
1556
+ 'label',
1557
+ {
1558
+ className: 'pp-hint',
1559
+ style: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 12, cursor: busy ? 'default' : 'pointer' },
1560
+ },
1561
+ h('input', {
1562
+ type: 'checkbox',
1563
+ checked: Boolean(enableOnInstall),
1564
+ disabled: busy,
1565
+ onChange: (event) => onToggleEnable?.(event.target.checked),
1566
+ }),
1567
+ t('enable.onInstall'),
1568
+ ),
1569
+
1518
1570
  // 大按钮(对齐自带的整宽「安装」)
1519
1571
  h(
1520
1572
  'button',
@@ -1531,10 +1583,18 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1531
1583
  }
1532
1584
 
1533
1585
  function ConflictDialog({ pending, onConfirm, onCancel, t }) {
1586
+ /**
1587
+ * 弹窗列的是包里的**所有**插件,不只是重名的。
1588
+ * - 重名的(exists=true)→ 可选「覆盖 / 跳过」
1589
+ * - 新的(exists=false) → 标「新建」,没有二选一
1590
+ * 这样用户在真正写盘之前,能看清这次到底要装什么。
1591
+ */
1534
1592
  const conflicts = pending?.conflicts ?? [];
1593
+ const rows = pending?.rows ?? conflicts;
1594
+ const existing = rows.filter((item) => item.exists);
1535
1595
  /** 初始全部选「覆盖」——多数人是想升级到新版本。 */
1536
1596
  const [decisions, setDecisions] = useState(() =>
1537
- Object.fromEntries(conflicts.map((item) => [item.name, 'overwrite'])),
1597
+ Object.fromEntries(existing.map((item) => [item.name, 'overwrite'])),
1538
1598
  );
1539
1599
 
1540
1600
  // 弹窗打开时挂 Esc
@@ -1548,9 +1608,10 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1548
1608
 
1549
1609
  if (!pending) return null;
1550
1610
 
1551
- const setAll = (value) => setDecisions(Object.fromEntries(conflicts.map((item) => [item.name, value])));
1611
+ const setAll = (value) => setDecisions(Object.fromEntries(existing.map((item) => [item.name, value])));
1552
1612
  const overwriteCount = Object.values(decisions).filter((v) => v === 'overwrite').length;
1553
1613
  const skipCount = Object.values(decisions).filter((v) => v === 'skip').length;
1614
+ const createCount = rows.length - existing.length;
1554
1615
 
1555
1616
  return h(
1556
1617
  'div',
@@ -1587,30 +1648,38 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1587
1648
  h(
1588
1649
  'div',
1589
1650
  { style: { padding: '12px 16px', borderBottom: '.5px solid var(--dsw-alias-border-l1,#eee)' } },
1590
- h('b', { style: { fontSize: 14, fontWeight: 600, display: 'block', marginBottom: 4 } }, t('conflict.title')),
1591
1651
  h(
1592
- 'span',
1593
- { className: 'pp-hint' },
1594
- `${conflicts.length} ${t('conflict.subtitle')}`,
1652
+ 'b',
1653
+ { style: { fontSize: 14, fontWeight: 600, display: 'block', marginBottom: 4 } },
1654
+ existing.length ? t('conflict.title') : t('conflict.titleNew'),
1595
1655
  ),
1596
- ),
1597
- // 一键按钮
1598
- h(
1599
- 'div',
1600
- { className: 'pp-bar', style: { padding: '10px 16px 0', marginBottom: 0 } },
1601
- h('button', { className: 'pp-btn sm', onClick: () => setAll('overwrite') }, t('conflict.allOverwrite')),
1602
- h('button', { className: 'pp-btn sm', onClick: () => setAll('skip') }, t('conflict.allSkip')),
1603
1656
  h(
1604
1657
  'span',
1605
- { className: 'pp-hint', style: { marginLeft: 'auto' } },
1606
- `${t('conflict.overwrite')} ${overwriteCount} · ${t('conflict.skip')} ${skipCount}`,
1658
+ { className: 'pp-hint' },
1659
+ existing.length
1660
+ ? `${rows.length} ${t('conflict.subtitleAll')} · ${existing.length} ${t('conflict.subtitle')}`
1661
+ : `${rows.length} ${t('conflict.subtitleAll')}`,
1607
1662
  ),
1608
1663
  ),
1609
- // 列表
1664
+ // 一键按钮(只有存在重名时才有意义)
1665
+ existing.length
1666
+ ? h(
1667
+ 'div',
1668
+ { className: 'pp-bar', style: { padding: '10px 16px 0', marginBottom: 0 } },
1669
+ h('button', { className: 'pp-btn sm', onClick: () => setAll('overwrite') }, t('conflict.allOverwrite')),
1670
+ h('button', { className: 'pp-btn sm', onClick: () => setAll('skip') }, t('conflict.allSkip')),
1671
+ h(
1672
+ 'span',
1673
+ { className: 'pp-hint', style: { marginLeft: 'auto' } },
1674
+ `${t('conflict.overwrite')} ${overwriteCount} · ${t('conflict.skip')} ${skipCount}`,
1675
+ ),
1676
+ )
1677
+ : null,
1678
+ // 列表:全部插件
1610
1679
  h(
1611
1680
  'div',
1612
1681
  { style: { flex: 1, minHeight: 120, overflow: 'auto', padding: '8px 16px' } },
1613
- conflicts.map((item) =>
1682
+ rows.map((item) =>
1614
1683
  h(
1615
1684
  'div',
1616
1685
  {
@@ -1625,34 +1694,49 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1625
1694
  'div',
1626
1695
  { className: 'pp-name' },
1627
1696
  item.name,
1628
- item.installedVersion
1697
+ item.exists && item.installedVersion
1629
1698
  ? h('span', { className: 'pp-ver' }, `${item.installedVersion} → ${item.version}`)
1630
1699
  : h('span', { className: 'pp-ver' }, item.version),
1631
- item.sameVersion ? h('span', { className: 'pp-ver' }, t('conflict.sameVersion')) : null,
1632
- ),
1633
- h('div', { className: 'pp-meta pp-mono', style: { fontSize: 11 } }, item.dest),
1634
- ),
1635
- // 每个插件单独选
1636
- h(
1637
- 'div',
1638
- { style: { display: 'flex', gap: 4, flex: 'none' } },
1639
- h(
1640
- 'button',
1641
- {
1642
- className: `pp-btn sm${decisions[item.name] === 'overwrite' ? ' primary' : ''}`,
1643
- onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'overwrite' })),
1644
- },
1645
- t('conflict.overwrite'),
1646
- ),
1647
- h(
1648
- 'button',
1649
- {
1650
- className: `pp-btn sm${decisions[item.name] === 'skip' ? ' primary' : ''}`,
1651
- onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'skip' })),
1652
- },
1653
- t('conflict.skip'),
1700
+ item.exists && item.sameVersion
1701
+ ? h('span', { className: 'pp-ver' }, t('conflict.sameVersion'))
1702
+ : null,
1654
1703
  ),
1704
+ item.dest ? h('div', { className: 'pp-meta pp-mono', style: { fontSize: 11 } }, item.dest) : null,
1655
1705
  ),
1706
+ // 重名的给二选一;新的只标「新建」
1707
+ item.exists
1708
+ ? h(
1709
+ 'div',
1710
+ { style: { display: 'flex', gap: 4, flex: 'none' } },
1711
+ h(
1712
+ 'button',
1713
+ {
1714
+ className: `pp-btn sm${decisions[item.name] === 'overwrite' ? ' primary' : ''}`,
1715
+ onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'overwrite' })),
1716
+ },
1717
+ t('conflict.overwrite'),
1718
+ ),
1719
+ h(
1720
+ 'button',
1721
+ {
1722
+ className: `pp-btn sm${decisions[item.name] === 'skip' ? ' primary' : ''}`,
1723
+ onClick: () => setDecisions((prev) => ({ ...prev, [item.name]: 'skip' })),
1724
+ },
1725
+ t('conflict.skip'),
1726
+ ),
1727
+ )
1728
+ : h(
1729
+ 'span',
1730
+ {
1731
+ className: 'pp-pill',
1732
+ style: {
1733
+ flex: 'none',
1734
+ background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 14%,transparent)',
1735
+ color: 'var(--dsw-alias-state-success-primary,#15803d)',
1736
+ },
1737
+ },
1738
+ t('conflict.create'),
1739
+ ),
1656
1740
  ),
1657
1741
  ),
1658
1742
  ),
@@ -1665,9 +1749,16 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1665
1749
  borderTop: '.5px solid var(--dsw-alias-border-l1,#eee)',
1666
1750
  display: 'flex',
1667
1751
  gap: 8,
1668
- justifyContent: 'flex-end',
1752
+ alignItems: 'center',
1669
1753
  },
1670
1754
  },
1755
+ h(
1756
+ 'span',
1757
+ { className: 'pp-hint', style: { flex: 1 } },
1758
+ existing.length
1759
+ ? `${t('conflict.overwrite')} ${overwriteCount} · ${t('conflict.skip')} ${skipCount} · ${t('conflict.create')} ${createCount}`
1760
+ : `${t('conflict.create')} ${createCount}`,
1761
+ ),
1671
1762
  h('button', { className: 'pp-btn', onClick: onCancel }, t('conflict.cancel')),
1672
1763
  h(
1673
1764
  'button',
@@ -1856,6 +1947,10 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1856
1947
  const [netResult, setNetResult] = useState(null);
1857
1948
  /** 「添加整合包」弹窗是否打开。 */
1858
1949
  const [netOpen, setNetOpen] = useState(false);
1950
+ /** 安装后是否立即启用(勾选框)。默认关:先装上,用户想用再启用。 */
1951
+ const [enableOnInstall, setEnableOnInstall] = useState(false);
1952
+ /** 刚装完的插件及其启用状态,用于装完后再切换。 */
1953
+ const [enablePrompt, setEnablePrompt] = useState(null);
1859
1954
  /** 后台任务列表:切走画面回来靠它恢复 */
1860
1955
  const [jobs, setJobs] = useState([]);
1861
1956
  /** 体积上限(MiB),0 = 不限制。默认跟着服务端配置走。 */
@@ -2017,6 +2112,43 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2017
2112
  }
2018
2113
  };
2019
2114
 
2115
+ /**
2116
+ * 记下这次装完的插件和它的启用状态,供界面上的「启用 / 停用」切换。
2117
+ *
2118
+ * 装完不等于启用:enable=false 时文件在磁盘上、依赖也写好了,
2119
+ * 但不在 dsh.profile.bundles 里,dsh 启动时不会加载它。
2120
+ */
2121
+ const rememberEnableState = (result) => {
2122
+ if (!result?.ok || result.dryRun) {
2123
+ setEnablePrompt(null);
2124
+ return;
2125
+ }
2126
+ const names = (result.plugins ?? [])
2127
+ .filter((plugin) => plugin && plugin.action !== 'skipped')
2128
+ .map((plugin) => plugin.name)
2129
+ .filter(Boolean);
2130
+ if (!names.length) {
2131
+ setEnablePrompt(null);
2132
+ return;
2133
+ }
2134
+ setEnablePrompt({ names, enabled: result.enabled !== false });
2135
+ };
2136
+
2137
+ /** 点「启用」或「停用」:只动 bundles,不动文件。 */
2138
+ const toggleEnabled = async (enabled) => {
2139
+ if (!enablePrompt?.names?.length) return;
2140
+ setBusy(true);
2141
+ try {
2142
+ const data = await setEnabled(enablePrompt.names, enabled);
2143
+ setEnablePrompt({ ...enablePrompt, enabled: Boolean(data?.enabled) });
2144
+ await load();
2145
+ } catch (err) {
2146
+ setImportResult({ error: String(err?.message ?? err) });
2147
+ } finally {
2148
+ setBusy(false);
2149
+ }
2150
+ };
2151
+
2020
2152
  /** 「本地导入插件」:把一个已存在的插件目录接进当前 profile。 */
2021
2153
  /** 拖放进来:zip/tgz 走整合包导入,目录走本地插件导入。 */
2022
2154
  const runDrop = async (file) => {
@@ -2065,21 +2197,35 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2065
2197
  return;
2066
2198
  }
2067
2199
 
2068
- if (conflicts.length) {
2069
- /* 先把冲突列表给出来,用户选完才上传 + 真导入。
2070
- * 这时还没上传,所以待决策任务在服务端也不存在 —— 全靠确认时再登记。 */
2071
- setPendingImport({
2072
- kind: 'bundle',
2073
- pendingFile: file, // 还没上传,先拿着 File 对象
2074
- file: null,
2075
- conflicts,
2076
- plugins: packPlugins,
2077
- quick: true,
2078
- });
2079
- setBusy(false);
2080
- setPhase('');
2081
- return;
2082
- }
2200
+ /* 不管有没有重名,都在写盘之前弹一次确认框,列出包里所有插件:
2201
+ * - 重名的 → 覆盖 / 跳过
2202
+ * - 新的 → 标「新建」
2203
+ * 这时文件还没上传,取消就什么都不发生。 */
2204
+ const rows = packPlugins.map((plugin) => {
2205
+ const hit = installed.get(plugin.name);
2206
+ return {
2207
+ name: plugin.name,
2208
+ version: plugin.version ?? null,
2209
+ installedVersion: hit?.version ?? null,
2210
+ dest: hit?.dir ?? null,
2211
+ bytes: plugin.bytes ?? null,
2212
+ sameVersion: (hit?.version ?? null) === (plugin.version ?? null),
2213
+ exists: Boolean(hit),
2214
+ };
2215
+ });
2216
+
2217
+ setPendingImport({
2218
+ kind: 'bundle',
2219
+ pendingFile: file, // 还没上传,先拿着 File 对象
2220
+ file: null,
2221
+ conflicts: rows.filter((row) => row.exists),
2222
+ rows,
2223
+ plugins: packPlugins,
2224
+ quick: true,
2225
+ });
2226
+ setBusy(false);
2227
+ setPhase('');
2228
+ return;
2083
2229
  }
2084
2230
 
2085
2231
  // 第二步:确认无冲突(或拿不到快速清单)才上传
@@ -2087,18 +2233,32 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2087
2233
  const uploaded = await uploadZip(file);
2088
2234
  setZipPath(uploaded.file);
2089
2235
 
2090
- // 先 dryRun 探一次:有重名冲突就弹窗让用户决定
2236
+ // 先 dryRun 探一次:拿到完整插件清单,写盘前弹确认
2091
2237
  setPhase(t('import.checking'));
2092
2238
  const preview = await doImport({ file: uploaded.file, dryRun: true });
2093
2239
  const conflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
2094
- if (conflicts.length) {
2095
- // 服务端在 dryRun 时已登记了"待决策"任务,带 jobId 回来
2240
+ const conflictByName = new Map(conflicts.map((item) => [item.name, item]));
2241
+ const previewRows = (preview?.plugins ?? []).map((plugin) => {
2242
+ const hit = conflictByName.get(plugin.name);
2243
+ return {
2244
+ name: plugin.name,
2245
+ version: plugin.version ?? null,
2246
+ installedVersion: hit?.installedVersion ?? null,
2247
+ dest: hit?.dest ?? null,
2248
+ sameVersion: Boolean(hit?.sameVersion),
2249
+ exists: Boolean(hit),
2250
+ };
2251
+ });
2252
+
2253
+ if (previewRows.length) {
2254
+ // 服务端在 dryRun 时已登记了"待决策"任务(仅当有重名),带 jobId 回来
2096
2255
  pendingJobId.current = preview?.jobId ?? null;
2097
2256
  setPendingImport({
2098
2257
  kind: 'bundle',
2099
2258
  jobId: preview?.jobId ?? null,
2100
2259
  file: uploaded.file,
2101
2260
  conflicts,
2261
+ rows: previewRows,
2102
2262
  plugins: preview.plugins ?? [],
2103
2263
  });
2104
2264
  setBusy(false);
@@ -2114,6 +2274,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2114
2274
  linkNodeModules: true,
2115
2275
  });
2116
2276
  setImportResult(result);
2277
+ rememberEnableState(result);
2117
2278
  await load();
2118
2279
  await refreshJobs();
2119
2280
  } else {
@@ -2184,6 +2345,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2184
2345
  plugins: fetched.plugins,
2185
2346
  });
2186
2347
  setImportResult(result);
2348
+ rememberEnableState(result);
2187
2349
  await load();
2188
2350
  await refreshJobs();
2189
2351
  // 装完关掉弹窗,回到列表看结果
@@ -2191,19 +2353,32 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2191
2353
  return;
2192
2354
  }
2193
2355
 
2194
- // 下载好的包按普通导入走一遍:先 dryRun 看有没有重名冲突
2356
+ // 下载好的包:先 dryRun 拿完整清单,写盘前弹确认
2195
2357
  const preview = await doImport({ file: fetched.file, dryRun: true });
2196
- const conflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
2197
- if (conflicts.length) {
2358
+ const netConflicts = Array.isArray(preview?.conflicts) ? preview.conflicts : [];
2359
+ const netConflictByName = new Map(netConflicts.map((item) => [item.name, item]));
2360
+ const netRows = (preview?.plugins ?? []).map((plugin) => {
2361
+ const hit = netConflictByName.get(plugin.name);
2362
+ return {
2363
+ name: plugin.name,
2364
+ version: plugin.version ?? null,
2365
+ installedVersion: hit?.installedVersion ?? null,
2366
+ dest: hit?.dest ?? null,
2367
+ sameVersion: Boolean(hit?.sameVersion),
2368
+ exists: Boolean(hit),
2369
+ };
2370
+ });
2371
+ if (netRows.length) {
2198
2372
  pendingJobId.current = preview?.jobId ?? null;
2199
2373
  setPendingImport({
2200
2374
  kind: 'bundle',
2201
2375
  jobId: preview?.jobId ?? null,
2202
2376
  file: fetched.file,
2203
- conflicts,
2377
+ conflicts: netConflicts,
2378
+ rows: netRows,
2204
2379
  plugins: preview.plugins ?? [],
2205
2380
  });
2206
- // 交给冲突弹窗接管,先关掉来源弹窗
2381
+ // 交给确认弹窗接管,先关掉来源弹窗
2207
2382
  setNetOpen(false);
2208
2383
  return;
2209
2384
  }
@@ -2603,6 +2778,81 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2603
2778
  null,
2604
2779
  h(DropZone, { onFile: runDrop, busy, dropRef: lastDrop, phase }),
2605
2780
 
2781
+ /* 安装后是否立即启用。
2782
+ * 勾着 → 装进 dsh.profile.bundles,重启就加载;
2783
+ * 取消 → 只装文件与依赖,重启也不加载,之后想用再点「启用」。 */
2784
+ h(
2785
+ 'label',
2786
+ {
2787
+ className: 'pp-hint',
2788
+ style: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 10, cursor: busy ? 'default' : 'pointer' },
2789
+ },
2790
+ h('input', {
2791
+ type: 'checkbox',
2792
+ checked: enableOnInstall,
2793
+ disabled: busy,
2794
+ onChange: (event) => {
2795
+ const next = event.target.checked;
2796
+ installPrefs.enable = next;
2797
+ setEnableOnInstall(next);
2798
+ },
2799
+ }),
2800
+ t('enable.onInstall'),
2801
+ ),
2802
+
2803
+ /* 装完之后的选择:立即启用 / 不启用。
2804
+ * 停用只是把它从 bundles 里拿掉,文件还在,随时能再打开。 */
2805
+ enablePrompt
2806
+ ? h(
2807
+ 'div',
2808
+ { className: 'pp-card', style: { marginTop: 12 } },
2809
+ h(
2810
+ 'div',
2811
+ { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
2812
+ h(
2813
+ 'span',
2814
+ { style: { fontSize: 13, fontWeight: 500 } },
2815
+ `${t('enable.installedTitle')} · ${enablePrompt.names.length} ${t('plugins')}`,
2816
+ ),
2817
+ h(
2818
+ 'span',
2819
+ {
2820
+ className: 'pp-pill',
2821
+ style: {
2822
+ background: enablePrompt.enabled
2823
+ ? 'color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 14%,transparent)'
2824
+ : 'color-mix(in srgb,var(--dsw-alias-label-secondary,#6b7280) 14%,transparent)',
2825
+ color: enablePrompt.enabled
2826
+ ? 'var(--dsw-alias-state-success-primary,#15803d)'
2827
+ : 'var(--dsw-alias-label-secondary,#6b7280)',
2828
+ },
2829
+ },
2830
+ enablePrompt.enabled ? t('enable.enabled') : t('enable.disabled'),
2831
+ ),
2832
+ h('span', { style: { flex: 1 } }),
2833
+ h(
2834
+ 'button',
2835
+ {
2836
+ className: 'pp-btn sm',
2837
+ disabled: busy,
2838
+ onClick: () => toggleEnabled(!enablePrompt.enabled),
2839
+ },
2840
+ enablePrompt.enabled ? t('enable.disable') : t('enable.enable'),
2841
+ ),
2842
+ ),
2843
+ h(
2844
+ 'div',
2845
+ { className: 'pp-hint', style: { marginTop: 6 } },
2846
+ enablePrompt.enabled ? t('enable.enabledHint') : t('enable.disabledHint'),
2847
+ ),
2848
+ h(
2849
+ 'div',
2850
+ { className: 'pp-hint pp-mono', style: { marginTop: 4, fontSize: 11 } },
2851
+ enablePrompt.names.join(', '),
2852
+ ),
2853
+ )
2854
+ : null,
2855
+
2606
2856
  // 后台任务:切走画面回来能看到刚才那次导入的结果
2607
2857
  h(JobList, { jobs, onClear: clearJobHistory, t }),
2608
2858
 
@@ -2910,6 +3160,11 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2910
3160
  phase,
2911
3161
  onInstall: runNetworkInstall,
2912
3162
  onClose: () => setNetOpen(false),
3163
+ enableOnInstall,
3164
+ onToggleEnable: (next) => {
3165
+ installPrefs.enable = next;
3166
+ setEnableOnInstall(next);
3167
+ },
2913
3168
  t,
2914
3169
  }),
2915
3170
  // 重名冲突弹窗:叠在最上层
package/lib/enable.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 启用 / 停用已安装的插件。
3
+ *
4
+ * DSH 里"启用"的判定很简单:包名在 `profile/package.json` 的
5
+ * `dsh.profile.bundles` 数组里,启动时就会被加载;不在就不加载。
6
+ *
7
+ * 所以启用 = 加进 bundles,停用 = 从 bundles 移出。
8
+ * 文件、依赖、node_modules 链接都不动 —— 停用不是卸载,
9
+ * 用户随时可以再启用,不用重新装一遍。
10
+ *
11
+ * @module dsh-plugin-pack/lib/enable
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+
16
+ import { readJson, writeJson } from './util.js';
17
+ import { clearScanCache } from './scan.js';
18
+
19
+ /** 读 profile 的 package.json(缺字段时补齐,但不写盘)。 */
20
+ function loadPkg(profileDir) {
21
+ const pkgFile = path.join(profileDir, 'package.json');
22
+ const pkg = readJson(pkgFile, null);
23
+ if (!pkg || typeof pkg !== 'object') {
24
+ const error = new Error(`profile 里没有 package.json: ${pkgFile}`);
25
+ error.code = 'BAD_PROFILE';
26
+ throw error;
27
+ }
28
+ pkg.dsh = pkg.dsh && typeof pkg.dsh === 'object' ? pkg.dsh : {};
29
+ pkg.dsh.profile = pkg.dsh.profile && typeof pkg.dsh.profile === 'object' ? pkg.dsh.profile : {};
30
+ if (!Array.isArray(pkg.dsh.profile.bundles)) pkg.dsh.profile.bundles = [];
31
+ return { pkg, pkgFile };
32
+ }
33
+
34
+ /**
35
+ * 读取一批插件当前的启用状态。
36
+ *
37
+ * @param {string} profileDir
38
+ * @param {string[]} names
39
+ * @returns {Array<{ name: string, enabled: boolean, installed: boolean }>}
40
+ */
41
+ export function getEnabledState(profileDir, names) {
42
+ const { pkg } = loadPkg(profileDir);
43
+ const bundles = new Set(pkg.dsh.profile.bundles);
44
+ const deps = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {};
45
+ const localPluginsDir = path.join(profileDir, 'local-plugins');
46
+
47
+ return names.map((name) => {
48
+ const segments = String(name).split('/');
49
+ const inLocalPlugins =
50
+ segments.length === 1 && fs.existsSync(path.join(localPluginsDir, name));
51
+ return {
52
+ name,
53
+ enabled: bundles.has(name),
54
+ // 装了(在依赖里或 local-plugins 里有目录)但可能没启用
55
+ installed: Boolean(deps[name]) || inLocalPlugins || bundles.has(name),
56
+ };
57
+ });
58
+ }
59
+
60
+ /**
61
+ * 批量启用或停用。
62
+ *
63
+ * @param {object} options
64
+ * @param {string} options.profileDir
65
+ * @param {string[]} options.names 插件包名
66
+ * @param {boolean} options.enabled true=启用(加入 bundles),false=停用(移出)
67
+ * @param {boolean} [options.dryRun]
68
+ * @returns {{ ok: boolean, enabled: boolean, changed: Array<{name: string, from: boolean, to: boolean}>, bundles: string[], restartRequired: boolean }}
69
+ */
70
+ export function setPluginsEnabled({ profileDir, names, enabled, dryRun = false }) {
71
+ const profile = path.resolve(profileDir);
72
+ const list = Array.isArray(names) ? names.filter((name) => typeof name === 'string' && name.trim()) : [];
73
+ if (!list.length) {
74
+ const error = new Error('没有指定要启用/停用的插件');
75
+ error.code = 'EMPTY_SELECTION';
76
+ throw error;
77
+ }
78
+
79
+ const { pkg, pkgFile } = loadPkg(profile);
80
+ const before = new Set(pkg.dsh.profile.bundles);
81
+ const changed = [];
82
+
83
+ for (const name of list) {
84
+ const from = before.has(name);
85
+ if (from === enabled) continue; // 已经是目标状态,不动
86
+ changed.push({ name, from, to: enabled });
87
+ if (enabled) before.add(name);
88
+ else before.delete(name);
89
+ }
90
+
91
+ // bundles 保持原顺序,新增的追加到末尾;停用的直接消失
92
+ const nextBundles = pkg.dsh.profile.bundles.filter((name) => before.has(name));
93
+ for (const item of changed) {
94
+ if (item.to && !nextBundles.includes(item.name)) nextBundles.push(item.name);
95
+ }
96
+
97
+ if (!dryRun && changed.length) {
98
+ pkg.dsh.profile.bundles = nextBundles;
99
+ writeJson(pkgFile, pkg);
100
+ // 配置变了,扫描缓存要失效,否则界面还按旧状态显示
101
+ clearScanCache();
102
+ }
103
+
104
+ return {
105
+ ok: true,
106
+ enabled: Boolean(enabled),
107
+ changed,
108
+ bundles: nextBundles,
109
+ restartRequired: changed.length > 0,
110
+ };
111
+ }
@@ -222,9 +222,14 @@ export function importLocalPlugin(options = {}) {
222
222
 
223
223
  // 是"路径插件"(patch 里靠绝对路径挂的)就不进 bundles/依赖
224
224
  const pathPlugin = Boolean(info.hasPatch) && !info.entry;
225
+ /* enable=false:只装文件与依赖,不进 dsh.profile.bundles —— "装好了但先不启用"。
226
+ * 之后可以在界面上再打开。 */
227
+ const enable = options.enable !== false;
225
228
  if (!pathPlugin) {
226
229
  pkg.dependencies[info.name] = `link:./local-plugins/${segment}`;
227
- if (!pkg.dsh.profile.bundles.includes(info.name)) pkg.dsh.profile.bundles.push(info.name);
230
+ if (enable && !pkg.dsh.profile.bundles.includes(info.name)) {
231
+ pkg.dsh.profile.bundles.push(info.name);
232
+ }
228
233
  }
229
234
  writeJson(pkgFile, pkg);
230
235
 
@@ -260,8 +265,11 @@ export function importLocalPlugin(options = {}) {
260
265
  deps: info.deps,
261
266
  link,
262
267
  backup: backupRoot,
268
+ enabled: enable,
263
269
  restartRequired: true,
264
- note: '重启 dsh 加载这个插件;如果它带客户端界面,之后可以用热加载',
270
+ note: enable
271
+ ? '重启 dsh 加载这个插件;如果它带客户端界面,之后可以用热加载'
272
+ : '已安装但未启用。重启后也不会加载,需要时在界面上点「启用」。',
265
273
  };
266
274
  }
267
275
 
package/lib/import-tgz.js CHANGED
@@ -101,6 +101,8 @@ export function importTarball(options = {}) {
101
101
  mode: options.mode === 'skip' ? 'skip' : 'merge',
102
102
  // 单个插件也可能重名,同样支持逐插件决策
103
103
  decisions: options.decisions && typeof options.decisions === 'object' ? options.decisions : {},
104
+ // enable=false → 只装不启用
105
+ enable: options.enable !== false,
104
106
  onProgress,
105
107
  // 关键:tgz 里的 node_modules 是插件的真实依赖,必须一起搬过去。
106
108
  // 漏了这一项会导致插件加载时报 ERR_MODULE_NOT_FOUND(dsh-pet 的 @electron/get 事故)。
package/lib/import.js CHANGED
@@ -447,10 +447,16 @@ 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
+ /* enable=false:只把文件装上、依赖写好,但不进 dsh.profile.bundles
451
+ * —— 也就是"装好了但先不启用"。用户之后可以在界面上再打开。
452
+ * files 与依赖照常写,所以不启用不等于没装。 */
453
+ const enable = options.enable !== false;
450
454
  for (const item of plan) {
451
455
  if (item.action === 'skip' || item.pathPlugin) continue;
452
456
  pkg.dependencies[item.name] = `link:./local-plugins/${item.segment}`;
453
- if (!pkg.dsh.profile.bundles.includes(item.name)) pkg.dsh.profile.bundles.push(item.name);
457
+ if (enable && !pkg.dsh.profile.bundles.includes(item.name)) {
458
+ pkg.dsh.profile.bundles.push(item.name);
459
+ }
454
460
  }
455
461
  writeJson(pkgFile, pkg);
456
462
 
@@ -490,7 +496,10 @@ export function importBundle(options = {}) {
490
496
  conflicts,
491
497
  decisions: Object.fromEntries(plan.filter((item) => item.exists).map((item) => [item.name, item.action])),
492
498
  links,
493
- bundlesAdded: bundleAdds,
499
+ bundlesAdded: enable ? bundleAdds : [],
500
+ enabled: enable,
501
+ // 本次装上但没启用的插件名(界面据此提示"已安装,未启用")
502
+ disabledNames: enable ? [] : applied.filter((item) => item.action !== 'skipped').map((item) => item.name),
494
503
  patch: patchPlan.summary,
495
504
  dataRestored: record.dataDirs.length,
496
505
  warnings,
package/lib/server.js CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  } from './inbox.js';
40
40
  import { runJob, listJobs, getJob, runningJobs, clearFinished, openWaiting, getWaiting, listWaiting, cancelWaiting } from './jobs.js';
41
41
  import { fetchBundleFrom, parseSource } from './fetch-bundle.js';
42
+ import { setPluginsEnabled, getEnabledState } from './enable.js';
42
43
  import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, humanBytes } from './home.js';
43
44
  import { readJson, writeJson, isDir, isFile } from './util.js';
44
45
 
@@ -608,6 +609,7 @@ export function mountRoutes(ctx, config = {}) {
608
609
  dryRun,
609
610
  mode: body.mode === 'skip' ? 'skip' : 'merge',
610
611
  decisions: body.decisions && typeof body.decisions === 'object' ? body.decisions : {},
612
+ enable: body.enable !== false,
611
613
  onProgress: report,
612
614
  });
613
615
  if (dryRun) {
@@ -638,6 +640,8 @@ export function mountRoutes(ctx, config = {}) {
638
640
  only: Array.isArray(body.only) && body.only.length ? body.only : null,
639
641
  // 逐插件决策:界面弹窗后传进来,优先级高于全局 mode
640
642
  decisions,
643
+ // enable=false → 只装文件不启用(用户可以在装完后选)
644
+ enable: body.enable !== false,
641
645
  onProgress: report,
642
646
  });
643
647
  if (dryRun) {
@@ -683,6 +687,56 @@ export function mountRoutes(ctx, config = {}) {
683
687
  'plugin-pack: import',
684
688
  );
685
689
 
690
+ /* 启用 / 停用已装插件(装完后用户自己选的那个开关)。
691
+ * GET /plugin-pack/enabled?name=a&name=b 查状态
692
+ * POST /plugin-pack/enable { names, enabled } 批量切换
693
+ *
694
+ * 启用 = 加入 profile 的 dsh.profile.bundles;停用 = 移出。
695
+ * 文件与依赖不动,停用后随时能再打开,不用重装。 */
696
+ register(
697
+ {
698
+ kind: 'exact',
699
+ path: `${PREFIX}/enabled`,
700
+ handler: async (request, response) => {
701
+ if (request.method !== 'GET') {
702
+ sendJson(response, 405, { ok: false, error: '只支持 GET' });
703
+ return;
704
+ }
705
+ const url = new URL(request.url ?? '/', 'http://localhost');
706
+ const names = url.searchParams.getAll('name').filter(Boolean);
707
+ const local = contextOf(config);
708
+ sendJson(response, 200, {
709
+ ok: true,
710
+ plugins: getEnabledState(local.profileDir, names),
711
+ });
712
+ },
713
+ },
714
+ 'plugin-pack: enabled',
715
+ );
716
+
717
+ register(
718
+ {
719
+ kind: 'exact',
720
+ path: `${PREFIX}/enable`,
721
+ handler: async (request, response) => {
722
+ if (request.method !== 'POST') {
723
+ sendJson(response, 405, { ok: false, error: '只支持 POST' });
724
+ return;
725
+ }
726
+ const body = await readJsonBody(request);
727
+ const local = contextOf(config);
728
+ const result = setPluginsEnabled({
729
+ profileDir: local.profileDir,
730
+ names: Array.isArray(body.names) ? body.names : body.name ? [body.name] : [],
731
+ enabled: body.enabled !== false,
732
+ dryRun: Boolean(body.dryRun),
733
+ });
734
+ sendJson(response, 200, result);
735
+ },
736
+ },
737
+ 'plugin-pack: enable',
738
+ );
739
+
686
740
  /* 待决策任务:导入遇到重名时登记,界面切走再回来靠它捡回弹窗。
687
741
  * GET /plugin-pack/waiting 列出待决策任务(含冲突列表)
688
742
  * POST /plugin-pack/waiting/cancel 取消(用户切走/关页面/点取消)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",