dsh-m 0.3.0 → 0.4.0

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/README.en.md CHANGED
@@ -37,21 +37,33 @@ The default registry includes DSH Skins, ModSearch, the Lark / QQ / Weixin / WeC
37
37
  | View | Capabilities |
38
38
  |---|---|
39
39
  | **Market** | Card flow with search and category filters (server-side filtering + pagination, 50 per page — even a 1,000-entry registry probes the current page only); expandable details; "Install" — npm sources pin the latest exact version, GitHub sources pin the release/tag commit |
40
- | **Installed** | What your web profile actually has, annotated "via market / non-market"; update badges, upgrade, two-step confirm uninstall; 📖 README preview (64KB cap) |
41
- | **Settings** | Registry address draft with "Validate & apply / Restore default / Download default registry.json / Check entries reachability"; configured vs active address and status at a glance; force refresh; dsh-m self-update |
40
+ | **Installed** | What your web profile actually has, annotated "via market / non-market"; update badges, upgrade, two-step confirm uninstall; 📖 README preview (64KB cap); **live phase badge** (●active / ●failed / ○pending) and a **one-click toggle** (0.4.0: delegates to the official pluginManager service for live application; falls back to file-level edits + restart notice when absent; dsh-m itself and official host lifelines are locked) |
41
+ | **Settings** | Registry address draft with "Validate & apply / Restore default / Download default registry.json / Check entries reachability"; configured vs active address and status at a glance; force refresh; dsh-m self-update; **metadata source display** (npmjs / npmmirror ping race winner — affects metadata reads only, never the install path) |
42
42
 
43
43
  After any mutation, the already-open Market and Installed views refresh the profile state together, keeping badges and cards in sync without closing and reopening the marketplace; a "⚡ Restart" banner appears — under systemd, the DSH launcher's `appExit` hook hands the restart back to a unit configured with `Restart=on-failure` or `Restart=always`, avoiding a `systemctl` helper inside the unit cgroup that is about to stop; if `appExit` is unavailable, the fallback uses a manager-owned transient `systemd-run` service and only then a detached helper. The client confirms the replacement by boot id and dismisses the banner, leaving DSH Web's own background connection recovery in control; it does not force a full-page reload during the auth/route handoff. The current DSH Web `0.1.5-rc.1` runtime has now live-loaded dsh-m `0.2.11`; `/dshm` ping returned version `0.2.11`, and an authenticated page request completed `303 → 200`. The reported 404 was not reproducible once the service stabilized; journal evidence showed repeated `status=75/TEMPFAIL` restarts during the incident. Live DSH `0.1.2-rc.1`, transient-fallback, and repeated install/uninstall E2E experiments remain pre-release gaps. Installs stream live pnpm progress (resolve → download → link → build).
44
44
 
45
- ## Agent tools (7)
45
+ ### New in 0.4.0
46
+
47
+ - **Enablement toggle**: one-click on/off per installed card; internally routed to a row override (single-row plugins, applies live) or bundle selection (multi-row); the write path delegates to the official `pluginManager` service and falls back to direct loader operations when absent ([ADR-0001](./docs/adr/0001-delegate-with-fallback-for-plugin-manager.md)).
48
+ - **Live phase badges**: a projection of loader fiber state — failed plugins are visible at a glance.
49
+ - **Protection roster**: `dsh-m` itself plus the 16 official host lifeline modules cannot be toggled or uninstalled (upgrades unaffected).
50
+ - **Precise build approval**: when pnpm blocks build scripts, only the pending list is approved key-by-key; the allow-everything fallback is labeled honestly ([ADR-0002](./docs/adr/0002-precise-build-approval.md)).
51
+ - **Peer compatibility precheck**: install/upgrade validates `@deepseek-ai/dsh(-*)` peers against the runtime version before touching the profile (GitHub sources state the check was skipped); on mismatch the GUI asks, the agent tool returns structured data, the CLI takes `--force`.
52
+ - **Verified runtimes**: an optional `verified` array per registry entry records DSH runtimes actually tested — a claim of record, not a prediction; display-only, never gates installs.
53
+ - **Bundle identity check**: post-install warning when a package lands without a patch layer ("installed as a plain dependency").
54
+ - **Metadata source probe**: npmjs / npmmirror ping race (1500ms budget, 5-minute cache by default) that only selects dsh-m's metadata read source.
55
+
56
+ ## Agent tools (8)
46
57
 
