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 一律写「远程服务器」。
16 KiB
T01 · 档案 16 阶段 3/4 —— 实例内「我的技能」
- 日期:2026-09-11
- 状态:✅ 已完成(2026-09-15 执行会话
T01我的技能-0825落地;档案 100;插件business-plugins0.3.21 已投放并验收)- 阶段 3(实例内入口)= ✅ 完成,且形态按 2026-09-15 修正为并入「功能管理」内分组(见 §一 横幅 / §四 决策 6 / §九)
- 阶段 4(权限收口复核)= ✅ 收口:本单未新增 section、未改
ensure-role-profile-patch.cjs⇒ 用户可见性不变,不存在"新 section 的开放范围"问题(详 §八 回报)
- 来源:
04-调整方案/16-页面导航定稿-技能插件管理面三决策落地.md §六阶段 3 / 阶段 4 - 规划会话边界:本单由规划会话产出(未 ssh、未改码);执行会话按单开工
一、目标
让普通用户在实例内能自助管理个人技能(上传 / 启用 / 禁用 / 删除),并在 dsh 设置「功能管理」页内看到「我的技能」分组;随后完成阶段 4 的权限收口复核。
做完的判定:普通用户(guest)在实例内 dsh 设置 →「功能管理」里看到「我的技能」分组,能上传一个 zip 技能并完成 启用 → 禁用 → 删除 全流程,且共享技能行显示为锁定(不可操作)。
⚠️ 2026-09-15 形态修正:原述为「新增一个「我的技能」
settings.section」;现改为并入既有「功能管理」section 内分组。理由与边界见 §四 决策 6 与 §九。
二、只读前置(开工前先核实,别推断)
| # | 要核实的事实 | 命令 | 期望 |
|---|---|---|---|
| 1 | 后端 API 确实已就绪(档案 41 §实现 B) | ssh bt-server "git -C /opt/dshs ls-files | grep -E 'routes/skills' " 再读该文件 |
存在 GET/POST /api/skills/mine、POST /:name/enable、POST /:name/disable、DELETE /:name,全部 requireAuth |
| 2 | 现有 client bundle 的 section 注册写法(照抄形态,勿自创) | ssh bt-server "ls -l /opt/dsh/artifacts/ | grep -i biz" + 本机 04-调整方案/poc/business-plugins/ |
拿到 lib/client.js 的 settings.section 注册片段与 package.json 的 dsh.client 元数据 |
| 3 | 铺开链路入口(新用户自动 + cron) | ssh bt-server "git -C /opt/dshs ls-files | grep -E 'ensure-biz' " 再读该脚本 |
scripts/ensure-biz-plugins.cjs(幂等、版本感知、自动取最新产物) |
| 4 | 实例页注入的既有脚本仍在(别撞车) | ssh bt-server "grep -c '__dshRecover|__dshAssist' /opt/dshs/src/supervisor/proxy.ts" |
≥2(档案 50 / 56 两个注入并存) |
| 5 | 本次是否需要重启实例 | 取决于 #1:#1 通过则本单不改服务端,改的是 bundle → 需 POST /api/dsh/restart 或让用户在确认弹窗触发 |
— |
判定口径提醒(重要,来自档案 18 v3 的教训):
dsh --dump-config不反映 bundle / profile patch 层 —— 连已生效的portal-entry/business-plugins行都不显示。因此不能拿--dump-config当验收口径(档案 16 §六 阶段 4 原文的这句写法已过时)。改用「bundle 加载标记(看 journald)+ 实例页 UI 实测」。
三、范围
要改(路径以 git -C /opt/dshs ls-files 与 04-调整方案/poc/ 实际为准):
| 区 | 文件 | 动作 |
|---|---|---|
| client bundle | .../client.js |
在既有「功能管理」settings.section(id business-plugins)内新增「我的技能」分组(不新开 section,见 §四 决策 6):列表(含 source/enabled/locked)+ 上传表单 + 启用/禁用/删除按钮 |
| bundle 版本 | package.json |
必须升版本号(pnpm 同版本会命中缓存 → 改了不生效,档案 18 踩坑 1) |
| 加载标记 | .../lib/index.js |
加一行 [<bundle>] loaded …(把回归从"看 UI"变成"看日志";档案 26 §建议 / B5) |
| 网关/路由 | 无需改(除 #1 发现 bug → 另立单) | — |
明确不动:
- ❌ 不动 dsh 官方包与缓存(红线 2);❌ 不改
src/web/routes/skills.ts的鉴权语义 - ❌ 不动门户
#/skills(admin 面);❌ 不动workspace-scoped-picker/portal-entry - ❌ 不碰
folder_plugins/enablePatch分支(档案 16 §八 保留说明:无业务调用,勿加功能) - ❌ 不扩大任何权限(API 已是
requireAuth,本单只是加 UI)→ 不触发 R5 权限扩大门禁
四、决策点
| # | 决策 | 结论 |
|---|---|---|
| 1 | 扩展 @dsh-local/business-plugins 注册第二个 section,还是新建 @dsh-local/my-skills |
✅ 已定(2026-09-12 用户拍板):采用 A —— 扩展 @dsh-local/business-plugins 注册第二个 section(用户原话「扩展 business-plugins 不用做两套」)。不新建 my-skills 包。理由:复用现成的「铺开 / 探活 / 版本感知 / 新用户自动铺 + cron 巡检」链路,成本最低;代价是 business-plugins 职责由一个变两个(在包 README 与本档案里说明即可)。 |
| 2 | 上传是否复用门户的 P0/P1 内容扫描 | 已定(必须复用):上传走平台 root 链路,档案 41 的 symlink P0 就是这类。UI 只负责把 zip POST 到 /api/skills/mine,扫描在服务端(档案 41 §A / 档案 19 §C6) |
| 3 | admin 是否也看到这个 section | 已定(档案 16 §五):所有用户(含 admin)都显示 |
| 4 | 是否顺手补 B5(给两个 bundle 加加载标记) | 已定(顺手做):零边际成本,正是本单要改的文件(03-路线图 B5 行已降级为"顺手做") |
| 5 | 是否同时补门户 #/files 下载按钮(P3) |
不在本单(另立) |
| 6 | 「我的技能」是并进既有「功能管理」section,还是新开一个 section | ✅ 已定(2026-09-15,自决,可推翻):并入「功能管理」内分组,不新开 section。依据 ① 用户口径「不要分开管理」② 该分区已由档案 60 命名为**「功能管理」**——另立「我的技能」= 在"功能"之外再造一个平行概念,与口径反向 ③ 素材库 U2「不做两套实现,能扩展就不新建」④ 两者交互形态同构(列表 + 启停)。代价:一个 section 承载两组内容(决策 1 已认下)。⛔ 仅入口层合并 —— 机制层保持分离:两条后端 API、两套落盘位置、两种生效方式都不合并(见 §九) |
五、步骤(每步自带验证)
- 核实前置:#1–#4 逐条执行;若 #1 缺端点 → 停,回报"后端未就绪",不要自己补后端。
- 读规范:
06-工作台UI规范.md(强制基线)+档案 16 §七设计点;参考04-调整方案/poc/business-plugins/现有 section 的写法与 token 用法。 验证:能指出新 section 用到的设计 token 来源(--dsw-*)。 - 写 UI(决策 1 选定的载体):列表字段 = 名称 / 来源(shared·user)/ 状态(enabled·disabled)/ 锁定标记;操作 = 上传(zip)、启用、禁用、删除;共享行
locked:true→ 按钮禁用。 验证:本地npm run build(或该 bundle 的构建命令)通过。 - 升版本 + 打包 + 安装:
pnpm add file:<tgz>,HOME=<ws>必须设,且 guest 的 profile 是 pnpm workspace 根需加-w、admin 不需要(档案 25 踩坑)。产物放/opt/dsh/artifacts/,不要复制进用户 ws(档案 28 的平台污染 bug)。 验证:ls确认lib/client.js与lib/index.js都在(档案 18 踩坑 2:scp 静默失败);比对 md5。 - 重启 + 探活:走编排器
POST /api/dsh/restart(按 uid 选 pid);或复用restartAndProbe的探活口径(档案 34)。 验证:journalctl -u dshs --since '5 min ago'出现[<bundle>] loaded,且无duplicate loader entry id(档案 25 事故特征)。 - 端到端实测(用 R4 模板:注册 → admin approve → 用完 DELETE,临时会话
poc-*用完即删;禁止用真实账号做 API 登录测试):上传 → 禁用 → 启用 → 删除。 验证:见 §六 B–F。 - 阶段 4 权限收口复核:确认新 section 对 guest 可见、且没误伤 guest 已禁的
ui-settings-*裁剪(档案 09 / 16 §六 阶段 4)。 验证:用实例页实测 + 加载标记判定(不要用--dump-config,见 §二 提醒);输出一份"开 new section 前后 guest 可见 section 列表"对照。 - 归档:新建档案(原子占号
mkdir 04-调整方案/.lock-<NN>;当前下一号见README的「下一号」);更新INDEX.md §二对应行状态、03-路线图勾销阶段 3/4 行、README下一号;git mv本单到archive/交接单-已完成/。 验证:python3 scripts/docs-audit.py退出码 0。
六、验收标准(可被第三方复现)
| # | 命令 / 动作 | 期望 |
|---|---|---|
| A | ssh bt-server "cd /opt/dshs && bash scripts/ci.sh" |
退出码 0,单测全绿 |
| B | 硬刷新 guest 实例子域页 → dsh 设置 →「功能管理」 | 页内出现「我的技能」分组(与「功能插件」同页),列出共享技能(显示锁定)与个人技能 |
| C | 上传一个正常 zip 技能 | 200,列表出现该项且 enabled;$DSH_HOME/skills/<name> 存在 |
| D | 点禁用 → 点启用 → 点删除 | 禁用后文件在 home/skills-library/<name>;启用后回到 skills/;删除后两处均无 |
| E | 上传与共享技能同名的包 / 对锁定行操作 | 409(档案 41 的守卫生效) |
| F | 实例页仍含两个注入脚本 | 页面源码含 __dshRecover 且 含 __dshAssist |
| G | journalctl |
有 [<bundle>] loaded;无 duplicate loader entry id、无崩溃重启 |
七、回滚
- bundle 回退:装回上一版本 tgz(版本号必须不同,否则 pnpm 命中缓存)→ 重启实例;或让该 section 不注册。
- 本单不改服务端 → 服务端无需回滚;若 #1 阶段确实改了服务端,按
cp <file>.bak-<ts> <file> && npm run build && systemctl restart dshs。 - 确认无残留:
ls <profile>/node_modules/@dsh-local/与 profile 的dsh.profile.bundles一致。
八、回报格式(执行会话填)
## T01 执行回报
- 决策 1 选定:A · 扩展 business-plugins(2026-09-12 用户拍板);形态修正为「并入既有「功能管理」内分组」(2026-09-15,见 §四 决策 6)
- 档案号:100
- 插件版本:`@dsh-local/business-plugins` 0.3.20 → **0.3.21**(产物 70,758 B / sha256 `77429d2c…`)
- 验收:A ✅ / B ✅ / C ✅ / D ✅ / E ✅ / F ✅ / G ✅(**逐项证据见下**)
- 偏离:① 形态由「新增 section」改为「并入既有 section 分组」(有依据,见 §四 决策 6);② **新增** `scripts/verify-my-skills.mjs` 并纳入 `npm run verify`(原单未要求,但把"不换行 / 危险操作二次确认 / 锁定行无按钮"钉成断言可防回退);③ 未改动服务端(同原单预期)
- 证据:
· A 本地 `npm run verify` 全绿(词典 zh/en 各 366 键、引用键 343 个全部已声明)
· B 两实例 profile 均 0.3.21;`msk.group` 命中 3 / `bp-skillRow` 命中 4
· C 壳页 combo 出现 `@dsh-local/business-plugins`;壳页 58,126 B → 58,724 B;`rev=533537bbc02b`
· D bundle HTTP 200 · **11,363,655 B** · 含 `msk.group` / `msk.replaceWarn` / `bp-skillRow` / 「我的技能」
· E 端到端 **19/19**(一次性用户,用完即删):上传 → 列表 enabled → 同名 `conflict`+`stagedId` → `apply` 全量替换 → 停用(文件落 `skills-library`、不在 `skills`)→ 启用(回到 `skills`)→ 删除(两处皆无、列表消失)
· F 对共享技能 `platform-capabilities` 的 disable / delete **均 409**;非 `.zip` **400**「仅支持 .zip 文件」
· G 浏览器(`agent-browser`)**全链路零缺陷**:设置→「功能管理」可见「我的技能」分组(与「功能插件」同页);共享技能行显示 `共享|🔒只读|已启用|1 个文件 · 2.9 KB` 且**无动作按钮**;展开上传面板 → 选 `poc-skill-demo.zip` → 点「上传」= `POST /api/skills/mine` **200** 且页面提示「✓ 已上传技能…」、列表新增 `我的|已启用|3 个文件 · 575 B|[停用][删除]`;点「删除」出确认弹窗(点名 + "不可恢复")→ 确认 = `DELETE` **200**、提示「✓ 已删除…」、列表回到只剩共享行。截图与逐条证据见档案 100 §8.4
- 阶段 4 复核:本单**未新增 section**(形态 = 既有 section 内分组)⇒ 段位可见性零变化;`ensure-role-profile-patch.cjs` **未改**(我的改动清单 = client.js / package.json×2 / verify-my-skills.mjs);实例侧 `cordis.patch.yml` 的禁用项(`ui-settings-models` / `-settings-plugins` / `-plugin-inventory` / `ui-cordis`)原样保留
九、口径依据与适用范围(2026-09-15 追加,本单形态修正的由来)
问题:「把 skill 和插件都看成功能(能力),统一管理」这条提法,适用于用户管理个人 skills 吗?
9.1 原文与该提法的适用范围
- 原话(2026-09-12 09:18,会话
ca58e09f):「你是方案规划 不是执行,在明确一下 A 按照最佳方案处理 B 按照你的建议处理 !业务技能能否打包到插件中 一起安装和使用,不要分开管理! C D 按照最优方案处理」 - 它落地成什么 = 投放链路的合并,即
archive/交接单-已完成/T03§4.1「技能随插件包投放」:整合包内置skills/+skills/.manifest.json,插件 host 面每次实例启动跑ensureSkills()幂等对账;与共享技能层互斥(同名会按 rank 抢)。对应素材库 U4「交付面越少越好」。 - 适用对象 = 平台/业务侧投放的技能(MCN 这类)。判据内核 = 用户心智「我装一个插件就全有了」,优于"分层更干净"。
9.2 对「用户管理个人 skills」——分两层,结论相反
| 层 | 是否适用 | 依据 |
|---|---|---|
| 机制 / 投放层 | ❌ 不适用 | 个人技能不是投放物,是用户自己上传的产物 —— 没有"第二条投放链路"可合并,也没有"插件包"能装它;硬套会把「用户自己产生的东西」塞进「平台投放的包」,方向反了。而且两向隔离已被现决策刻意设计:T03 §4.1 第 4 条明确「不覆盖用户自建的同名技能(目录存在但无 .installed.json)→ 跳过 + 日志 + UI 提示"被同名技能占用"」。⇒ 二者必须保持两条独立通道 |
| 入口 / 心智层 | ✅ 适用 | 正是本单形态修正的原因(§四 决策 6):实例内那一栏已由档案 60 命名为**「功能管理」**,再另立一个「我的技能」section = 在"功能"之外再造平行概念,与「不要分开管理」反向 |
9.3 ⛔ 不能一起"统一"掉的机制差异(避免过度统一)
| 维度 | 功能插件(候选池) | 个人技能(用户自传) |
|---|---|---|
| 来源 | admin 投放(门户 → 候选池) | 用户自己上传 zip |
| 生效方式 | 需重启实例 | watch 即时生效,无需重启 |
| 默认态 | 进池默认禁用 | 上传即启用 |
| 平台共享层的语义 | 投放进池、用户自助启用 | 投放即生效且锁定只读(不造开关 —— 档案 16 §2.1 决策 2「技能与插件启用语义不同」) |
| 用户可否删除 | 可禁用(包由平台管) | 可启用 / 可禁用 / 可删除(档案 41 用户决策) |
| 落盘位置 | profile node_modules + dsh.profile.bundles |
$DSH_HOME/skills 启用位 / home/skills-library 禁用位 |
| 后端 API | /api/plugins/* |
/api/skills/{shared,mine}/* |
⇒ 本单"合并"的只有可点击的那一层;后端、落盘、生效链路一律各自独立。
9.4 相关档案
04-调整方案/16(导航定稿 + 三层归属 + §2.1 决策 2)· 04-调整方案/41(技能上传安全加固 + 用户启停)· 04-调整方案/60(分区改名「功能管理」)· 04-调整方案/11(技能管理面)· archive/交接单-已完成/T03 §4.1(技能随包投放)· skills/dsh-decision-method/references/素材库-U-用户决策.md(U2 / U4 / U10)。