回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复: - 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录) - tmp/(32.4 M,按接续棒命名的过程临时区) - .workbuddy/tmp/(39.5 M) - 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物) - tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留 入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与 接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、 .workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。 排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、 打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
19 KiB
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 登记;双端同步 |
七、关键机制与约束
- installed/enabled 两态分离:install=复制+chown+bundles;enable=enabled 集(DB)+ relaunch;disable=移除 enabled 集 + relaunch(副本保留)。
- relaunch 语义:只重算 patch(enabled∩installed ∩ 已装过滤同 launch route)并带新 patch 重启,不落库不改 profile 配置;复用既有 spawn/waitForLaunchToken 流程。
- chown 纪律:复制后 chown 用户 uid/gid(先顶层再子项,档案 11 教训);共享库/审查区 root 600;检测全程只读。
- 确认弹窗必含:插件清单 + "将安装到你的个人配置并重启你的 dsh 实例,当前会话可能中断"。
- 并发写锁:按用户 id 单写锁(install/enable/disable 串行化);launch 与 relaunch 天然互斥(mains 检查)。
- 重启中状态:relaunch 期间页面置"重启中",防"未重启仍见旧 bundle"误判(poc v0.4.3)。
- 红线补充:不写/不改 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。