47
58
  | Tool | Purpose |
48
59
  |---|---|
49
60
  | `dshm_search` | Search the curated registry (renders cards in chat) |
50
61
  | `dshm_list` | List installed plugins (market / non-market annotated) |
51
- | `dshm_install` | Install by listing id |
62
+ | `dshm_install` | Install by listing id (peer compatibility precheck before install; incompatible results return structured data for user confirmation, then `force` retries) |
52
63
  | `dshm_uninstall` | Uninstall (confirm first; data kept, leftovers reported) |
53
64
  | `dshm_outdated` | Check for newer versions |
54
65
  | `dshm_upgrade` | Upgrade to the latest |
66
+ | `dshm_toggle` | Toggle a plugin on/off (0.4.0; confirm by default, execute directly when the user already asked in the same message) |
55
67
  | `dshm_restart` | Restart DSH Web (with user consent) |
56
68
 
57
69
  ## CLI
@@ -59,9 +71,10 @@ After any mutation, the already-open Market and Installed views refresh the prof
59
71
  ```sh
60
72
  dshm search [--query topic] [--category ui]
61
73
  dshm list | outdated | registry
62
- dshm install --id dsh-web-search
63
- dshm upgrade --pkg dsh-web-search --yes
74
+ dshm install --id dsh-web-search [--force] # --force: skip the precheck gate after confirming the risk
75
+ dshm upgrade --pkg dsh-web-search --yes [--force]
64
76
  dshm uninstall --pkg dsh-web-search --yes
77
+ dshm toggle --pkg dsh-web-search --on|--off --yes # file-level edit; applies on restart
65
78
  dshm restart --yes
66
79
  ```
67
80
 
package/README.md CHANGED
@@ -37,21 +37,33 @@
37
37
  | 视图 | 能力 |
38
38
  |---|---|
39
39
  | **市场** | 收录卡片流;关键词搜索、分类筛选(服务端过滤 + 分页,每页 50 条,1,000 条清单也只探测当前页);卡片展开详情与「安装」;npm 源锁定最新精确版本,GitHub 源锁定 release/tag 指向的 commit |
40
- | **已装** | web profile 实装列表,标注「市场安装 / 非市场安装」;可升级徽标、升级、两段式确认卸载;📖 README 预览(64KB 截断) |
41
- | **设置** | registry 地址草稿 +「校验并应用 / 恢复默认 / 下载默认 registry.json / 检查条目可达性」;配置地址、生效来源与状态一目了然;强制刷新;dsh-m 自更新 |
40
+ | **已装** | web profile 实装列表,标注「市场安装 / 非市场安装」;可升级徽标、升级、两段式确认卸载;📖 README 预览(64KB 截断);**运行相位徽标**(●active / ●failed / ○pending)与**一键开关**(0.4.0:委派官方 pluginManager 服务活体生效,服务缺席时文件级编辑 + 重启提示;dsh-m 自身与官方宿主命脉锁定不可开关) |
41
+ | **设置** | registry 地址草稿 +「校验并应用 / 恢复默认 / 下载默认 registry.json / 检查条目可达性」;配置地址、生效来源与状态一目了然;强制刷新;dsh-m 自更新;**元数据源展示**(npmjs / npmmirror ping 竞速胜出,只影响元数据读取,不动安装链路) |
42
42
 
43
43
  安装 / 卸载 / 升级完成后,当前已打开的市场页与已装页会一起重新读取 profile 状态并同步徽标/卡片,不需要关闭后重新打开插件市场;随后出现「⚡ 一键重启」横幅——受 systemd 管理时通过 DSH launcher 的 `appExit` 交给服务的 `Restart` 策略,避免在待停止 unit 的 cgroup 内启动 `systemctl` helper;无 `appExit` 的 systemd 兜底改用 manager-owned transient `systemd-run`,最后才退回 detached-helper。客户端按 boot id 确认新进程已恢复后关闭横幅,交由 DSH Web 自身的后台连接重试恢复页面,不强制整页刷新,避免认证/路由切换期间白屏。已完成当前 DSH Web `0.1.5-rc.1` 运行时的 live 核验:web profile 已加载 dsh-m `0.2.11`,重启后 `/dshm` ping 返回 `version: 0.2.11`,携带当前认证 token 的页面请求流程为 `303 → 200`;无认证请求会被拒绝。此次白屏截图对应的 404 在服务稳定后未复现;journal 显示截图时段发生多次 `status=75/TEMPFAIL` 重启,当前暂判定为重启/认证过渡窗口现象,未发现 dsh-m Host 路由崩溃。DSH `0.1.2-rc.1` live E2E、transient fallback live E2E,以及连续安装/卸载实验仍是正式发布前的验证 gap。安装过程实时显示 pnpm 进度(解析 → 下载 → 链接 → 构建)。
