# T01 · 档案 16 阶段 3/4 —— 实例内「我的技能」 - 日期:2026-09-11 - 状态:✅ **已完成**(2026-09-15 执行会话 `T01我的技能-0825` 落地;档案 **100**;插件 `business-plugins` **0.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` | 加一行 `[] 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. **核实前置**:#1–#4 逐条执行;若 #1 缺端点 → **停**,回报"后端未就绪",不要自己补后端。 2. **读规范**:`06-工作台UI规范.md`(强制基线)+ `档案 16 §七` 设计点;参考 `04-调整方案/poc/business-plugins/` 现有 section 的写法与 token 用法。 *验证*:能指出新 section 用到的设计 token 来源(`--dsw-*`)。 3. **写 UI**(决策 1 选定的载体):列表字段 = 名称 / 来源(shared·user)/ 状态(enabled·disabled)/ 锁定标记;操作 = 上传(zip)、启用、禁用、删除;共享行 `locked:true` → 按钮禁用。 *验证*:本地 `npm run build`(或该 bundle 的构建命令)通过。 4. **升版本 + 打包 + 安装**:`pnpm add file:`,**`HOME=` 必须设**,且 **guest 的 profile 是 pnpm workspace 根需加 `-w`、admin 不需要**(档案 25 踩坑)。产物放 `/opt/dsh/artifacts/`,**不要复制进用户 ws**(档案 28 的平台污染 bug)。 *验证*:`ls` 确认 `lib/client.js` 与 `lib/index.js` **都在**(档案 18 踩坑 2:scp 静默失败);比对 md5。 5. **重启 + 探活**:走编排器 `POST /api/dsh/restart`(按 uid 选 pid);或复用 `restartAndProbe` 的探活口径(档案 34)。 *验证*:`journalctl -u dshs --since '5 min ago'` 出现 `[] loaded`,且**无** `duplicate loader entry id`(档案 25 事故特征)。 6. **端到端实测**(用 R4 模板:注册 → admin approve → 用完 DELETE,临时会话 `poc-*` 用完即删;**禁止用真实账号做 API 登录测试**):上传 → 禁用 → 启用 → 删除。 *验证*:见 §六 B–F。 7. **阶段 4 权限收口复核**:确认新 section 对 guest 可见、且**没误伤** guest 已禁的 `ui-settings-*` 裁剪(档案 09 / 16 §六 阶段 4)。 *验证*:用**实例页实测 + 加载标记**判定(**不要用 `--dump-config`**,见 §二 提醒);输出一份"开 new section 前后 guest 可见 section 列表"对照。 8. **归档**:新建档案(**原子占号** `mkdir 04-调整方案/.lock-`;当前下一号见 `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/` 存在 | | D | 点禁用 → 点启用 → 点删除 | 禁用后文件在 `home/skills-library/`;启用后回到 `skills/`;删除后两处均无 | | E | 上传与共享技能同名的包 / 对锁定行操作 | **409**(档案 41 的守卫生效) | | F | 实例页仍含两个注入脚本 | 页面源码含 `__dshRecover` **且** 含 `__dshAssist` | | G | `journalctl` | 有 `[] loaded`;无 `duplicate loader entry id`、无崩溃重启 | ## 七、回滚 - **bundle 回退**:装回上一版本 tgz(版本号必须不同,否则 pnpm 命中缓存)→ 重启实例;或让该 section 不注册。 - **本单不改服务端** → 服务端无需回滚;若 #1 阶段确实改了服务端,按 `cp .bak- && npm run build && systemctl restart dshs`。 - **确认无残留**:`ls /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)。