Files
dsh_shenxian/dsh-server-docs/archive/工作区草案/插件管理面_调整方案草案_20260910.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

206 lines
19 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.
# 13-插件管理面(修订 v5:入库审查门禁 + 用户自选启用,Step 0 核查结论已回填)
> 状态:**方案草案 v5,待用户拍板 3 个决策点后实施**|2026-09-10|对象:dshs + dsh 0.1.2-rc.1
> v3 范围(07:57):普通用户可查看 admin 上传的全部插件,卡片多选 →「启用」弹窗确认 → 才复制进用户 profile → 重启该用户 dsh 实例后生效。
> v4 范围(08:00):admin 上传一律先进审查区,过「安全检测 + 多用户检测」无 P0/P1 才可发布入库;用户选择页仅展示 published。
> v5 范围(08:1x,Step 0 服务器实测回填):**平台已自带部分插件机制**(见 §四),本方案收敛为在其上新增四件事——admin 全局库+审查门禁、用户拉取式安装(复制进 profile)、启用后**带新 patch 立即重启**(新增 relaunch)、插件选择页。启用状态源修正为 DB `folder_plugins`(非 profile patch disabled 行)。
> 评审稿:`D:\AI技能\aliyun-dsh-server\插件管理面_调整方案草案_20260910.md`;核对后定稿 `04-调整方案/` 档案 13 双端同步。
## 一句话结论
平台已能"列出用户 profile 已装插件 + 按工作区 folder 勾选启停(launch 时渲染 patch 生效)",且有桌面端「插件」面板(desktop.html);**缺的是上游**:admin 的全局插件库与入库审查门禁、用户把库插件**安装**进自己 profile 的入口、启用后**立即生效**(现有机制只在下次 launch 生效)。本方案补上这四块:admin 上传 → 审查区(安全 S1-S10 + 多用户 M1-M9,P0/P1 阻断,报告留痕)→ 发布入库;用户在「插件选择页」浏览 published 库 → 卡片勾选(批量)→「启用」弹窗确认 → 后端 安装(复制进 profile + 追加 bundles;已装则跳过)→ 写入当前工作区 enabled 集 → **relaunch(重算 patch 并重启实例)** → 生效。
## 一、角色与流程
| 操作 | admin | 普通用户 |
|---|---|---|
| 查看已发布插件库(选择页) | ✅ | ✅ |
| 上传插件(进审查区,≠入库) | ✅ | ❌ |
| 检测/出报告(安全 + 多用户) | ✅(自动 + 复核终审) | ❌ |
| 发布入库 / 打回需改造 | ✅ | ❌ |
| **安装**(从库复制进自己 profile) | ✅ | ✅(选择页,随启用自动) |
| **启用**(= enabled 集 + relaunch) | ✅ | ✅ **核心动作** |
| **禁用**(= 移出 enabled 集 + relaunch) | ✅ | ✅ |
| 删除/移除已发布包(全局) | ✅ | ❌ |
**插件入库时序(admin)**:同 v4(upload → under_review → 自动检测 S/M → 无 P0/P1 + admin 复核 → release → published)。
**用户启用时序(v5 精确流程,对齐平台真实机制)**:
```
1. 打开「插件选择」页 → GET /api/plugins/library?folder=<当前工作区>
→ published 全量 × 本人状态:未安装 / 已安装-未启用 / 已启用(本 folder)
2. 勾选卡片(可批量)→ 点「启用」
3. 弹窗确认:插件清单 + 「将安装到你的个人配置并重启你的 dsh 实例,当前会话可能中断」
4. POST /api/plugins/library/enable {names:[...], folder}
5. 后端 per 插件:
a. 未安装 → 从 published 库复制包目录 → profile/node_modules/<pkg>
+ chown 用户 uid/gid(先顶层再子项)+ profile package.json dsh.profile.bundles 追加
(冲突预检:短 id/路由/官方名/同名不同版本 → 409)
b. enabled 集(DB folder_plugins,按 folder workspace)写入该插件 id
c. orchestrator.relaunch(user):按该 folder 重算 patch = renderPatch(enabled∩installed)
→ 以新 patch 重启 main + watchdog(复用既有 launch 的计算逻辑,抽公共函数)
6. 返回 → 页面状态刷新 = 已启用;工作台出现插件入口
```
**禁用时序**:勾选已启用插件 → 「禁用」→ 确认 → enabled 集移除该 id → relaunch → 入口消失(profile 副本保留,再次启用免复制秒级恢复)。
## 二、总体机制
```
admin 上传 ─▶ 审查区 under_review(dataRoot/plugin-reviews,用户不可见,root 600)
│ 自动静态检测 S1-S10 + M1-M9 → 报告(P0/P1/P2)
├─ P0/P1 → needs_fix 需改造 ─▶ 改造重传复检
└─ 无 P0/P1 + admin 复核 ─▶ release ─▶ published 库
│ dataRoot/plugin-library(root 600 只读)
▼
插件选择页 plugins.html(仅 published)
│ enable = 安装(复制进 profile)+enabled集+relaunch
▼
用户 userRoot=dataRoot/users/<uuid>/
├─ home/profiles/web/package.json ← dsh.profile.bundles(installed 源)
├─ home/profiles/web/node_modules/<pkg> ← 复制落点(chown 用户)
├─ home/profiles/web/cordis.patch.yml ← role patch,勿动(ensure-role-profile-patch.cjs 管)
└─ patches/main.yml ← launch/relaunch 渲染的 --patch(DB enabled 派生)
▼ relaunch(新 patch)→ dsh --profile web 重启 → 生效
```
- **installed 与 enabled 是两态**:installed = profile 已含该 bundle(复制+声明);enabled = 当前工作区 folder 的 DB enabled 集(决定 launch patch 是否注入)。桌面端既有「插件」面板只做 enabled 勾选(下次 launch 生效,不重启);选择页做"库浏览 + 安装 + 启用(立即 relaunch)"。
- **共享库 = 源,profile = 副本,enabled 集 = 状态**;禁用 ≠ 卸载(副本保留)。
- 用户选择页仅 published;审查区/报告仅 admin 可见。
## 三、入库审查门禁(安全检测 + 多用户检测)— 同 v4
**放行规则**:canRelease = 无 P0 且无 P1;P2 不阻断留档;admin 人工复核为 release 必选终审;检测只读扫描、不执行包内脚本;报告 JSON 留痕(root 600,按版本保留)。
**安全检测 S1-S10**:
| # | 检查内容 | 级别 | 整改建议 |
|---|---|---|---|
| S1 | 归档:恰一个顶层目录;无 `..`/符号链接逃逸/设备文件 | **P0** | 重打包 |
| S2 | package.json 合法;`dsh.bundle.patch` 存在且指向存在文件 | **P0** | 补清单 |
| S3 | `@deepseek-ai/*`、官方同名 | **P0** | 改名(红线 2) |
| S4 | install/postinstall 等 npm 脚本 | **P0** | 移除 |
| S5 | exec/spawn、动态 eval/Function、vm 逃逸 | P1 | 说明+复核;纯函数化 |
| S6 | http(s) 出网非白名单 | P1 | 登记域名+用途 |
| S7 | 混淆/隐藏文件/超大二进制(>5MB 说明) | P1 | 去混淆可审计 |
| S8 | fs 写绝对路径/越出插件目录/触碰他人 profile | **P0** | 改注入的相对路径 |
| S9 | chown/root 级写/监听 0.0.0.0 | P1 | 移除,编排器统一管 |
| S10 | client 硬编码 token/密钥/内网地址 | P1 | 走 profile 密文配置 |
**多用户检测 M1-M9**(针对单用户 dsh 开发假设):
| # | 检查内容 | 级别 | 整改建议 |
|---|---|---|---|
| M1 | 硬编码绝对路径/固定配置位置 | P1 | 注入的相对路径/dataDir |
| M2 | 独占固定端口/0.0.0.0 | P1 | 实例内路由/动态端口 |
| M3 | 共享外部服务单槽(VoxEMW current_session 教训) | P1 | 无状态/云 API/多槽 |
| M4 | 持久化写到 profile 外共享全局文件 | **P0** | profile 内 JSONL |
| M5 | 短 id/路由/`__mcnEntries` 键与已发布或审查中冲突 | **P0** | 登记表比对改唯一 |
| M6 | 模块级可变单例跨租户串用 | P1 | 随实例或按用户键 |
| M7 | 无锁写共享资源/假定唯一活跃会话 | P1 | 单写锁/编排器串行 |
| M8 | 大模型/GPU/长驻进程等资源声明 | P2 | 卡片标注,admin 视容量放行 |
| M9 | 假定热加载/无需重启 | P2 | 明示需重启(页面已含提示) |
## 四、Open Questions 前置核查结论(Step 0 服务器实测,2026-09-10)
| # | 结论(实测事实) | 对实现的影响 |
|---|---|---|
| OQ1 | **profile 布局 ✅**:dataRoot=`/var/lib/dshs`(env `DSHS_DATA_ROOT`);`userRoot=dataRoot/users/<uuid>`(owner=每用户 uid/gid,如 `dsh-eeccbc...`/100002);profile=`userRoot/home/profiles/web`(MAIN_PROFILE `web`);installed 源=profile `package.json` 的 `dsh.profile.bundles`;包实体=`profile/node_modules/<pkg>` | 复制落点=该 profile node_modules;写后 chown 用户 uid/gid(先顶层再子项) |
| OQ2 | **复制式安装未实证,待 dry-run**:现 profile node_modules 无 `@deepseek-ai/*` 实体(未运行/由 dsh CLI 首启安装);listInstalledPlugins 按 `node_modules/<pkg>/package.json` 读 description | 实现后用无害测试插件在测试用户上 dry-run:复制 → listInstalledPlugins 可见 → relaunch 生效;若 pnpm/安装逻辑清掉手放目录 → fallback `dsh plugin add`(走 CLI) |
| OQ3 | 新用户 profile 由 spawn 首启 `dsh --profile web` 创建(09-09 建号已见 profiles/web + node_modules 目录骨架) | 复制目标不存在 → ensureDir + 最小骨架 package.json(bundles:[])+ chown;极端情况走 OQ1 错误引导 |
| OQ4 | **✅ 有用户域重启端点但语义修正**:`POST /api/dsh/restart {command}`(requireAuth,操作 `request.user.id`)调用 `orchestrator.restartMain`,但 **restartMain 复用内存里的旧 patch,只能重启不能换插件集** | **新增 `orchestrator.relaunch(userId)`**:按当前 main 的 folder 从 DB 重算 patch(与 launch route 同逻辑,抽公共函数)→ 以新 patch 重启 main + watchdog。enable/disable 内部调用它,不新增公开端点亦可 |
| OQ5 | **✅ 平台已有插件启停面(桌面端)**:`desktop.html`「插件」面板调 `GET /api/plugins?folder=`(已装 + 每 folder enabled)与 `POST /api/plugins/select`(存 DB `folder_plugins`),**无重启动作,下次 launch 生效**;backend 已注册 `pluginRoutes`(09-08 底座)。官方 dsh 会话内设置是否另有插件分区:无运行实例未实证,UI 验证阶段补查 | 双 UI 定位:选择页 = 库浏览+安装+启用(立即 relaunch);桌面面板 = 已装插件快速勾选(保留)。desktop 加提示"装新插件请到插件选择页";若官方会话设置确有启停分区,再加互链(记录为验证项,不阻塞 v1) |
| OQ6 | **✅ 状态源修正**:installed = profile `dsh.profile.bundles`(listInstalledPlugins 现有实现);enabled per folder = DB `folder_plugins`(workspaces 按 user+folder 键)。**草案旧假设"profile cordis.patch.yml 写 enabled/disabled 行"不适用**;且 profile 现存 cordis.patch.yml 是 role patch(`ui-settings-models` disabled,ensure-role-profile-patch.cjs 管理)——**勿手改** | enabled 一律走 DB 集操作(getOrCreateWorkspace + setFolderPlugins / getEnabledPluginIds 已存在) |
| OQ7 | 冲突检测:复制时按 name 比对 profile 已装版本(bundles + node_modules 包 version);库 release 时同名同版本拒绝覆盖 | 409 version-conflict;卡片显示"已装 vX,库 vY" |
| 附加 | launch route 已实现 patch 计算(`renderPatch(enabled∩installed)`)但为内联逻辑;`renderPatch` 恒注入 `dshs-runtime`(supervisor/patch.ts) | 抽 `patchForLaunch(...)` 公共函数,launch/relaunch 共用,行为零变化 |
## 五、API 设计
统一基址 `/api/plugins`;鉴权 requireAuth / requireAdmin。**既有 `/api/plugins` GET 与 `/api/plugins/select` 保留不动**(桌面端在用)。
**用户(新)**
| 接口 | 行为 |
|---|---|
| `GET /api/plugins/library?folder=` | published 全量(name/version/desc/hasClient/hasServer/reviewedAt/pkgSize)+ 本人状态 `{state: none\|installed\|enabled, installedVersion}` |
| `POST /api/plugins/library/install` | body `{names:[], folder?}` → 仅安装(复制+chown+bundles 追加),不启停 → `{ok, installed:[...]}`(供"仅安装稍后启用") |
| `POST /api/plugins/library/enable` | body `{names:[], folder?}` → 未装则先安装 → 写 enabled 集 → **relaunch** → `{ok, enabled:[...], restarted:true}` |
| `POST /api/plugins/library/disable` | body `{names:[], folder?}` → 移除 enabled 集 → relaunch → `{ok, disabled:[...], restarted:true}` |
**Admin(新)**
| 接口 | 行为 |
|---|---|
| `POST /api/plugins/admin/reviews` | 上传进审查区 → 自动检测 → `{reviewId,name,version,status,report}` |
| `GET /api/plugins/admin/reviews` | 审查区+库全条目(status/版本/报告摘要/P0·P1 计数/历史) |
| `GET /api/plugins/admin/reviews/:id` | 详情 + 最新报告全文 |
| `POST /api/plugins/admin/reviews/:id/release` | 发布入库;有 P0/P1 → 409 has_blockers;同名同版本 → 409 version-conflict |
| `POST /api/plugins/admin/reviews/:id/reject` | 打回 needs_fix + `{comment}` |
| `DELETE /api/plugins/admin/reviews/:id` | 丢弃审查条目 |
| `DELETE /api/plugins/admin/library/:name` | 移除已发布包(用户副本不受影响) |
| `GET /api/plugins/admin/users` | 只读矩阵:每用户 installed/enabled 状态 + 版本 |
错误码:`400 invalid_plugin_name / unsupported archive type / archive must contain exactly one top-level dir / missing package.json(dsh.bundle) / reserved-package(@deepseek-ai/*) / archive member escapes root`、`404 not_found`、`409 already_enabled / has_blockers / version-conflict / id-conflict`。
上传校验规则同 v4(恰一顶层目录、dsh.bundle.patch 必在、防穿越、禁 @deepseek-ai/*;校验通过 ≠ 入库)。
## 六、改动文件清单(服务器 /opt/dshs,备份 `*.bak-YYYYMMDD-HHMM`)
| 文件 | 改动 |
|---|---|
| `src/config.ts` | 加 `pluginLibraryDir`(默认 `<dataRoot>/plugin-library`)、`pluginReviewDir`(`<dataRoot>/plugin-reviews`) |
| `src/fs/plugins.ts` | 扩展:`installToProfile`(复制+chown+追加 bundles)、`isInstalled`/`installedVersion`(OQ7 比对) |
| `src/fs/plugin-library.ts` | **新增**:published 库读写/列表/删除(root 600 语义) |
| `src/lib/pluginScan.ts` | **新增**:静态检测引擎(S1-S10+M1-M9 + 分级报告,只读不执行) |
| `src/supervisor/orchestrator.ts` | **新增 `relaunch(userId)`**:重算 patch + 带新 patch 重启 main + watchdog;launch 内 patch 计算抽公共函数(行为零变化) |
| `src/web/routes/archive.ts` | **新增**:解压+穿越校验+chown 公共 helper(skills/plugins/reviews 复用) |
| `src/web/routes/pluginLibrary.ts` | **新增**:用户库(list/install/enable/disable → 内部 relaunch) |
| `src/web/routes/pluginReviews.ts` | **新增**:admin 审查生命周期 + 库 CRUD + users 矩阵 |
| `src/web/server.ts` | register 新增路由 |
| `web/plugins.html` | **新增**:用户=published 库卡片多选+启用/禁用+确认弹窗(含"将重启实例");admin=审查区(状态/报告/发布/打回)+库管理+矩阵 |
| `web/desktop.html` | 加「插件选择」入口 + 面板内提示"装新插件到插件选择页" |
| `web/login.html` / `admin.html` | 入口(admin 视图) |
| `lib/**` | `npm run build`(tsc) |
| 文档 | 本档案 + `03-路线图与待办.md` 登记;双端同步 |
## 七、关键机制与约束
1. **installed/enabled 两态分离**:install=复制+chown+bundles;enable=enabled 集(DB)+ relaunch;disable=移除 enabled 集 + relaunch(副本保留)。
2. **relaunch 语义**:只重算 patch(enabled∩installed ∩ 已装过滤同 launch route)并带新 patch 重启,不落库不改 profile 配置;复用既有 spawn/waitForLaunchToken 流程。
3. **chown 纪律**:复制后 chown 用户 uid/gid(先顶层再子项,档案 11 教训);共享库/审查区 root 600;检测全程只读。
4. **确认弹窗必含**:插件清单 + "将安装到你的个人配置并重启你的 dsh 实例,当前会话可能中断"。
5. **并发写锁**:按用户 id 单写锁(install/enable/disable 串行化);launch 与 relaunch 天然互斥(mains 检查)。
6. **重启中状态**:relaunch 期间页面置"重启中",防"未重启仍见旧 bundle"误判(poc v0.4.3)。
7. **红线补充**:**不写/不改 profile 既有 `cordis.patch.yml`(role patch,ensure-role-profile-patch.cjs 管理)**;不动官方 bundle 与 dsh 主程序缓存;不自动升级 dsh。
## 八、验证清单
**API(curl)**
- [ ] 无 sid → 401;用户访问 admin 接口 → 403
- [ ] admin 上传 → under_review;用户 GET library 不含该包
- [ ] 恶意包:`..` 成员/@deepseek-ai/install 脚本 → P0 拒收;exec 动态串 → P1 → release 409 has_blockers;needs_fix 报告可查
- [ ] 多用户包:硬编码绝对路径(M1)/写 profile 外共享文件(M4)→ 命中打回
- [ ] 改造重传 v+1 → 复检 → release → published → 用户 library 出现(reviewedAt)
- [ ] 同名同版本 release → 409 version-conflict
- [ ] enable 单/多:安装副本入 profile(chown 正确)+ bundles 追加 + enabled 集写入 + relaunch;`restarted:true`
- [ ] 未装包 enable = 自动先 install;重复 enable → 409 already_enabled;disable → 集移除 + relaunch;再 enable 免复制
- [ ] OQ2 dry-run:复制式安装后 listInstalledPlugins 可见、relaunch 后入口生效;不符 → 切 CLI fallback 并记录
- [ ] 报告落盘 pluginReviewDir root 600;删除已发布包后已装用户不受影响
- [ ] 回归:`GET /api/plugins` 与 `/api/plugins/select`(桌面在用)行为不变;profile cordis.patch.yml 未被改写
**UI(浏览器,admin + 用户)**
- [ ] 用户:选择页仅 published 卡片(未安装/已安装/已启用角标)→ 多选 2 → 启用 → 弹窗文案正确 → relaunch → 工作台两插件入口出现(bundle rev 对比)
- [ ] 禁用 → relaunch → 入口消失;桌面「插件」面板勾选仍可用(下次 launch 生效)
- [ ] admin:上传 → 审查状态流转 → 报告 → 发布 → 用户端可见;矩阵正确
- [ ] 回归:技能管理面(档案 11)不受影响;官方 @deepseek-ai/* 任何视图不出现
## 九、风险
| 级别 | 风险 | 缓解 |
|---|---|---|
| P1 | relaunch 打断当前会话(单活跃实例) | 弹窗强提示 + 确认;relaunch 复用既有 spawn 流程,失败自动回退(watchdog 机制兜底) |
| P1 | 复制式安装与 profile 的 pnpm/dsh 安装逻辑冲突(手放目录被清/不可解析) | OQ2 dry-run 先行;不符即切 `dsh plugin add` CLI fallback |
| P1 | 静态检测为启发式,无法证明绝对安全/绝对多用户 | 人工复核必选终审 + 来源明确 + 报告留痕;沙箱试运行列为增强 |
| P2 | 与桌面既有插件面板/官方会话设置双 UI | 定位分工 + 页面互链提示;验证项补查官方会话设置 |
| P2 | 用户装多个大插件占 profile 空间 | 库卡片展示体积;副本体积纳入 admin 矩阵 |
| P2 | 审查放行依赖人工复核效率 | 自动覆盖硬规则(S1-S4/S8、M4/M5),人工只复核启发式项 |
## 十、红线遵守
不自动升级 dsh;不改官方主程序与缓存(只操作 plugin-library/plugin-reviews + 用户 profile 层);拒绝 `@deepseek-ai/*` 入库;上传包不执行任何脚本;**不改 profile 既有 cordis.patch.yml(role patch)与 ensure-role-profile-patch.cjs 产物**;共享库/审查区 root 600。