dsh-plugin-pack 1.0.11 → 1.0.13
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 +170 -2
- package/lib/enable.js +111 -0
- package/lib/import-plugin.js +23 -6
- package/lib/import-tgz.js +2 -0
- package/lib/import.js +11 -2
- package/lib/scan.js +15 -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': '松手就导入',
|
|
@@ -320,6 +328,14 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
320
328
|
'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
329
|
'net.done': 'Downloaded and imported',
|
|
322
330
|
'net.hintTitle': 'What you can do: ',
|
|
331
|
+
'enable.onInstall': 'Enable right after install',
|
|
332
|
+
'enable.installedTitle': 'Installed',
|
|
333
|
+
'enable.enabled': 'Enabled',
|
|
334
|
+
'enable.disabled': 'Not enabled',
|
|
335
|
+
'enable.enable': 'Enable',
|
|
336
|
+
'enable.disable': 'Disable',
|
|
337
|
+
'enable.enabledHint': 'These plugins will load after you restart dsh.',
|
|
338
|
+
'enable.disabledHint': 'Files are installed but will not load. Click Enable when you need them, then restart dsh.',
|
|
323
339
|
'import.pageTitle': 'Import plugin bundle',
|
|
324
340
|
'drop.title': 'Drop a zip / tgz or plugin folder here',
|
|
325
341
|
'drop.release': 'Release to import',
|
|
@@ -528,9 +544,19 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
528
544
|
}
|
|
529
545
|
}
|
|
530
546
|
|
|
547
|
+
/* 安装偏好:由界面上的「安装后立即启用」勾选同步过来。
|
|
548
|
+
* 放这里是因为 doImport 在接口层、拿不到组件 state;
|
|
549
|
+
* 用一个普通对象透传,避免把选项硬编码进每个调用点。 */
|
|
550
|
+
const installPrefs = { enable: true };
|
|
551
|
+
|
|
531
552
|
const getStatus = () => api('/plugin-pack/status');
|
|
532
553
|
const doExport = (body) => api('/plugin-pack/export', { method: 'POST', body: JSON.stringify(body) });
|
|
533
|
-
const doImport = (body) =>
|
|
554
|
+
const doImport = (body) =>
|
|
555
|
+
api('/plugin-pack/import', {
|
|
556
|
+
method: 'POST',
|
|
557
|
+
// 显式传 enable;body 里如果自己带了 enable 就以 body 为准
|
|
558
|
+
body: JSON.stringify({ enable: installPrefs.enable, ...body }),
|
|
559
|
+
});
|
|
534
560
|
// inspect 接口保留在服务端(工具/CLI 会用),界面这层不再需要它
|
|
535
561
|
const doRollback = (backup) => api('/plugin-pack/rollback', { method: 'POST', body: JSON.stringify({ backup }) });
|
|
536
562
|
const doBrowse = (dir, options = {}) =>
|
|
@@ -547,6 +573,9 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
547
573
|
api('/plugin-pack/waiting/cancel', { method: 'POST', body: JSON.stringify({ id, reason }) });
|
|
548
574
|
/** 从网络来源(GitHub / npm / 直链)拉整合包。 */
|
|
549
575
|
const fetchBundle = (body) => api('/plugin-pack/fetch-bundle', { method: 'POST', body: JSON.stringify(body) });
|
|
576
|
+
/** 启用 / 停用已装插件(装完后用户自己选的那个开关)。 */
|
|
577
|
+
const setEnabled = (names, enabled) =>
|
|
578
|
+
api('/plugin-pack/enable', { method: 'POST', body: JSON.stringify({ names, enabled }) });
|
|
550
579
|
|
|
551
580
|
/* ------------------------- 浏览器端读 zip 头部 ------------------------- */
|
|
552
581
|
|
|
@@ -1343,7 +1372,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1343
1372
|
* @param {() => void} props.onClose
|
|
1344
1373
|
* @param {(key: string) => string} props.t
|
|
1345
1374
|
*/
|
|
1346
|
-
function AddBundleDialog({ open, value, onChange, result, busy, phase, onInstall, onClose, t }) {
|
|
1375
|
+
function AddBundleDialog({ open, value, onChange, result, busy, phase, onInstall, onClose, t, enableOnInstall, onToggleEnable }) {
|
|
1347
1376
|
const [showExamples, setShowExamples] = useState(false);
|
|
1348
1377
|
|
|
1349
1378
|
// Esc 关闭
|
|
@@ -1515,6 +1544,22 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1515
1544
|
)
|
|
1516
1545
|
: null,
|
|
1517
1546
|
|
|
1547
|
+
// 安装后是否立即启用(和导入页那个勾选是同一个状态)
|
|
1548
|
+
h(
|
|
1549
|
+
'label',
|
|
1550
|
+
{
|
|
1551
|
+
className: 'pp-hint',
|
|
1552
|
+
style: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 12, cursor: busy ? 'default' : 'pointer' },
|
|
1553
|
+
},
|
|
1554
|
+
h('input', {
|
|
1555
|
+
type: 'checkbox',
|
|
1556
|
+
checked: enableOnInstall !== false,
|
|
1557
|
+
disabled: busy,
|
|
1558
|
+
onChange: (event) => onToggleEnable?.(event.target.checked),
|
|
1559
|
+
}),
|
|
1560
|
+
t('enable.onInstall'),
|
|
1561
|
+
),
|
|
1562
|
+
|
|
1518
1563
|
// 大按钮(对齐自带的整宽「安装」)
|
|
1519
1564
|
h(
|
|
1520
1565
|
'button',
|
|
@@ -1856,6 +1901,10 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
1856
1901
|
const [netResult, setNetResult] = useState(null);
|
|
1857
1902
|
/** 「添加整合包」弹窗是否打开。 */
|
|
1858
1903
|
const [netOpen, setNetOpen] = useState(false);
|
|
1904
|
+
/** 安装后是否立即启用(勾选框)。关掉 → 只装文件,不进 bundles。 */
|
|
1905
|
+
const [enableOnInstall, setEnableOnInstall] = useState(true);
|
|
1906
|
+
/** 刚装完的插件及其启用状态,用于装完后再切换。 */
|
|
1907
|
+
const [enablePrompt, setEnablePrompt] = useState(null);
|
|
1859
1908
|
/** 后台任务列表:切走画面回来靠它恢复 */
|
|
1860
1909
|
const [jobs, setJobs] = useState([]);
|
|
1861
1910
|
/** 体积上限(MiB),0 = 不限制。默认跟着服务端配置走。 */
|
|
@@ -2017,6 +2066,43 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2017
2066
|
}
|
|
2018
2067
|
};
|
|
2019
2068
|
|
|
2069
|
+
/**
|
|
2070
|
+
* 记下这次装完的插件和它的启用状态,供界面上的「启用 / 停用」切换。
|
|
2071
|
+
*
|
|
2072
|
+
* 装完不等于启用:enable=false 时文件在磁盘上、依赖也写好了,
|
|
2073
|
+
* 但不在 dsh.profile.bundles 里,dsh 启动时不会加载它。
|
|
2074
|
+
*/
|
|
2075
|
+
const rememberEnableState = (result) => {
|
|
2076
|
+
if (!result?.ok || result.dryRun) {
|
|
2077
|
+
setEnablePrompt(null);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
const names = (result.plugins ?? [])
|
|
2081
|
+
.filter((plugin) => plugin && plugin.action !== 'skipped')
|
|
2082
|
+
.map((plugin) => plugin.name)
|
|
2083
|
+
.filter(Boolean);
|
|
2084
|
+
if (!names.length) {
|
|
2085
|
+
setEnablePrompt(null);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
setEnablePrompt({ names, enabled: result.enabled !== false });
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
/** 点「启用」或「停用」:只动 bundles,不动文件。 */
|
|
2092
|
+
const toggleEnabled = async (enabled) => {
|
|
2093
|
+
if (!enablePrompt?.names?.length) return;
|
|
2094
|
+
setBusy(true);
|
|
2095
|
+
try {
|
|
2096
|
+
const data = await setEnabled(enablePrompt.names, enabled);
|
|
2097
|
+
setEnablePrompt({ ...enablePrompt, enabled: Boolean(data?.enabled) });
|
|
2098
|
+
await load();
|
|
2099
|
+
} catch (err) {
|
|
2100
|
+
setImportResult({ error: String(err?.message ?? err) });
|
|
2101
|
+
} finally {
|
|
2102
|
+
setBusy(false);
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2020
2106
|
/** 「本地导入插件」:把一个已存在的插件目录接进当前 profile。 */
|
|
2021
2107
|
/** 拖放进来:zip/tgz 走整合包导入,目录走本地插件导入。 */
|
|
2022
2108
|
const runDrop = async (file) => {
|
|
@@ -2114,6 +2200,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2114
2200
|
linkNodeModules: true,
|
|
2115
2201
|
});
|
|
2116
2202
|
setImportResult(result);
|
|
2203
|
+
rememberEnableState(result);
|
|
2117
2204
|
await load();
|
|
2118
2205
|
await refreshJobs();
|
|
2119
2206
|
} else {
|
|
@@ -2184,6 +2271,7 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2184
2271
|
plugins: fetched.plugins,
|
|
2185
2272
|
});
|
|
2186
2273
|
setImportResult(result);
|
|
2274
|
+
rememberEnableState(result);
|
|
2187
2275
|
await load();
|
|
2188
2276
|
await refreshJobs();
|
|
2189
2277
|
// 装完关掉弹窗,回到列表看结果
|
|
@@ -2603,6 +2691,81 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2603
2691
|
null,
|
|
2604
2692
|
h(DropZone, { onFile: runDrop, busy, dropRef: lastDrop, phase }),
|
|
2605
2693
|
|
|
2694
|
+
/* 安装后是否立即启用。
|
|
2695
|
+
* 勾着 → 装进 dsh.profile.bundles,重启就加载;
|
|
2696
|
+
* 取消 → 只装文件与依赖,重启也不加载,之后想用再点「启用」。 */
|
|
2697
|
+
h(
|
|
2698
|
+
'label',
|
|
2699
|
+
{
|
|
2700
|
+
className: 'pp-hint',
|
|
2701
|
+
style: { display: 'flex', alignItems: 'center', gap: 6, marginTop: 10, cursor: busy ? 'default' : 'pointer' },
|
|
2702
|
+
},
|
|
2703
|
+
h('input', {
|
|
2704
|
+
type: 'checkbox',
|
|
2705
|
+
checked: enableOnInstall,
|
|
2706
|
+
disabled: busy,
|
|
2707
|
+
onChange: (event) => {
|
|
2708
|
+
const next = event.target.checked;
|
|
2709
|
+
installPrefs.enable = next;
|
|
2710
|
+
setEnableOnInstall(next);
|
|
2711
|
+
},
|
|
2712
|
+
}),
|
|
2713
|
+
t('enable.onInstall'),
|
|
2714
|
+
),
|
|
2715
|
+
|
|
2716
|
+
/* 装完之后的选择:立即启用 / 不启用。
|
|
2717
|
+
* 停用只是把它从 bundles 里拿掉,文件还在,随时能再打开。 */
|
|
2718
|
+
enablePrompt
|
|
2719
|
+
? h(
|
|
2720
|
+
'div',
|
|
2721
|
+
{ className: 'pp-card', style: { marginTop: 12 } },
|
|
2722
|
+
h(
|
|
2723
|
+
'div',
|
|
2724
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
2725
|
+
h(
|
|
2726
|
+
'span',
|
|
2727
|
+
{ style: { fontSize: 13, fontWeight: 500 } },
|
|
2728
|
+
`${t('enable.installedTitle')} · ${enablePrompt.names.length} ${t('plugins')}`,
|
|
2729
|
+
),
|
|
2730
|
+
h(
|
|
2731
|
+
'span',
|
|
2732
|
+
{
|
|
2733
|
+
className: 'pp-pill',
|
|
2734
|
+
style: {
|
|
2735
|
+
background: enablePrompt.enabled
|
|
2736
|
+
? 'color-mix(in srgb,var(--dsw-alias-state-success-primary,#16a34a) 14%,transparent)'
|
|
2737
|
+
: 'color-mix(in srgb,var(--dsw-alias-label-secondary,#6b7280) 14%,transparent)',
|
|
2738
|
+
color: enablePrompt.enabled
|
|
2739
|
+
? 'var(--dsw-alias-state-success-primary,#15803d)'
|
|
2740
|
+
: 'var(--dsw-alias-label-secondary,#6b7280)',
|
|
2741
|
+
},
|
|
2742
|
+
},
|
|
2743
|
+
enablePrompt.enabled ? t('enable.enabled') : t('enable.disabled'),
|
|
2744
|
+
),
|
|
2745
|
+
h('span', { style: { flex: 1 } }),
|
|
2746
|
+
h(
|
|
2747
|
+
'button',
|
|
2748
|
+
{
|
|
2749
|
+
className: 'pp-btn sm',
|
|
2750
|
+
disabled: busy,
|
|
2751
|
+
onClick: () => toggleEnabled(!enablePrompt.enabled),
|
|
2752
|
+
},
|
|
2753
|
+
enablePrompt.enabled ? t('enable.disable') : t('enable.enable'),
|
|
2754
|
+
),
|
|
2755
|
+
),
|
|
2756
|
+
h(
|
|
2757
|
+
'div',
|
|
2758
|
+
{ className: 'pp-hint', style: { marginTop: 6 } },
|
|
2759
|
+
enablePrompt.enabled ? t('enable.enabledHint') : t('enable.disabledHint'),
|
|
2760
|
+
),
|
|
2761
|
+
h(
|
|
2762
|
+
'div',
|
|
2763
|
+
{ className: 'pp-hint pp-mono', style: { marginTop: 4, fontSize: 11 } },
|
|
2764
|
+
enablePrompt.names.join(', '),
|
|
2765
|
+
),
|
|
2766
|
+
)
|
|
2767
|
+
: null,
|
|
2768
|
+
|
|
2606
2769
|
// 后台任务:切走画面回来能看到刚才那次导入的结果
|
|
2607
2770
|
h(JobList, { jobs, onClear: clearJobHistory, t }),
|
|
2608
2771
|
|
|
@@ -2910,6 +3073,11 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2910
3073
|
phase,
|
|
2911
3074
|
onInstall: runNetworkInstall,
|
|
2912
3075
|
onClose: () => setNetOpen(false),
|
|
3076
|
+
enableOnInstall,
|
|
3077
|
+
onToggleEnable: (next) => {
|
|
3078
|
+
installPrefs.enable = next;
|
|
3079
|
+
setEnableOnInstall(next);
|
|
3080
|
+
},
|
|
2913
3081
|
t,
|
|
2914
3082
|
}),
|
|
2915
3083
|
// 重名冲突弹窗:叠在最上层
|
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
|
@@ -33,15 +33,20 @@ export function describeLocalPlugin(dir) {
|
|
|
33
33
|
throw error;
|
|
34
34
|
}
|
|
35
35
|
const entry = resolveEntry(target, meta);
|
|
36
|
-
|
|
36
|
+
// dsh.bundle.patch 可能是字符串或数组(一个插件可声明多个 patch 文件)
|
|
37
|
+
const patchSpec = meta?.dsh?.bundle?.patch;
|
|
38
|
+
const patchRels = (Array.isArray(patchSpec) ? patchSpec : [patchSpec]).filter(
|
|
39
|
+
(rel) => typeof rel === 'string' && rel.trim(),
|
|
40
|
+
);
|
|
37
41
|
return {
|
|
38
42
|
dir: target,
|
|
39
43
|
name: meta.name.trim(),
|
|
40
44
|
version: meta.version ?? '0.0.0',
|
|
41
45
|
description: typeof meta.description === 'string' ? meta.description : '',
|
|
42
46
|
entry,
|
|
43
|
-
hasPatch:
|
|
44
|
-
patchRel,
|
|
47
|
+
hasPatch: patchRels.some((rel) => isFile(path.join(target, rel))),
|
|
48
|
+
patchRel: patchRels[0] ?? null,
|
|
49
|
+
patchRels,
|
|
45
50
|
client: meta.dsh?.client ?? null,
|
|
46
51
|
deps: Object.keys(meta.dependencies ?? {}).filter((dep) => !dep.startsWith('@deepseek-ai/')),
|
|
47
52
|
};
|
|
@@ -96,7 +101,11 @@ export function importLocalPlugin(options = {}) {
|
|
|
96
101
|
const insertRows = [];
|
|
97
102
|
const skippedRows = [];
|
|
98
103
|
if (info.hasPatch) {
|
|
99
|
-
|
|
104
|
+
// 多 patch 文件的插件要把每一份都读进来合并,只读第一个会丢配置
|
|
105
|
+
const ownPatch = (info.patchRels ?? [info.patchRel])
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.map((rel) => readText(path.join(info.dir, rel), '') ?? '')
|
|
108
|
+
.join('\n');
|
|
100
109
|
const target = { name: info.name, dirs: [info.dir] };
|
|
101
110
|
for (const entry of splitTopLevel(ownPatch)) {
|
|
102
111
|
if (entry.kind === 'insert') {
|
|
@@ -213,9 +222,14 @@ export function importLocalPlugin(options = {}) {
|
|
|
213
222
|
|
|
214
223
|
// 是"路径插件"(patch 里靠绝对路径挂的)就不进 bundles/依赖
|
|
215
224
|
const pathPlugin = Boolean(info.hasPatch) && !info.entry;
|
|
225
|
+
/* enable=false:只装文件与依赖,不进 dsh.profile.bundles —— "装好了但先不启用"。
|
|
226
|
+
* 之后可以在界面上再打开。 */
|
|
227
|
+
const enable = options.enable !== false;
|
|
216
228
|
if (!pathPlugin) {
|
|
217
229
|
pkg.dependencies[info.name] = `link:./local-plugins/${segment}`;
|
|
218
|
-
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
|
+
}
|
|
219
233
|
}
|
|
220
234
|
writeJson(pkgFile, pkg);
|
|
221
235
|
|
|
@@ -251,8 +265,11 @@ export function importLocalPlugin(options = {}) {
|
|
|
251
265
|
deps: info.deps,
|
|
252
266
|
link,
|
|
253
267
|
backup: backupRoot,
|
|
268
|
+
enabled: enable,
|
|
254
269
|
restartRequired: true,
|
|
255
|
-
note:
|
|
270
|
+
note: enable
|
|
271
|
+
? '重启 dsh 加载这个插件;如果它带客户端界面,之后可以用热加载'
|
|
272
|
+
: '已安装但未启用。重启后也不会加载,需要时在界面上点「启用」。',
|
|
256
273
|
};
|
|
257
274
|
}
|
|
258
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/scan.js
CHANGED
|
@@ -557,6 +557,17 @@ async function buildPlugin(item, { profileDir, dshHome, topEntries, includeSecre
|
|
|
557
557
|
if (!entry && meta) pluginWarnings.push('找不到入口文件,导入后可能加载失败');
|
|
558
558
|
if (pluginWarnings.length) warnings.push(...pluginWarnings.map((text) => `${name}: ${text}`));
|
|
559
559
|
|
|
560
|
+
/**
|
|
561
|
+
* `dsh.bundle.patch` 可能是字符串,也可能是数组
|
|
562
|
+
* (一个插件声明多个 patch 文件,实测 dsh-agentic-pet 就是数组)。
|
|
563
|
+
* 早先只当字符串处理,遇到数组会让 path.join 抛
|
|
564
|
+
* ERR_INVALID_ARG_TYPE: The "path" argument must be of type string。
|
|
565
|
+
*/
|
|
566
|
+
const patchSpec = meta?.dsh?.bundle?.patch;
|
|
567
|
+
const patchRels = (Array.isArray(patchSpec) ? patchSpec : [patchSpec]).filter(
|
|
568
|
+
(rel) => typeof rel === 'string' && rel.trim(),
|
|
569
|
+
);
|
|
570
|
+
|
|
560
571
|
return {
|
|
561
572
|
name,
|
|
562
573
|
version: meta?.version ?? '0.0.0',
|
|
@@ -568,8 +579,10 @@ async function buildPlugin(item, { profileDir, dshHome, topEntries, includeSecre
|
|
|
568
579
|
inBundles: item.inBundles,
|
|
569
580
|
bundleIndex: item.bundleIndex,
|
|
570
581
|
sources: [...item.sources],
|
|
571
|
-
hasPatch:
|
|
572
|
-
|
|
582
|
+
hasPatch: patchRels.some((rel) => isFile(path.join(item.dir, rel))),
|
|
583
|
+
// 主 patch 路径(多文件时取第一个,其余在 patchRels 里)
|
|
584
|
+
patchRel: patchRels[0] ?? null,
|
|
585
|
+
patchRels,
|
|
573
586
|
client: meta?.dsh?.client ?? null,
|
|
574
587
|
entry,
|
|
575
588
|
deps,
|
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