44
44
 
45
- ## Agent 工具(7 个)
45
+ ### 0.4.0 新增
46
+
47
+ - **开关(Enablement Toggle)**:已装卡片一键启停,内部自动路由行覆盖(单行插件,即时生效)或 Bundle 选择(多行插件);写路径委派官方 `pluginManager` 服务、缺席时降级 loader 直操作([ADR-0001](./docs/adr/0001-delegate-with-fallback-for-plugin-manager.md))。
48
+ - **运行相位徽标**:loader fiber 状态投影,failed 一眼可见。
49
+ - **保护名单**:`dsh-m` 自身与官方宿主命脉 16 项不可开关、不可卸载(升级不受影响)。
50
+ - **精确构建放行**:needs-builds 拦截后按 pnpm 待决名单逐键放行,全量放行降为兜底并如实标注([ADR-0002](./docs/adr/0002-precise-build-approval.md))。
51
+ - **peer 兼容预检**:安装/升级前校验 `@deepseek-ai/dsh(-*)` peers 与运行时版本(GitHub 源明示未检);不兼容时 GUI 弹确认、agent 回结构化结果、CLI `--force`。
52
+ - **实测版本清单(verified)**:registry 条目可选 `verified` 数组记录实测过的 DSH 运行时版本——实测声明而非预测声明,只展示不拦截。
53
+ - **bundle 身份验证**:装后检测无补丁层的包并警告「已装入为纯依赖」。
54
+ - **元数据源竞速**:npmjs / npmmirror ping 并发探测(默认 1500ms 预算、5 分钟缓存),只影响 dsh-m 元数据读取源。
55
+
56
+ ## Agent 工具(8 个)
46
57
 
47
58
  | 工具 | 用途 |
48
59
  |---|---|
49
60
  | `dshm_search` | 搜收录清单(对话内出卡片) |
50
61
  | `dshm_list` | 列已装插件(市场/非市场标注) |
51
- | `dshm_install` | 按收录 id 安装 |
62
+ | `dshm_install` | 按收录 id 安装(装前 peer 兼容预检;不兼容时回结构化结果,用户确认后 `force` 重试) |
52
63
  | `dshm_uninstall` | 卸载(先确认;不删数据,报告残留路径) |
53
64
  | `dshm_outdated` | 检查最新版本 |
54
65
  | `dshm_upgrade` | 升级到最新 |
66
+ | `dshm_toggle` | 切换插件运行状态(0.4.0;默认先确认,用户同句已明确可直接执行) |
55
67
  | `dshm_restart` | 一键重启 DSH Web(先征得同意) |
56
68
 
57
69
  ## CLI
@@ -59,9 +71,10 @@
59
71
  ```sh
60
72
  dshm search [--query 主题] [--category ui]
61
73
  dshm list | outdated | registry
62
- dshm install --id dsh-web-search
63
- dshm upgrade --pkg dsh-web-search --yes
74
+ dshm install --id dsh-web-search [--force] # --force:确认兼容风险后跳过预检拦截
75
+ dshm upgrade --pkg dsh-web-search --yes [--force]
64
76
  dshm uninstall --pkg dsh-web-search --yes
77
+ dshm toggle --pkg dsh-web-search --on|--off --yes # 文件级编辑,重启生效
65
78
  dshm restart --yes
66
79
  ```
67
80
 
package/docs/DESIGN.md CHANGED
@@ -86,16 +86,26 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
86
86
  - **卸载**(事务定序写死):validate(严格读取:NOT_INSTALLED / NOT_DSH_PLUGIN / PLUGIN_METADATA_UNREADABLE,全部零写入零快照)→ 快照 → live-disable(先让 client bundle 下线,避免 404;回滚时尽力反向)→ 摘除该包补丁条目(`pnpm-workspace.yaml` 顶层 `patchedDependencies` 与 `package.json#pnpm.patchedDependencies`;残留条目会令 pnpm 以 `ERR_PNPM_UNUSED_PATCH` 整单失败,只精确匹配 `pkg` / `pkg@ver`,补丁文件本体保留并计入残留报告)→ remove → verify gone(仍在则回滚)。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
