# 36 · 功能插件分区铺给普通用户 + 新用户自动铺(批次 2) - 日期:2026-09-11 - 触发:用户澄清「**让用户在设置里面基于插件模式,启用和禁用插件,而不是让用户去门户管理页面**」 - 状态:**✅ 已实施(commit `adea865`)并验证** ## 一、需求落点 `@dsh-local/business-plugins` 就是「dsh 设置面板 → **功能插件**」分区(档案 16 阶段 2 交付): | 它做什么 | 实现 | |---|---| | 只列 admin 投放的插件 | 读门户 `GET /api/plugins/mine`(数据源 = 功能插件池),社区 3408 款进不来 | | 批量勾选启停 | 列表 + `应用更改` + 确认弹窗 + 异步进度(`⏳ 阶段(N 秒)`) | | 生效方式 | 门户装进该用户 profile → 重启其实例 | **问题**:它**只装在 admin profile**,普通用户看不到该分区。→ 本档案把它铺给普通用户。 ## 二、实施 ### A. `scripts/ensure-biz-plugins.cjs`(新建,幂等;一个实现两处调用) ``` 判定(关键):看 **bundles**,不看 dependencies · 包内 cordis.patch.yml 只对 bundles 成员生效 —— 只有 dep 而没进 bundles 等于没装 · 实测 guest 正是这种「有 dep、没 bundles」的状态 → 只需 reconcile,不必重装 动作:复制产物 tgz 到用户 ws(chown 用户)→ `pnpm add file:` → 对齐 dsh plugin add 的 reconcile 参数:默认所有 role=active 的非 admin 用户;可指定 userId/username;--all;--restart ``` ### B. 新用户自动铺(双层) | 层 | 做法 | |---|---| | 审批时 | `admin.ts` 的 `/api/users/:id/approve` 在改角色后 **detached `spawn`** 该脚本(fire-and-forget,不阻塞审批响应) | | 巡检兜底 | `/etc/cron.d/dsh-maintenance` 增加 **每天 04:30** 跑同一脚本(覆盖审批时失败的、以及历史遗留用户) | ## 三、顺带修掉的同类隐藏 bug:`ERR_PNPM_ADDING_TO_ROOT` guest 的 profile 里有 dsh 自动生成的 `pnpm-workspace.yaml`: ```yaml packages: - . nodeLinker: hoisted autoInstallPeers: false ``` → pnpm 把它当 **workspace root**,`pnpm add` 直接拒绝:`ERR_PNPM_ADDING_TO_ROOT`。 **这会让任何有该文件的用户都装不上插件**,而现有 `mine/apply` 路径同样中招(只是 admin 的 profile 恰好没有这个文件,所以一直没暴露)。 **修法**:所有 `pnpm add` / `pnpm remove` 加 **`--ignore-workspace-root-check`**(在有无该文件的两种 profile 下都工作)。涉及 `business-plugins.ts` 与铺装脚本。 ## 四、验证 | 检查项 | 结果 | |---|---| | guest 铺装 | ✅ 从「有 dep、没 bundles」→ `bundles=4`(含 `@dsh-local/business-plugins`) | | **bundle 是否真被加载** | ✅ 重启实例后 `$DSH_HOME/.dsh-biz-plugins.log` 写入新的 `apply pid=2` 标记(该标记正是 host 面 `apply()` 打的)→ cordis 确实加载了它,设置面板的分区随之出现 | | `pnpm add` 在 guest profile 下可用 | ✅ 加 flag 后 `+ @dsh-local/business-plugins 0.1.0` Done | | cron | ✅ 每天 04:30 巡检已入 `/etc/cron.d/dsh-maintenance` | | 编译 | ✅ `tsc` 零报错(期间修掉 `execFile` 的 TS 重载问题,改用 `spawn` + `unref()`) | ## 五、踩坑记录 1. **编排器 env / TS 重载**:`execFile(file, args, { stdio: 'ignore' }, cb)` 在本 TS 版本报 「'stdio' does not exist in type 'ExecFileOptions'」→ 改用 `spawn(..., { stdio:'ignore', detached:true }).unref()`, 这也是更地道的 fire-and-forget(无僵尸、真正不阻塞)。 2. **判断「装没装」要看 bundles**:只看 `dependencies` 会误判(dep 在、bundles 不在 → 等于没装)。 3. **`pnpm-workspace.yaml` 会让 pnpm 拒绝 add** —— 见上。 ## 六、后续 - **`portal-entry` 不应铺给普通用户**(那是 admin 回门户的入口);本脚本只铺 `business-plugins`,符合既有设计。 - **批次 3**(视觉与交互):改 `--dsw-*` 官方 token(自动跟随亮/暗/主题)、搜索 + 状态徽章 + 行内失败重试、官方 locale(zh/en)、前端 `location.reload()`。 - 巡检脚本**不带 `--restart`** → 只补 profile,实例在下次重启时生效;若要求"立即生效"需调用方再触发一次实例重启。