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 +322 -67
- package/lib/enable.js +111 -0
- package/lib/import-plugin.js +10 -2
- package/lib/import-tgz.js +2 -0
- package/lib/import.js +11 -2
- package/lib/server.js +54 -0
- package/package.json +1 -1
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) =>
|
|
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(
|
|
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(
|
|
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
|
-
'
|
|
1593
|
-
{
|
|
1594
|
-
|
|
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'
|
|
1606
|
-
|
|
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
|
-
|
|
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.
|
|
1632
|
-
|
|
1633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
-
|
|
2095
|
-
|
|
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
|
-
//
|
|
2356
|
+
// 下载好的包:先 dryRun 拿完整清单,写盘前弹确认
|
|
2195
2357
|
const preview = await doImport({ file: fetched.file, dryRun: true });
|
|
2196
|
-
const
|
|
2197
|
-
|
|
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
|
+
}
|
package/lib/import-plugin.js
CHANGED
|
@@ -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))
|
|
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:
|
|
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))
|
|
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