87
87
  - **升级**:**按需检查**(`dshm_outdated` / `dshm_list` 时实时比对本地版本 vs npm latest / GitHub release,半自动——展示升级计划,确认后执行,即重开一次安装事务)。**不做后台定时器**。
88
88
  - **自更新**:dsh-m 对自己同样做版本比对 + 提示升级(设置页呈现);执行即 `install-npm` 事务(integrity 缺失直接拒绝,不进事务)。
89
+ - **开关(0.4.0,术语见 CONTEXT.md「开关/行覆盖/Bundle 选择/委派降级」)**:已装插件运行状态的可逆切换,**不经 Profile 变更事务**。粒度自动路由——包内补丁层单 insert 行且无配置补丁行 → 行覆盖(`cordis.patch.yml` 保注释 `disabled` 编辑,官方 `writePluginEnabled` 同款语义,启用写显式 `disabled: false`);多行/不可解析 → Bundle 选择(`dsh.profile.bundles` 数组增删)。写路径委派优先:运行时探测官方 `pluginManager` 服务(`ctx.get`,按存在性不判版本号)→ `setPluginEnabled`/`setBundleEnabled`(白拿保护判定、unaddressable 识别、hmr 活体重组;applied 以复读 `listPlugins()` 为准);服务缺席降级 loader `entry.update()` 直操作 + 锁内文件编辑(bundle 级恒 restart-required)。**必须持官方同款锁**(`withFileLock(<profile>/package.json)`)防与官方 UI 并发丢更新。架构决策见 [ADR-0001](./adr/0001-delegate-with-fallback-for-plugin-manager.md)。
90
+ - **运行相位(0.4.0)**:loader fiber 状态投影(active/failed/pending/loading/unloading;fiber 缺失即已停用),读路径始终自读 loader entries,读路径零服务依赖。
91
+ - **保护名单(0.4.0)**:`dsh-m` 自身 + 逐字镜像官方 `protectedModules` 16 项全集(锚点 `dsh-plugin-manager@0.1.7-rc.2 lib/index.js L1077`;DSH 升级后 diff 官方名单)。作用于**开关与卸载**(保护门为开关路径最先判定,纯名单检查先于在装枚举);升级不受影响;委派路径信任官方 `readOnlyReason`。
92
+ - **peer 兼容预检(0.4.0)**:install/upgrade 事务外 metadata 阶段,校验 `@deepseek-ai/dsh(-*)` peers 与运行时版本(`workspace:^/~/ *` 视为运行时版本;semver includePrerelease;官方 `evaluatePluginCompatibility` 同语义)。运行时版本解析失败或预检元数据读取失败 → 不拦(如实标注未检);不兼容 → GUI 弹确认 / agent 回结构化 issue / CLI `--force`;无豁免机制(确认即通道);GitHub 源明示 `compatSkipped`。
93
+ - **精确构建放行(0.4.0,[ADR-0002](./adr/0002-precise-build-approval.md))**:needs-builds 拦截点(adapter 的 `makeAddViaLadder`)先读 `pnpm-workspace.yaml` 的 pnpm 待决名单(值为 `set this to true or false` 的无通配符键),逐键写 `allowBuilds.<pkg>: true`;名单空/不可读才走全量兜底并如实标注。放行结果沿 `RunnerOutcome.buildApprovals`/`fallbackAllBuilds` 上行(`usedAllowAllBuilds` 字段已删除)。
94
+ - **bundle 身份验证(0.4.0)**:事务 verify 阶段观察——包内无 `cordis.patch.yml` 且不在 bundles 数组 → `bundleWarning: 'no-patch-layer'`(装成纯依赖),只警告不回滚(存在合法的无补丁层 dsh 包)。
95
+ - **实测版本清单(0.4.0,术语「实测版本清单」)**:registry 条目可选 `verified: string[]`(精确 semver),来自各仓库对照源的实测声明;只展示与收录质量提示,**不做安装拦截依据**。
96
+ - **元数据源竞速(0.4.0)**:npmjs/npmmirror `/-/ping` 并发竞速(首个 2xx 胜出、败者 abort、双败保持默认 npmjs;1500ms 预算 / 5 分钟缓存,`probeEnabled/probeTimeoutMs/probeCacheTtlMin` 可调);**只读不写**——只影响 dsh-m 元数据/packument 读取源与市场页展示,安装链路(pnpm 读 profile .npmrc)不动。
89
97
  - **重启**:内置**一键重启**。在服务管理器托管的 DSH 进程内,只有确认 unit 的 `Restart=on-failure` / `Restart=always` 且 `75` 未被 `SuccessExitStatus` / `RestartPreventExitStatus` 覆盖时,才调用 launcher 提供的 `appExit(75)`;策略未知或不满足时改用 manager-owned transient `systemd-run` 调用 `systemctl`,不猜测或硬编码 unit 名称,也不在即将停止的 cgroup 内 detached spawn `systemctl`。无 systemd/appExit 时再退回 detached-helper 兼容路径。安装/卸载/升级完成后 GUI 弹「需重启生效 [一键重启]」横幅;客户端以 boot id 确认替换进程后关闭横幅,交由 DSH Web 自身后台连接恢复,不强制整页刷新;工具返回重启提示。
