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 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) => api('/plugin-pack/import', { method: 'POST', body: JSON.stringify(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
+ }
@@ -33,15 +33,20 @@ export function describeLocalPlugin(dir) {
33
33
  throw error;
34
34
  }
35
35
  const entry = resolveEntry(target, meta);
36
- const patchRel = typeof meta?.dsh?.bundle?.patch === 'string' ? meta.dsh.bundle.patch : null;
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: patchRel ? isFile(path.join(target, patchRel)) : false,
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
- const ownPatch = readText(path.join(info.dir, info.patchRel), '') ?? '';
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)) 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
+ }
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: '重启 dsh 加载这个插件;如果它带客户端界面,之后可以用热加载',
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)) 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/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: Boolean(meta?.dsh?.bundle?.patch) && isFile(path.join(item.dir, meta.dsh.bundle.patch)),
572
- patchRel: typeof meta?.dsh?.bundle?.patch === 'string' ? meta.dsh.bundle.patch : null,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",