Files
dsh_shenxian/dsh-server-docs/04-调整方案/16-页面导航定稿-技能插件管理面三决策落地.md
T

120 lines
12 KiB
Markdown
Raw Normal View History

# 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),删除风险不对称。**请勿往这两张表/该分支加新功能。**