90
98
  - **安全基线(5 条)**:
91
99
  1. 所有拉取仅 HTTPS + 响应大小上限 + 超时;
92
100
  2. npm 安装校验 integrity;
93
101
  3. GitHub 安装强制 pinned SHA;
94
- 4. pnpm 构建脚本被拦时,沿用 skillhub 的 `dangerouslyAllowAllBuilds` 重试,但**必须明确报告**「该插件需要执行构建脚本」;
102
+ 4. pnpm 构建脚本被拦时,先按 pnpm 待决名单**逐键精确放行**(ADR-0002),名单不可读才 `dangerouslyAllowAllBuilds` 全量兜底,且**必须明确报告**放行了哪些包/是否兜底;
95
103
  5. 不做签名/验签体系(自用,明确不做)。
96
104
 
97
105
  ## 4. GUI(旗舰,v1 必须做好)
98
106
 
107
+ 0.4.0 增补:已装卡右上 `dshm-switch` 开关(受 `toggleable` 控制,锁因 title 提示);sub 行相位点 `● active · v1.0.8 · npm`(相位点只映射 phase 五值,「已停用」归 Switch,两输入源各管各的);开关结果通知按 `applied` 分流(live → 绿 toast「即时生效」;restart-required → 沿用重启横幅 + 一键重启);安装遇兼容拦截(409 + issue)弹「仍要安装」确认(红字风险 + peers 清单 → `forceIncompatible` 重发);设置页「元数据源」行展示竞速胜出源。
108
+
99
109
  3 个视图,卡片展开式详情(不做独立详情页),**中文优先**,跟随 DSH Web 深色主题:
100
110
 
101
111
  1. **市场页**(默认):registry 卡片流;搜索/分类为**服务端过滤**(Host 强制 `withLatest=true`、每页 50、1,000 条清单第一页只探测当前页);`MarketPanel` 是市场/已装数据唯一 owner(请求 generation + AbortController 丢弃旧响应);安装/卸载/升级完成后通过统一协调器同时刷新市场与已装快照,不要求关闭并重新打开面板;分页控件 + 超过 200 条性能提示 + 默认/自定义/缓存/不可用短提示(不含本地路径);卡片详情保留 README markdown 预览与 npm/GitHub 官方外链。
package/lib/cli.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * registry/search/list/outdated 在清单不可用时打印配置/实际生效地址并 exit 1。
7
7
  */
8
8
  import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, } from './core/market.js';
9
+ import { togglePlugin as coreTogglePlugin } from './core/toggle.js';
9
10
  import { loadRegistry } from './core/registry.js';
10
11
  import { scheduleRestart } from './core/restart.js';
11
12
  import { pathToFileURL } from 'node:url';
@@ -43,6 +44,16 @@ function needFlag(flags, name) {
43
44
  }
44
45
  return v.trim();
45
46
  }
