# 67 · 「功能管理」section 按 UI 规范重做(v0.2.4) - 日期:2026-09-12 - 触发:用户指出实例内「设置 → 功能管理」页「UI 交互太简陋、没参考 UI 规范、插件没重点显示中文说明」;核对成立后按方案执行 - 结论一句话:**把信息层次反过来(用途说明做主视觉)、字号与组件对齐 `06-工作台UI规范.md`、空态补层次、加行 hover** —— 并顺带发现一个会阻塞用户启用插件的平台 bug。 - 状态:✅ 已完成并部署(admin 已生效);⚠️ **guest 侧升级被一个平台 bug 阻塞**(见 §五) > **TL;DR**|**结论**:规范第 5 行明确「含其 client 面 section」,本页在约束内,逐项核对确认「简陋」成立;主视觉原本是包名(说明只作 11px 灰字),与门户 plugins 页相反 —— 已反转。 > **关键**:执行中发现 `node_modules` 有 **561 个 root 属主文件**,根因是**门户候选池的启用路径以 root 跑 pnpm**,导致该用户此后任何 `pnpm add` 都 EACCES。 > **状态**:✅ UI 已上线(admin 验证);⏸ root 属主修复待确认(R7:561 > 10) --- ## 一、核对结论(只读,详见 2026-09-12 日志) `06-工作台UI规范.md` 第 5 行适用范围明确含「**其 client 面 section**」→ 本页无豁免。逐项对照 `poc/business-plugins/lib/client.js`: | 规范 | 改前 | |---|---| | 正文 15-16px / 次要 13-14px | 容器 13px;副行与徽章 **11px**(低于阶梯下限) | | 空态:图标 + 标题 17px/600 + 说明 14px | **一行灰字** | | `.btn` 15px / `.btn-sm` 14px | 12px / `5px 10px` | | `.badge` `2px 8px` r10 13px | `1px 7px` r9 11px | | 列表行 hover 反馈 | 无 | | 第 7 行**强制**加载 `impeccable` + `taste-skill` | 二者在本机**不存在**(用户级技能目录只有 BT-* / dsh-*)→ 以规范文档为准(规范自称实测基线、优先级高于技能) | **中文说明缺失的三层根因**(另见档案 66 附录与日志): 1. 主次颠倒 —— 实例内包名做主视觉,门户反过来; 2. `description` 取自插件 `package.json`(英文); 3. **官方目录本来就有中文,但导入时没取用** —— 已在同批次修掉(见 §三.1)。 ## 二、改动(v0.2.4) `poc/business-plugins/lib/client.js` + `package.json`(版本 0.2.3 → 0.2.4,同名同版本改内容必须升版本号,否则 pnpm 缓存复用旧包): | 项 | 改前 | 改后 | |---|---|---| | **主视觉** | 包名 + 版本 13px | **用途说明 14px**(无说明时回退包名) | | 副行 | 说明 11px 灰字 | **包名 + 版本 12px**(有说明才渲染) | | 容器基准 | 13px | 14px(section 语境取 14/13/12 三档,不照搬页面级 16px) | | 徽章 | 11px / `1px 7px` / r9 | 12px / `2px 8px` / r10 | | 按钮 | 12px / `5px 10px` | 次要 13px / `5px 12px`;主按钮 14px / `7px 16px` | | 空态 / 加载态 | 一行灰字 | 「标题 15px/500 + 说明 13px」层次(规范 §4.9,padding 按 section 收窄) | | 列表行 hover | 无 | 注入式 CSS `.bp-row:hover`(规范 §4.3 / §5;内联样式表达不了 `:hover`) | | 按钮 hover | 无 | `.bp-btn:hover` 边框/文字转主色 | | 无匹配态 | 复用空态文案 | 独立文案(居中,13px) | | 布局 | gap 8px / padding `6px 8px` | gap 10px / padding `8px 10px`(规范 §2.4) | | 保留 | —— | `--dsw-*` design token(跟随 dsh 主题,**不**回退规范硬编码色)、i18n、探活刷新、rejected 行内提示 | 新增 locale key:`loadingDesc`、`emptyTitle`、`noMatch`(zh/en 同步,key 集一致)。搜索占位符改为「搜索插件名或用途」。 **交付**:`npm pack` → `business-plugins-0.2.4.tgz`(8951 B)→ `/opt/dsh/artifacts/` → `node scripts/ensure-biz-plugins.cjs --all --restart`。 ## 三、同批次的其他修复 1. **中文说明(方案第一步)**:`src/web/routes/whitelist.ts` 导入入库改用目录条目中文(`entry.description`,建索引时已按 `zh ?? en` 归一),并回填已入库的 3 条 → 门户「已投放插件」表与实例内列表均显示中文。已部署验证。 ## 四、部署结果 | 对象 | 结果 | |---|---| | **admin**(uid 114801) | ✅ 0.2.3 → 0.2.4 升级成功;bundles=6 含该包;当时无运行实例,下次访问即为新 UI | | **guest**(uid 100002) | ❌ `EACCES: permission denied, open '.../node_modules/.bin/modlens'` | ## 五、⚠️ 执行中发现的平台 bug:候选池启用路径污染属主 **现象**:guest 无法升级/启用任何插件 —— 以用户身份跑 `pnpm add` 时撞 `EACCES`。 **实测污染规模**: | 用户 | profile 下 root 属主项 | 分布 | |---|---|---| | guest | **561** | `.pnpm` 555 / `@liustack` 2 / `.bin` 2 / `dsh-univer-office` 1 / 一个 `.bak` | | admin | 5 | 全在 `.pnpm` | **根因**:门户候选池的启停路径 `src/web/routes/business-plugins.ts` 的 `install()` / `uninstall()`: ```ts execFileSync('pnpm', ['add', 'file:' + plugin.tgzPath, ...], { cwd: dir, env: pnpmEnv(user.id) }) ``` **没有 `setpriv` 降权** → 以平台进程身份(root)执行 pnpm → 装出来的文件属主是 root。此后用户自己的 `pnpm add/remove` 必然 EACCES。 **证据吻合**:`audit_log` 记录 guest 在 01:58 通过候选池启用 `@liustack/modlens` + `dsh-univer-office` —— 正是这批 `.pnpm`/`@liustack`/`dsh-univer-office` 项的来源。 **与既有档案的关系**:档案 43 已处理过「平台 root 污染用户工作区」并加了属主自愈,但**候选池这条路径未被覆盖**(`ensure-biz-plugins.cjs` 等平台脚本用的是 `setpriv` 正姿,路线不同所以没暴露)。 **修法(待确认)**: 1. **短期解阻塞**:把该 profile 下 561 个 root 属主项 chown 给用户(`find -user root -exec chown : {} +`)。**R7:>10 文件,须先确认**。 2. **长期修根因**:`install()` / `uninstall()` 改为 `setpriv --reuid --regid --clear-groups env HOME= pnpm …`,与平台脚本姿势对齐;并考虑给 `apply` 路径补一次属主自愈。 --- ## 追加(2026-09-12 22:2x):v0.2.6 —— 插件列表改为**卡片形式,一行 2 个** - **触发**:用户「把设置中功能管理的插件列表改为卡片形式 一行放2个卡片 好看些」(2026-09-12 22:13)。 - **落点**:`poc/business-plugins/lib/client.js`(client bundle)。 - **改动**(4 处,全部是渲染层,不动数据与交互逻辑): 1. **新增内层网格容器**:`display:grid; gridTemplateColumns:repeat(2, minmax(0,1fr)); gap:10px` —— **单独包一层**,**不直接改外层 `wrap`**(它同时装标题 / 搜索 / 按钮 / 提示等纵向块);插件项先收集进 `cards[]` 再统一 push。 2. **每项由「行」改「卡片」**:`padding 12px 14px`、`borderRadius 12px`、`background: var(--dsw-alias-bg-layer-1, #fff)`、**常显 1px `--dsw-alias-border-1` 边框**(原来非错误项是 `transparent`)。 3. **hover 反馈改按规范 §4.5**:`.bp-row:hover{ border-color: brand-primary; box-shadow: 0 4px 12px rgba(47,111,237,.15) }`(原为 hover 换背景色)。 4. **卡片内 `alignItems: flex-start`** —— 复选框与徽章顶部对齐,长说明不再把行高撑歪。 - **规范依据**:`06-工作台UI规范` §4.5(卡片 r12 + hover 上浮 / 主色扩散影)、§2.4(信息卡 `14px 16px`)、§2.3(大卡片 r12);颜色走 dsh 官方 `--dsw-*` token(本 section 嵌在 dsh 面板内,不照搬门户亮色基线)。 - **部署**:版本 **0.2.5 → 0.2.6**;产物 `business-plugins-0.2.6.tgz` → `/opt/dsh/artifacts/` → `node scripts/ensure-biz-plugins.cjs --all`(**未加 `--restart`**,不主动打断在线实例)→ admin(`cce6d1cd…`)与 guest(`4092b965…`)均已确认装到 **0.2.6**、且包内含改动(`gridTemplateColumns` 命中 1)。 - **生效条件**:client bundle 在**实例启动时**加载 ⇒ **用户需重启自己的实例**(实例页重启,或等下次冷启动)才看得到。 - **未做**:**未做浏览器渲染验证**(本机无 Chromium;按规范 §6.5,此类 section 级渲染只能由用户硬刷新确认)⇒ **待用户目视验收**。 --- ## v0.2.7 / v0.2.8 追加(2026-09-13,用户要求) > 按「档案只增不改」追加。记录规范之外的两块新增交互:**内存预估** 与 **页内弹窗**。 ### 触发(用户原话) - 「先不改挡位,先优化设置中 功能插件卡片的样式,把预估所占内存数值也写上去(卡片可以增加高度便于UI布局),在卡片列表上方 增加一个内存占用状态条(显示当前内存占用量,用户点击启用卡片时增加预估占用量,显示是否超出最大内存占用,让用户知道插件启用和内存占用预估状态)」 - 「启用插件内存预估值超过上限 点击确认按钮弹窗需要有警告(还有所有弹窗改为页面弹窗 参考mcn工作台弹窗实现)」 ### v0.2.7 · 内存预估(卡片 + 状态条) - 内存模型常量:`MEM_LIMIT_MIB=384`(档案 58)/ `MEM_BASE_MIB=285`(空载基线,smaps 实测)/ `MEM_TABLE={dsh-univer-office:64, dsh-plugin-mcn-suite:39}`(隔离 cgroup 实测 rss 增量)/ `MEM_FALLBACK_MIB=2`(轻量兜底)/ `MEM_TIGHT_RATIO=0.85`。 - 卡片:`padding 14/16` + `minHeight 112`;信息列第三行加「预估内存 ≈ N MiB」徽章,**≥60 红 / ≥30 黄 / 其余次要色**。 - 列表上方「内存预估」状态条:`当前 X MiB → 勾选后 Y MiB / 上限 384` + 三态词 + **双色进度条**(实心 = 服务端当前已启用那批 `savedIds`;浅色 = 本次勾选增量)+ 口径小字「预估(实测基准,非实时读数)」;超限时整条红边 + 警告行。 - 实现要点:`load()` 时快照服务端已启用集合 `savedIds` —— 否则 `p.enabled` 被 toggle 就地改掉后无法区分「当前 / 勾选后」。 ### v0.2.8 · 全部弹窗改页内弹窗 - **弃用 `window.confirm`**;`applyChanges()` 拆为 `askApply()`(开弹窗)+ `doApply()`(真提交)。 - 弹窗**照 MCN 工作台范式**(`dsh-plugin-mcn/lib/client.js`):`fixed inset:0` 遮罩(`rgba(0,0,0,.35)` / `zIndex 2000` / 居中)+ 面板(`radius 12` / `maxHeight 82vh` / `padding 18·22` / 阴影)+ **点遮罩关闭** + 面板 `stopPropagation` + ✕;三段结构:标题 / 正文(明说重启后果)/ 按钮行。 - **超限时**:弹窗内内存块转**红底红边** + 红色标题「⚠ 本次勾选将超出单实例内存上限」+ 说明;**确认键转危险色**;**未点确认不发请求**。 - 全文已无原生弹窗调用(`window.confirm/alert/prompt` 仅存于注释)。 ### 口径(重要) - 状态条是**预估值**(客户端拿不到 cgroup 实测值)。做**实时读数**需平台加只读路由(读 `memory.usage_in_bytes` / `MemoryMax`)→ 平台代码改动 + build + 重启服务,**尚未做**。 ### 部署与验证 - 产物:`business-plugins-0.2.8.tgz`(14,358 B,md5 `3ccb05bc6b138231bcaf1f81d7b50549`);**0.2.7 未单独部署**(0.2.8 含其全部改动)。 - 部署:`scp` → `/opt/dsh/artifacts/` → `node scripts/ensure-biz-plugins.cjs --all`(admin/guest 0.2.6 → 0.2.8)→ `--restart`(优雅停 scope,下次访问自动拉起)。 - **线上端到端自证**:临时会话 `enter` → 实例页 200 → 抠出 combo bundle URL(含 `@dsh-local/business-plugins/client.js`,`rev=27340d41a454`)→ 拉取 **4,585,899 B** → 命中 `mem.title`×4 / `__confirm`×1 / `confirm.overTitle`×3 / `MEM_LIMIT_MIB`×5 /「预估内存」×5。 - 仿真验证(临时 harness,用完即删):初始 `326/326` → 勾选 univer `390` → 点「应用更改」弹窗打开且**不发请求** → 点「确认应用」才 `POST /api/plugins/mine/apply`。 - **未做**:浏览器视觉验收(按 §6.5,此类 section 渲染只能由用户确认)。