Files
dsh_shenxian/dsh-server-docs/04-调整方案/16-页面导航定稿-技能插件管理面三决策落地.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

121 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 16 · 页面导航定稿 + 插件三层归属模型(方案)
- 日期:2026-09-10
- 触发:多轮对齐后,用户对"技能/插件管理面"拍板,并给出插件三层归属模型
- 结论一句话:**技能/插件各独立入口 + 独立页面(`skills.html` 技能 + 新建 `plugins.html` 插件,仅 admin 管公共面)+「我的技能」走 dsh 设置页 section + 功能插件按「候选池投放→按需启用→重启生效」三层归属 + folder_plugins 废弃**。
- 状态:**已封板,实施中 —— 阶段 0(bad6ed7)、阶段 1(6f86a5f)、阶段 2(b7fd85d)、页面 UI 优化(b97c9f8)、门户 SPA 化(9df64ac)已完成**
> **TL;DR**|**结论**:技能/插件管理面定稿:**技能与插件各独立页面**(`skills.html` / `plugins.html`,公共面仅 admin)+「我的技能」走**实例设置页 section**。
> **关键**:功能插件三层归属:**候选池投放 → 按需启用 → 重启生效**;`folder_plugins` 废弃。
> **状态**:✅ 已封板(阶段 0-2 已实施)
---
## 一、需求背景
档案 13「登录直达」覆盖了档案 11 建在 portal 页上的入口 → 登录成功后用户只在实例 UI 里,`desktop.html` / `admin.html` / `skills.html` 三个管理页在成功路径上根本不会被加载,导致用户(含 admin)看不到技能/插件入口。经多轮对齐,入口形态与插件归属最终定稿如下。
## 二、决策定稿
### 2.1 技能面(已拍板)
| # | 决策点 | 选定 | 说明 |
|---|---|---|---|
| 1 | 新页形态 | **技能/插件各独立入口 + 独立页面**:`skills.html`(技能)+ 新建 `plugins.html`(插件) | 「我的技能」不进门户页 |
| 2 | 共享技能启用 | **投放即生效** | admin 投放共享技能 → 目标用户只读可见,**不造开关**(技能与插件启用语义不同) |
| 3 | 「我的技能」入口 | **dsh 设置页自建 section** | 所有用户(含 admin)在实例设置里上传/管理自己的 skill,不跳页面、不回门户 |
### 2.2 插件面(三点归属模型 + 4 条补充,2026-09-10 用户提出)
**三层归属:**
1. **现有 `settings-plugins`** 管 **dsh 自带插件**(cordis 148 个),**仅 admin 可访问**(已收归)。
2. **门户 `plugins.html`(独立页面)** 管 **系统外插件 = 功能插件**,admin 投放,**所有用户可用**。
3. **实例设置新增「功能插件」section**,admin + 用户**手动启用/禁用功能插件**;**启用时复制插件到该用户对应目录**。
**补充决策(用户答复):**
- 投放 = **只进候选池、默认禁用**;用户启用后**需重启该用户 dsh 实例**才生效。
- 支持**批量设置启用/禁用**,操作需**点击确认弹窗**,点弹窗确认按钮才执行**重启实例**。
- **上传权限边界**:**上传插件 = 仅 admin(门户)**;普通用户只能上传 skill 自用,**不能上传插件**(用户侧「功能插件」section 只有启用/禁用,无上传入口)。
- folder_plugins 历史作用见 §四。
- **确认最佳方案后再实现**(先方案、后代码)。
## 三、插件三层归属模型(定稿)
| 层 | 入口 | 对象 | 受众 | 动作 |
|---|---|---|---|---|
| 自带插件 | dsh 设置 →「插件」(现有) | dsh 自带 cordis 148 | 仅 admin | 开关(已收归) |
| 功能插件·投放 | 门户 `plugins.html`(独立页面) | 系统外插件(业务 bundle) | 仅 admin | 上传 → 进候选池(默认禁用) |
| 功能插件·启停 | dsh 设置 →「功能插件」(新增) | 候选池内的功能插件 | admin + 用户 | 批量启用/禁用 → 确认弹窗 → 重启实例 |
**流程**:admin 投放(候选池,默认禁用)→ 用户在实例设置勾选启用 → 确认弹窗 → 复制插件到该用户 profile 目录 + 写 cordis patch → 重启该用户实例 → 生效。
## 四、folder_plugins 去留(已查明,建议废弃)
**历史作用(源码实锤)**:
- 档案 05 时期为"**按工作目录启停用户自装插件**"设计的预留机制。
- 实现:`folder_plugins` 表按 `workspace_id` 存 `plugin_id + enabled`;`POST /api/dsh/launch` 时 `renderPatch(enabled)` 生成 cordis `- insert:` patch(`dshs-runtime` + 勾选插件),经 `--patch` 传给子 dsh。
- **总开关 `enablePatch` 默认 `false`,线上未设 `DSHS_ENABLE_PATCH`(进程 env 已核实)→ 从未生效**。真正让 `portal-entry` 生效的是 profile bundles(`dsh plugin add`),与 folder_plugins 无关。
- 且"按工作目录"粒度与"每用户单实例"(ISOLATION=account)架构不匹配。
**结论**:**已确认废弃 folder_plugins**(含 `folder_plugins` 表、`/api/plugins` + `/api/plugins/select` 路由、desktop.html 插件面板),新模型直接走"复制 bundle 到 profile + 写 cordis patch + 重启"。
## 五、最终页面/入口地图(2026-09-10 SPA 化后)
> 门户管理面已合并为 **`portal.html` SPA**(hash 路由 + 顶栏面包屑,参考 mcn-work-shop 机制);`desktop.html` / `skills.html` / `plugins.html` 均重定向到 portal.html 对应路由。
| 侧 | 入口 | 受众 | 能力 |
|---|---|---|---|
| 门户 `portal.html` | `#/files`(文件 + DSH 启动) | 仅 admin | 文件树浏览 / 上传 / 新建 / 在此启动 DSH |
| 门户 `portal.html` | `#/keys`(密钥管理) | 仅 admin | 全局 API 密钥增删 / 启用 |
| 门户 `portal.html` | `#/users`(用户管理) | 仅 admin | 审批/禁用/删除 |
| 门户 `portal.html` | `#/skills`(技能管理) | 仅 admin | 共享技能上传/替换/删除 |
| 门户 `portal.html` | `#/plugins`(插件管理) | 仅 admin | 功能插件上传/替换/删除(候选池投放) |
| 实例 dsh「设置」 | 「功能插件」section(新增) | admin + 用户 | 批量启用/禁用候选池功能插件(**无上传**,确认弹窗 + 重启) |
| 实例 dsh「设置」 | 「我的技能」section(新增) | 所有用户 | 上传/管理自己的 skill(`/api/skills/mine`) |
| `login.html` | — | — | 清理错位"技能管理"链接 |
## 六、分阶段实施计划
| 阶段 | 目标 | 主要改动 | 红线 / 验证 |
|---|---|---|---|
| **0 零风险清理** ✅ | 消死链 + 废弃 folder_plugins | 删 `login.html` 错位链接;**废弃 folder_plugins**(删 `/api/plugins*` 路由 + desktop.html 插件面板;admin.html role 守卫实测已存在无需改;表定义/renderPatch 死代码保留) | 已验证:login/desktop 200、`/api/plugins*` 404、无残留;commit `bad6ed7` |
| **1 技能/插件独立页面** ✅ | admin 门户公共面 | `skills.html`(共享技能 `/api/skills/shared`)+ 新建 `plugins.html`(功能插件 `/api/plugins/business`);移除「我的技能」+ 加 role 守卫(仅 admin);上传接入安全检测 + 替换策略;**后续按 06 规范 Token + impeccable 做了 UI 优化、去退出登录** | 已验证:build 通过、端到端全通、表已建;commit `6f86a5f` + `b97c9f8` |
| **2 功能插件启停 section** ✅ | 实例侧启停 | 自建 client bundle `@dsh-local/business-plugins`(poc/business-plugins/)在 dsh 设置页注册「功能插件」section:候选池列表 + 批量勾选 + 确认弹窗(提示重启中断会话)+ 调 `/api/plugins/mine/apply` 启用/禁用 + 重启实例;门户加 CORS(仅 baseDomain 子域);启用=复制 tgz+手动解压到 profile node_modules+加 bundles(绕开 npm file: arborist bug) | R2/R3;已验证:mine 链路全通、CORS 子域 204/非法拒绝、bundle host 面加载(marker);commit `b7fd85d` |
| **3 「我的技能」section** | 实例侧个人技能 | 同自建 client 插件再注册「我的技能」section(或同一 bundle 两个 `settings.section`);上传表单调 `/api/skills/mine`;`dsh-client-ui-skill` 只读无上传,需自带表单 | R3;测试走 R4 |
| **4 权限收口复核** | 对齐设计意图 | 确认新 section 开放范围、不误伤 guest 已禁 `ui-settings-*` | 按 uid `--dump-config` 复核 |
## 七、实现细节(设计点定稿 + 技术要点)
### 7.1 功能插件设计点定稿(2026-09-10 用户拍板)
| # | 设计点 | 定稿 |
|---|---|---|
| 1 | 插件包格式 | 上传 **tgz** 文件(与 `dsh plugin add` 产物一致) |
| 2 | 候选池存储 | 新建 `business_plugins` 表(元数据)+ tgz 存磁盘 `.../plugins/<id>/` |
| 3 | 同名上传 | **替换策略(非覆盖)**:先删旧包再放新包,目标内容与新包**完全一致、无旧残留**;单版本,记 version |
| 4 | 启用机制 | 编排器自实现解压 tgz + 写 profile `dsh.profile.bundles`(不调 dsh CLI) |
| 5 | 禁用机制 | ~~保留包 + cordis patch `disabled: true`(不删 node_modules,便于快速重新启用)~~ **⚠️ 更正(2026-09-12,档案 64 §9.2 实测)**:实现在 `src/web/routes/business-plugins.ts` L399-402 / L410-413 —— 启用 = `pnpm add file:<tgz>`,**禁用 = `pnpm remove <id>`(真卸载,不是留包)**;因此任何「硬绑定 provider」的覆写段会在用户禁用后指向未注册的 provider(`WEB_PROVIDER_CONFIGURED_MISSING`,且 dsh 不回落)→ 已由**档案 65** 的「平台在 install/uninstall 后按当前 bundles 重算托管段」解决 |
| 6 | 上传安全检测 | **技能与插件上传都做**:结构校验(技能=SKILL.md 必填;插件=package.json + dsh bundle 结构)+ 危险内容扫描(命令注入 / 网络外联 / 越权文件读写等) |
| 7 | 投放范围 | 全员可见(先不做定向分组) |
> **替换策略 vs 覆盖策略(差别关键)**:覆盖 = 同名文件直接覆盖,旧包里已删除的文件会**残留**;替换 = 先删旧内容再放新内容,保证目标目录内容与新包**完全一致**。与技能管理 v2(档案 11)「apply 全量替换(旧目录整删、缺失文件一并清除)」同语义。
### 7.2 技术要点
1. **功能插件仓库(仅 admin 上传)**:门户新增上传 + 候选池(DB 表 + 磁盘 tgz);普通用户无上传入口。上传先过**安全检测**才进候选池。
2. **技能上传**:现有结构校验(档案 11 v2)之上,新增**危险内容扫描**。
3. **分发(启用)**:复制 bundle 到 `<root>/home/profiles/web/node_modules/<pkg>` + 写进 `dsh.profile.bundles`。
4. **生效**:改 profile 后走 `POST /api/dsh/restart` 重启该用户实例(按 uid 选 pid,勿 `pgrep -f '--profile'`)。
5. **确认弹窗**:批量勾选后弹确认(含「重启会中断当前会话」提示),点确认才执行复制 + 重启。
6. **安全**:功能插件 = 任意 node 代码,admin 投放即「给全员装代码」→ admin 可信为前提 + 上传安全检测 + 来源审计。
7. **技能 vs 插件语义区分**:技能「投放即生效」(只读共享层),插件「候选池默认禁用 + 按需启用」,勿混。
## 八、红线遵守
- **R1**:本方案不触发 dsh 升级。
- **R2**:改动限于门户静态页/编排器自建代码 + profile 层官方插件机制(`dsh plugin add`/bundles + cordis patch),**不改 dsh 主程序与缓存**。
- **R3**:阶段 2/3 client 插件严格镜像官方 bundle(只导出 `apply`+`inject`,无 default)。
- **R4**:验证用注册→admin approve→DELETE 模板,临时 admin session 标 `poc-curl` 用完即删,**禁用真实账号做 API 登录测试**。
- **UI**:所有页面改动先读 `06-工作台UI规范.md`,冲突以其实测 Token 为准。
> **【保留说明(2026-09-11,档案 19 §C5 便宜版)】** `folder_plugins` **表结构与 repo 函数保留,但已无任何业务/路由调用**(`grep` 实证:仅 `src/db/*` 自引用);`workspaces` 表仅被 `enablePatch` 分支使用,而 `DEFAULT_ENABLE_PATCH=false` 且生产 env 未覆盖 → **该分支在本部署不可达**。保留原因:k8s/PG 路径未验证(档案 19 §C8),删除风险不对称。**请勿往这两张表/该分支加新功能。**