47
+ /** 构建放行输出(ADR-0002):列包名;兜底全量放行如实标注。 */
48
+ function outBuildsNote(res, out) {
49
+ if (res.fallbackAllBuilds === true) {
50
+ out('⚠️ 该插件执行了构建脚本(名单不可读,已全量兜底放行)。');
51
+ return;
52
+ }
53
+ const names = res.buildApprovals ?? [];
54
+ if (names.length > 0)
55
+ out(`⚠️ 该插件执行了构建脚本,已精确放行:${names.join('、')}。`);
56
+ }
46
57
  function requireYes(flags, action) {
47
58
  if (flags.yes !== true) {
48
59
  throw new Error(`拒绝执行:${action} 是变更操作,必须带 --yes 显式确认。`);
@@ -88,9 +99,10 @@ const HELP = `dshm — DSH Marketplace(个人自用 DSH 插件市场)
88
99
  dshm registry 查看收录清单来源与条目
89
100
 
90
101
  变更命令(必须 --yes):
91
- dshm install --id <收录id> [--version 1.2.3]
92
- dshm upgrade --pkg <包名> --yes
102
+ dshm install --id <收录id> [--version 1.2.3] [--force] (--force:确认兼容风险后跳过预检拦截)
103
+ dshm upgrade --pkg <包名> --yes [--force]
93
104
  dshm uninstall --pkg <包名> --yes
105
+ dshm toggle --pkg <包名> --on|--off --yes (切换运行状态;文件级编辑,重启生效)
94
106
  dshm restart --yes
95
107
  注意:变更互斥仅在进程内生效——变更执行期间不要同时从 GUI / Agent 工具发起另一次变更。
96
108
 
@@ -117,6 +129,7 @@ async function runCliDispatch(argv, deps, io) {
117
129
  installFromRegistry: deps.installFromRegistry ?? installFromRegistry,
118
130
  uninstallPlugin: deps.uninstallPlugin ?? uninstallPlugin,
119
131
  upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
132
+ togglePlugin: deps.togglePlugin ?? coreTogglePlugin,
120
133
  };
121
134
  const { cmd, flags } = parseArgs(argv);
122
135
  const cfg = cliConfig();
@@ -215,25 +228,45 @@ async function runCliDispatch(argv, deps, io) {
215
228
  case 'install': {
216
229
  const id = needFlag(flags, 'id');
217
230
  const version = typeof flags.version === 'string' ? flags.version : undefined;
218
- const res = await d.installFromRegistry(id, cfg, { version, namespace: 'cli' });
231
+ const res = await d.installFromRegistry(id, cfg, {
232
+ version,
233
+ forceIncompatible: flags.force === true,
234
+ namespace: 'cli',
235
+ });
219
236
  out(`✅ 已安装 ${res.pkg}(${res.spec})`);
220
- if (res.usedAllowAllBuilds)
221
- out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
237
+ outBuildsNote(res, out);
222
238
  out('需要重启 DSH Web 生效:dshm restart --yes');
223
239
  return 0;
224
240
  }
225
241
  case 'upgrade': {
226
242
  const target = needFlag(flags, 'pkg');
227
243
  requireYes(flags, '升级');
228
- const res = await d.upgradePlugin(target, cfg, { namespace: 'cli' });
244
+ const res = await d.upgradePlugin(target, cfg, {
245
+ forceIncompatible: flags.force === true,
246
+ namespace: 'cli',
247
+ });
229
248
  const from = res.fromVersion ? `v${res.fromVersion} → ` : '';
230
249
  const to = res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : '最新';
231
250
  out(`✅ 已升级 ${res.pkg}(${from}${to})`);
232
- if (res.usedAllowAllBuilds)
233
- out('⚠️ 该插件执行了构建脚本(已按策略放行)。');
251
+ outBuildsNote(res, out);
234
252
  out('需要重启 DSH Web 生效:dshm restart --yes');
235
253
  return 0;
236
254
  }
255
+ case 'toggle': {
256
+ const target = needFlag(flags, 'pkg');
257
+ const onFlag = flags.on === true;
258
+ const offFlag = flags.off === true;
259
+ if (onFlag === offFlag)
260
+ throw new Error('开关命令需要恰好一个 --on 或 --off');
261
+ requireYes(flags, '切换插件运行状态');
262
+ const res = await d.togglePlugin(target, onFlag);
263
+ out(`✅ 已${onFlag ? '启用' : '停用'} ${res.pkg}(profile 文件已更新)`);
264
+ if (res.applied === 'live')
265
+ out('本次进程内已即时生效;重启后状态保持。');
266
+ else
267
+ out('需要重启 DSH Web 生效:dshm restart --yes');
268
+ return 0;
269
+ }
237
270
  case 'uninstall': {
238
271
  const target = needFlag(flags, 'pkg');
239
272
  requireYes(flags, '卸载');