Files
dsh_shenxian/dsh-server-docs/archive/交接单-已完成/T03-plugin_package整合为单插件并投放.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

346 lines
41 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.
# T03 · 把 `plugin_package` 7 个自研插件整合为 1 个功能插件并投放
- 日期:2026-09-12
- 状态:✅ **已完成并归档(2026-09-13 18:0x)** —— 投放链路全通(上传 → admin 装 → **guest 启用** → 实例重启探活成功);主体验收通过。收尾记录见 **§十**
> ⛔ **2026-09-13 09:4x 现场取证:这一步已被试过,但被平台自动回滚了。**
> guest 在 **07:42:52** 通过「功能管理」提交了启用(`POST /api/plugins/mine/apply`,任务 `c8ac2686539aeb7c`),
> 随后实例 **07:43–07:45 连崩 5 次**(`exitCode=134`,栈 = V8 `JsonParser`/`HeapAllocator` ⇒ **V8 堆打满 abort**),
> 07:46 触发熔断,平台**自动回滚**(`package.json.bak-rollback-20260913T0748`)⇒ 现在 guest bundles 里**没有** mcn-suite。
> **根因指向 `--max-old-space-size=160` 太紧**(档案 74 的下调)+ guest cgroup 峰值已 **382/384 MiB**
> ⇒ **先把内存预算调上去(需重启窗口),再重试启用**。详见 `04-调整方案/74` 文末「现场反证」。
**原状态行**:⏳ **待续做(2026-09-13 07:3x 复核更正)** —— **步骤 1–9 已完成,且「上传候选池」亦已完成**(现行池内 = `dsh-plugin-mcn-suite @0.3.9`,09-13 00:02 上传;**admin 侧已装 0.3.9 且 host 面 3 个注册加载通过**),**剩**:**`guest` 侧在实例「功能管理」启用**(`POST /api/plugins/mine/apply` —— **用户动作,平台不代劳**)→ 重启实例 → §八验收 → 归档。**2026-09-12 14:55 已释放占用锁**(原 `exec-session-B`),接手者**直接续做**;产物与验收证据见 `交接单/README.md §一` 的 T03 行。
> **🔎 只读核查结论(2026-09-12 20:2x)—— 执行前的三个事实**
> ① **候选池里只有 `dsh-univer-office`(39.9 MB)** ⇒ §七 防线②"下架旧 7 包"**实际无需做**:那 7 个包**从未上架过候选池**,不存在新旧并存。
> ② **guest 的 `profile/web` bundles = `base / dsh-web-app / business-plugins / portal-entry / workspace-scoped-picker / @liustack/modlens / dsh-univer-office`** ⇒ **不含任何旧包** ⇒ §七 头号风险(新旧并存 → `duplicate loader entry id` 崩实例)**在本单不成立**,切换顺序可简化。
> ③ **时机未到**:guest 活动实测 —— `ws/MCN短视频创作` mtime = **20:06**、`sessions` mtime = **20:02**(距核查 13–17 分钟)⇒ 判定**活跃中**,按 **R8「能避开活跃时段就避开」** 暂缓"启用 + 重启"。上传(零影响)与启用(断 guest ~1 分钟)**同窗口做更安全**(避免为"上传"单独占一次 op-lock),故**整体等待空闲窗口**(判据:最近 30 分钟无活动)。
>
> ⚠️ **同窗口须一并处理**:guest 的 bundles 里仍挂着 **`@liustack/modlens`** —— 该包今天已从候选池下架(档案 70 收尾),但用户 profile **仍启用它**(包文件尚在 `node_modules/@liustack/modlens`,故当前不致命)。这是"**已下架却仍启用**"的不一致态 ⇒ **与 T03 共用同一次重启窗口摘除**(从 bundles 移除 + 重启),避免两次中断用户。
### ✅ 上线进展(2026-09-12 22:0x)
| 步 | 结果 |
|---|---|
| ① 首传 0.3.0 | ❌ **被 T05 兼容性预检拒收**(HTTP 409 `compat_incompatible`)—— 判据 `@deepseek-ai/dsh-client-ui-sidebar@^0.1.0-rc.5` vs 平台 `0.1.2-rc.1`。**根因是 semver 的 prerelease 规则**:带 prerelease 的版本只能被「**同 major.minor.patch 且带 prerelease**」的比较器匹配,`^0.1.0-rc.5` 里没有 tuple `(0,1,2)` 的比较器 ⇒ 判不满足。**不是误报,是声明写窄了**(且 T05 16:35 才上线,而包 11:15 打的,单子当时没这道关) |
| ② 修正 + 重传 | ✅ **HTTP 200**,`compat.level = ok`、`findings[]` 空。改法:3 个 `dsh-client-*` peer 范围 → **`^0.1.2-rc.1`**(跟随平台版本);版本号 **0.3.0 → 0.3.1**;`npm pack` 重打包 → md5 **`adcb134e…`** / 322 条目。**内容与 0.3.0 逐项对齐**:`SKILL.md` 18=18、`lib/` 文件 19=19、`web/` 1=1(条目数差异纯属 `npm pack` 不写目录条目) |
| ③ 候选池 | 现 **2 条**(09-13 07:3x 复核):`dsh-plugin-mcn-suite @0.3.9`(09-13 00:02;admin 已装并验通,**guest 未启用**)+ `dsh-univer-office @0.2.15`(09-12 23:19,fork 版)|历史:曾为 `@0.3.1` + `@0.2.14`,`@liustack/modlens` 与 `anysearch` 已下架 |
| ④ 剩余 | **`guest` 在实例「功能管理」里启用**(`POST /api/plugins/mine/apply` —— **用户动作,平台不代劳**)→ 重启 → §八 验收 → 归档。~~同窗口一并摘 `@liustack/modlens`~~ **已消解**(09-13 07:0x 实测:guest `bundles` 与 `node_modules` 均已无该包)|
> ⚠️ **给接手人的两条提醒**
> 1. **上传前必过兼容性预检**(`POST /api/plugins/business` 会 409);`trust` 只用于"已人工确认后放行",别拿它盖住声明问题。
> 2. **peer 版本范围必须跟随平台 dsh 版本**(当前 `0.1.2-rc.1`)—— 平台升版时同步改,否则 prerelease 规则会再次拒收。插件源码**并不 import** 这些包(只靠 `dsh.client.inject` 注入点),所以放宽范围无功能风险。
- 触发:用户「**规划 `D:\dshworkspace\plugin_package` 整合成一个插件,可打包后由 admin 整体上传到 dsh 服务器插件,用户可以启用进行使用**」
- 关联:档案 16(插件三层归属 + 投放/启停流程)、27(MCN 插件平台化评估,P0 清单来源)、34(启用探活 + 快照回滚)、36/38(铺开链路)、41(技能启停 API)、58(实例内存配额)、60(分区改名「功能管理」)、19 §C6 + 41(上传安全扫描)
- 规划会话边界:本单由**规划会话**产出,只读普查了本机 `D:\dshworkspace\plugin_package`(未 ssh、未改码)
---
## 一、目标
把 `D:\dshworkspace\plugin_package` 下的 **7 个自研插件整合成 1 个功能插件包**:admin 在门户一次性上传候选池 → 用户在实例设置「**功能管理**」分区启用一次 → 重启实例 → **全部功能可用**。
**做完的判定**:一个**全新用户**启用该整合包并重启后,侧边栏入口 + 工作台各功能页逐个可用,journald 无 `duplicate loader entry id`、无崩溃循环;**并且业务技能随包自动就位**(见 §4.1)—— `$DSH_HOME/skills/` 出现 `mcn-short-video` 与 4 个业务子技能,**用户不需要另外上传或启用技能**。
## 二、现状(2026-09-12 只读普查结论)
| 包 | loader id | 体积 | client 行 | host 行 | 角色 |
|---|---|---|---|---|---|
| `dsh-plugin-mcn` | `mcn-nav` | 945K | **3045** | 3095 + 9 模块 | **宿主壳**(唯一声明 `dsh.client.inject=[runtime, sidebar]`) |
| `dsh-plugin-douyin-accounts` | `douyin-accounts` | 55K | 666 | **9(空壳)** | 功能页 |
| `dsh-plugin-douyin-account-detail` | `douyin-account-detail` | 67K | 877 | **9(空壳)** | 功能页 |
| `dsh-plugin-douyin-video-detail` | `douyin-video-detail` | 39K | 496 | **9(空壳)** | 功能页 |
| `dsh-plugin-mcn-schedule` | `mcn-schedule` | 54K | 253 | 483 | 功能页 + 定时任务 |
| `dsh-plugin-social-workbench` | `social-workbench` | 37K | 150 | 218 | 外部服务守护(**Windows 本地**) |
| `dsh-plugin-voxemw-cloud` | `voxemw-cloud` | 77K | 134 | 233 + 6 模块 + `web/app.html` | 云 API 面板 |
### 2.1 结构性结论(**这是"必须整合"的真正理由**)
**它不是 7 个平行插件,而是「1 个宿主壳 + 6 个功能页」。**
- `dsh-plugin-mcn` 注册了 **25+ 条 `/mcn/api/*` HTTP 路由**、`inject = ["slots","sessions"]`、在 `sidebar.footer.action` 注册唯一入口,并提供两个**全局扩展点**:
- `window.__mcnEntries` —— **功能页注册表**(工作台首屏图标网格读它渲染,源注释写明"外部插件注册的功能入口,即插即用")
- `window.__mcnNav` —— **页内路由**(`open(entryId, params)` / `openView(id)` / `back()`)
- 其余 6 个包**全靠往 `__mcnEntries` 注册 + 被 `__mcnNav.open()` 打开**;三个 douyin 包的 host 面只有 **9 行空壳**,注释原文写着"数据复用 `dsh-plugin-mcn` 服务端注册的 `/mcn/api/*` 路由"。
- ⇒ **拆开的状态下"逐个启用"本身就违背设计**:缺了 mcn,其余 5 个只是死链;反过来只启用 mcn 则功能残缺。**整合是回归正确形态,不只是图省事。**
- 附注:`dsh-plugin-mcn` **没有** `defineTool` / `tools.register`(`grep` 实证 = 0)→ 它是 UI + HTTP 插件,**不给 agent 注册工具**;agent 侧能力只来自技能与 prompt。
### 2.2 打包链路现状:**不存在,要新建**
7 个包均**无 `scripts` 字段、无 build 脚本、无 `package/` 目录、无 tgz 产物**;`lib/*.js` 是**直接维护的产物**(client.js 顶部自带 `var module = {exports:{}}` 的 bundle 外壳)。→ 整合包必须**新建打包脚本**。
### 2.3 需要一并处置的残留
`dsh-plugin-mcn/lib/` 内有 **2 个 `.bak`**(`index.js.bak-20260906`、`index.js.bak3-20260906`)→ 会被打进包、加重体积并可能触发上传扫描告警。`docs/ui-check-home-fix/`(1.6M)与插件无关,不进包。
## 三、范围
| 区 | 对象 | 动作 |
|---|---|---|
| 新建 | `D:\dshworkspace\plugin_package\dsh-plugin-mcn-suite\`(**7 个原包原地保留**作回滚参照) | 整合包源码 |
| 新建 | 整合包内 `skills/`(**技能原文 + `.manifest.json`**,来源 `C:\Users\Administrator\.dsh\skills\mcn-short-video`,11 MB / 711 文件) | 随包投放的业务技能,**按 §4.1 取舍表挑选**(不整包照搬) |
| 新建 | 打包脚本(`scripts/build-mcn-suite.cjs` 或等价) | 产出 `dsh-plugin-mcn-suite-<ver>.tgz`(**tgz 内需 `package/` 前缀**,档案 25/28 教训) |
| 改 | 整合包内的 host/client 代码 | §五 的 P0 适配 |
| 平台侧 | 门户候选池 | 上传新包 + **下架旧 7 包**(防并存,见 §七) |
| 文档 | 新建档案 + `INDEX §二` + `03-路线图` + `交接单/README §一` | 归档 |
**明确不动**:❌ dsh 官方包与缓存(红线 R2)❌ 平台自建三 bundle(`portal-entry` / `business-plugins` / `workspace-scoped-picker`)❌ `/opt/dsh` 权限 ❌ 7 个原包源码(保留)❌ 不新增平台侧权限能力(不触发 R5)
## 四、决策点(**已由用户 2026-09-12 拍板**)
> 用户口径:「**A 按照最佳方案处理;B 按照你的建议处理 —— 业务技能能否打包到插件中一起安装和使用,不要分开管理!;C、D 按照最优方案处理**」
| # | 决策 | 定稿 |
|---|---|---|
| 1 | `dsh-plugin-social-workbench` **进不进包** | ✅ **不进包**(采纳建议 A)。实测是 **Windows 本地服务守护**:`EASEL_DIR` 默认 `D:\dshworkspace\Easel`、可执行文件 `.venv/Scripts/easel.exe`、探针打 `127.0.0.1:7860`/`:18789`;平台实例里 ① 该 exe 不存在 ② `127.0.0.1` 已被档案 39 封(实测 BLOCKED)→ **必然不可用**。该入口在平台不投放,原包源码保留 |
| 2 | `dsh-plugin-voxemw-cloud` 进不进包 | ✅ **进包**。凭据全程走 env(**无硬编码** ✓、`getPublicConfig` 已剥离 apiKey ✓),平台实例无该 env → 入口显示「未配置」,不崩 |
| 3 | 整合后的入口形态 | ✅ **A=保留 `__mcnEntries` 机制、多入口并列**(工作台首屏图标网格),client 面行为与现状一致 |
| 4 | **业务技能投放** —— 用户明确要求 **打进插件包、一起安装使用、不分开管理** | ✅ **改为「技能随包投放」**,实现见 **§4.1**。**这推翻了规划会话原先"走门户技能管理分开投"的建议**,据此重设计 |
| 5 | 包名与版本 | `dsh-plugin-mcn-suite` **v0.3.0**。**版本号必须每次递增**:同名同版本 tgz 即使内容变了,pnpm 也复用旧包(档案 18 踩坑 1) |
### 4.1 技能随插件包投放(**用户要求:不分开管理**)
**为什么可行**:技能发现位在**实例内** —— `dsh discoverRoot` 只扫 `$DSH_HOME/skills`(档案 41 §B 实测:启用/禁用靠目录 rename 即可**即时生效、无需重启**);而插件的 host 面代码**就跑在实例内、以该用户 uid 运行** → **插件完全有能力自己把技能装到位**,不需要平台技能管理面参与。
**机制(建议,执行会话按此实现)**:整合包内置 `skills/`(技能原文)+ `skills/.manifest.json`(技能名 → 版本 / 文件数 / sha256)。host 面在**每次实例启动**调用 `ensureSkills()`:
1. 读 `.manifest.json`;对每个技能比对 `$DSH_HOME/skills/<name>/.installed.json` 记录的版本;
2. **幂等**:版本一致 → 跳过(不重复写盘);
3. 版本不一致 → **全量替换**(先删后放,旧版已删除的文件一并清除 —— 与档案 11「apply 全量替换」同语义);
4. **不覆盖用户自建的同名技能**(目录存在但无 `.installed.json`)→ 跳过 + 写日志 + UI 提示"被同名技能占用";
5. 写加载标记 `[mcn-suite] skills ensured: [email protected], …`(回归看日志即可,档案 26 §建议)。
**备选(更省事,但必须先实测)**:若 dsh 的 skill 扫描**认得目录软链**,可改用软链 `<DSH_HOME>/skills/<name>` → `<profile>/node_modules/dsh-plugin-mcn-suite/skills/<name>`,**升级插件即升级技能、零对账**。风险:symlink 是否被扫描/读盘正常要**实测**(档案 39 的教训:symlink 行为必须 realpath 级实测,别假设)。→ 执行会话先试软链,不行再退回"复制 + 版本对账"。
**投哪些 / 不投哪些 —— 按「引用驱动」裁剪,不按"看着像资料"裁**(用户 2026-09-12 定「参考资料不需要」;规划会话随后实测引用关系,**发现三类"像资料"的其实是被引用的功能件**,故按下表执行):
| 内容 | 体积 | 引用实测 | 定稿 |
|---|---|---|---|
| 主技能 `mcn-short-video`(`SKILL.md` + `references/` + `references-add/` + `scripts/` + `帮助文档.md`) | ~1.3 MB | — | ✅ **投** |
| 子技能 `mcn-dou-analysis` / `mcn-script-review` / `mcn-video-prompt` / `mcn-data-insight` 主体 | 主体 | 插件 prompt 正是引用这 4 个 | ✅ **投** |
| **`mcn-data-insight/` —— 它本身是"技能集索引",下挂 12 个二级子技能**(`douyin-top-account`(140K) / `douyin-account-diagnosis`(136K) / `douyin-rise-ranking`(76K) / `douyin-hot-trend`(72K) / `douyin-ai-feed` / `douyin-prohibited-word` / `playlet-douyin-feed` / `douyin-weekly-surge` / `douyin-works-crawler` / `douyin-content-surge` / `douyin-daily-hot` / `douyin-search`,**全部带 `SKILL.md`**) | **869 KB** | 插件的「榜单更新」任务依赖其中的 `douyin-top-account`;`workshop.js` 的赛道白名单同源 | ✅ **整块投**(这是"技能树"的第二层,**不是参考资料**;`find -maxdepth` 容易漏它,核对时注意层级) |
| `mcn-dou-analysis/references/样例/`(`模板_旧梦留声机账号设定卡.svg` 19 KB + `参考_旧梦留声机_账号设定卡_长图版.png` **994 KB**) | 992 KB | **`references/feature/06_生成账号设定卡片.md:67` 写明"动手前必须先读"这两份** → **功能件,不是资料** | ✅ **保留**。用户说的"参考资料不需要"**不覆盖这一项**;若你仍要剔,则必须同改那条指令,否则留下死引用 |
| `nuwa-skill-main/` **主体**(去掉 `examples/`) | **104 KB / 11 文件** | **被引为"主方法论"**:`feature/03_提炼账号设定.md:81`、`references/人设卡生成方法.md:186` | ✅ **保留** |
| `nuwa-skill-main/examples/`(`trump-perspective/` 英文研究文档等) | ≈ **2.4 MB** | `人设卡生成方法.md:187-188` 引了 `examples/mrbeast-perspective/`(**未引 `trump-perspective`**) | ❌ **剔**。精确做法:只剔未被引的 `trump-perspective/`;若要连 `mrbeast-perspective` 一起剔,**必须同改那两行引用** |
| `mcn-video-prompt/参考skills/`(seedance2.0-prompt-skill、script-writing-studio 上游全文) | **2.0 MB** | **实测 0 引用**(`grep` 命中 0)→ 纯资料 | ❌ **剔** ✓ —— 用户"参考资料不需要"指的就是这类 |
| `browser-harness/`(整包 Python + 自带 venv + docs/png) | **2.3 MB** | **多篇 feature 要求"浏览器优先"**(`feature/01_获取账号信息.md:12`、`feature/02_获取视频.md:10`)+ 主 `SKILL.md` 有专章(L88/105-112) | ❌ **剔目录**,但**必须同步改文档**:写明「平台无浏览器(档案 38a 实测 Playwright 装不起来)→ 改走 MCP / 其它路径」,否则留下死引用,agent 会像档案 55 那样反复撞墙 |
| `scripts/mcp-config.json` | 16 KB | 插件 MCP 依赖(功能必需部分 = URL) | ⚠️ **剔 `MYAI_FEISHU_APP_ID`(`cli_a84d0a47f93e1013`)、保留 URL**(`maiya-trans.youmanvideo.com`);再按 §五#7 过扫描 |
**裁剪后体积预期**:11 MB → **≈4.3 MB**(剔掉 6.7 MB:`参考skills` 2.0 + `browser-harness` 2.3 + `nuwa/examples` 2.4)。
⚠️ **投的是整棵技能树,不是一个技能**:全树 **36 个 `SKILL.md` / 711 文件**(主技能 1 + 一级子技能 5 + `mcn-data-insight` 下 12 个二级子技能 + 更深层的参考技能目录)。**核对层级时别用浅 `find`**(规划会话首轮就是这么误判 `douyin-top-account` "缺失"的)。
**裁剪必须机械校验(新增,防"剔完留死引用")**:剔完跑一次**死引用扫描** ——
```bash
grep -rIn -E "参考skills|browser-harness|nuwa-skill-main/examples/trump|参考_旧梦留声机" <整合包>/skills/ \
| grep -v -E "已移除|无浏览器|不再使用"
```
期望:**只剩"已移除 / 已改走 X"这类说明行**(即上面要求改写的指令),**没有一条指向已删文件的活引用**。
**⚠️ 与共享技能层互斥**:本单走"插件内投"后,**不要再把 `mcn-short-video` 投到平台共享技能层**(`bundledSkillDir`,档案 10/40)—— 两条通道同名会按 rank 抢,行为不可预测。详见 §七。
**禁用 / 卸载语义(取舍,已定)**:插件被禁用(`disabled: true`)时**不删**已装技能(用户可能正在用),只在下次启用时按版本对账;**卸载插件**会留下技能残留 → 记为已知残留(清理走实例内「我的技能」或目录手工删)。
### 4.2 技能落地方式(装到哪)与撞名规则(2026-09-12 用户提问定稿)
**问题一:装到 `$DSH_HOME/skills/`,还是"就在插件内目录使用"?**
关键事实:**dsh 的技能扫描根是一个固定集合**(个人 `~/.dsh/skills` rank300 / 项目级 `.dsh/skills` rank100 / `.agents/skills` rank200 / 平台注入的 `bundledSkillDir`,档案 10 §Skill 装载机制 / 档案 41 §B),**插件包目录(`profile/node_modules/<pkg>/skills/`)不在其中**。
⇒ **"就在插件内目录使用"意味着 agent 无法按技能名加载**(skill 机制发现不了),只能靠 prompt 给绝对路径让它自己读 `SKILL.md`。
| 方案 | 能否按技能名加载 | 副本/维护 | 备注 |
|---|---|---|---|
| **A 复制**到 `$DSH_HOME/skills/<name>` | ✅ 能 | 每用户一份 ≈4.3 MB;需版本对账 | 最简单确定;用户能在「我的技能」看到(但归插件管) |
| **B 软链** `$DSH_HOME/skills/<name>` → 插件包内目录 | ✅ 能(**前提:实测 dsh 跟随 symlink**) | **零副本**;**升级插件即升级技能**,零对账 | 副作用:用户在「我的技能」里删/禁 → 断链或删链,需处理;**必须先实测**(档案 39 教训:symlink 行为必须 realpath 级实测,不许假设) |
| **C 不落扫描根**,prompt 给"包内绝对路径"(插件运行时用 `import.meta.url` 算出) | ❌ 不能 | 零副本零对账 | 兜底方案:agent 用不了 skill 名;用户不可见、不可管理;每次 prompt 都要注入路径 |
→ **定稿:B 优先(一条实测命令即可确认);B 不通退回 A;C 仅作最终兜底。**
**问题二:同名技能怎么处理(三层,按优先级)**
| 场景 | 处理 | 理由 |
|---|---|---|
| 插件技能 ↔ **用户自建**同名 | **让位**:不覆盖,跳过 + 日志 + UI 提示"被你的同名技能占用" | 用户的东西优先;档案 41 已有"同名 409"的先例,不静默覆盖 |
| 插件技能 ↔ **平台共享技能层**同名 | **互斥**:本单不投共享层;若共享层已有同名,插件让位并提示 | 共享层 rank 更高会压住个人层,静默失败比明说更糟(§4.1 互斥条) |
| 插件技能 ↔ **另一个插件**同名 | **owner 标记 + 先到先得 + 冲突报错**:每份由插件装的技能写 `.installed.json`(`owner: <包名>@<版本>`、`skillVersion`、`sha256`);安装前查目标目录 —— 无标记=用户自建→让位;`owner`=自己→按版本对账;**`owner`=别的插件→拒绝写 + 明确提示** | 多插件共存时行为可预测、冲突可见;**不建议给技能名加命名空间前缀**(要改所有 prompt,且用户看到怪名字) |
**判定顺序(实现时按此写)**:`目标目录不存在` → 装;`存在且 .installed.json.owner == 本包` → 比版本,不同则全量替换;`存在但无 .installed.json` → 用户自建,让位;`存在且 owner == 其他包` → 拒绝 + 报错。
## 五、平台化 P0 清单(**不修则"传上去也不能用"**)
| # | 问题 | 实测位置 | 改法 |
|---|---|---|---|
| **1** | **`homedir()/.dsh` 硬编码** —— 平台实例的 DSH 家目录是 `$DSH_HOME`,不是 `~/.dsh` | `mcn/lib/config.js:6,28,68`;`mcn/lib/index.js:733,771,865,881,935,964,978,1124,1167,1496,1565,1630,1969,2143,2840`;`mcn-schedule/lib/index.js:11,27,84` | 统一 `const DSH_HOME = process.env.DSH_HOME` → `join(DSH_HOME, …)`。**注意三处子路径**:`storages/workspace.json`、`sessions/`、`profiles/`,都在 DSH_HOME 下 |
| **2** | **技能路径硬编码** `~/.dsh/skills/mcn-short-video/subskills/<x>/` | `mcn/lib/index.js` 多处:1255、1565、1798、1813、1834、2078、2313、2364、2491、2546、2984、2986… | 改成**只报技能名**(`mcn-dou-analysis` 等),由 agent 自行解析(档案 27 P0-2);技能**随包投放**(见 §4.1),prompt 里不再出现任何绝对路径 |
| **3** | **`spawn("powershell", …)` 杀进程树 = Windows only** | `mcn/lib/index.js:919`(两个 `.bak` 同位置) | 平台是 Linux → 改 `process.kill(-pid, …)` / `pkill -P`,或直接删掉该清理分支(评估残留影响) |
| **4** | **MCP `spawn(npx -y myai-mcp)` 每次现拉包** | `mcn/lib/mcp.js:21`、`config.js:14` | 平台侧**预装成包**(档案 27 P1),并确认实例内 **npm registry 可达**(档案 38a:仅云元数据端点被封,其余放行但只记日志) |
| **5** | **`.bak` 混在 lib/** | `mcn/lib/index.js.bak-20260906`、`index.js.bak3-20260906` | 打包前剔除(构建脚本过滤 + `files` 白名单) |
| **6** | **上传扫描可能误伤**(P0 阻断 / P1 告警) | 整包 | **打包后先本地 dry-run `src/web/security-scan.ts` 的规则**(P0:`/etc/shadow`、base64 解码执行、`nc -e`、`/dev/tcp` 反弹、fork 炸弹、给系统二进制加 setuid;P1:`child_process`、`vm`/`eval`/`new Function`、敏感 env、超长 base64、外部网络、隐藏目录)。已知 `child_process`(mcn spawn、social-workbench spawn)只会触发 **P1 告警、不阻断**,但要确认整包 **P0 = 0**。**注意技能原文里有大量代码示例/脚本,P0 风险比插件本身高** |
| **7** | **技能原文里的凭据 / 平台情报**(随包投放新增) | `mcn-short-video/scripts/mcp-config.json`(实测含 `MYAI_FEISHU_APP_ID=cli_a84d0a47f93e1013` + 私域 URL `maiya-trans.youmanvideo.com`)、`scripts/MCN_CYLG_API.py`(命中 `key/token` 模式,**需逐个核对**)、`subskills/browser-harness/.env.example` | 入包前对 `skills/` 整目录做**凭据扫描**:`grep -riE "api[_-]?key|access[_-]?token|secret|password|passwd" skills/` → **真密钥一律剔除**(改 env 注入);非密钥的平台情报(app id、私域域名)按最小必要原则剔除;结论写进档案 |
| **8** | **插件 prompt 里的技能路径层级不符**(2026-09-12 复核,**更正规划会话先前的误判**) | 插件 prompt 写的是 `subskills/douyin-top-account`(`mcn/lib/index.js:3085`、`3086`;`mcn/lib/workshop.js:9,22`),**真实路径是 `subskills/mcn-data-insight/subskills/douyin-top-account/`**(少了一层)。技能本身**完整存在**(36 个 `SKILL.md` / 711 文件)—— 规划会话首轮的"缺失"结论是**搜索深度不够(`-maxdepth 5` 差一层)导致的误判,已更正** | 无需补文件;**并入 #2 一起修**:所有 prompt 改成**只报技能名**(`douyin-top-account`),由 agent 自己解析 → 层级问题自然消失。**打包前必跑**:`grep -rn "subskills/" lib/` 逐条核对是否与技能实际层级一致 |
## 六、步骤(每步自带验证)
1. **前置只读核查**:`git -C <平台代码> log -1`(取当前 HEAD 备写档案)/ `ls /opt/dsh/artifacts/`(看版本命名与 `package/` 前缀)/ 门户 `#/plugins` 候选池现有 7 包的实际状态(是否有人已启用)。
*验证*:拿到"候选池里有哪几个、谁在用"的事实清单。
2. **决策与技能完整性**:§四 五个决策点已定稿;**另需向用户确认 §五#8 —— `douyin-top-account` 子技能的去向**(补进包 / 已废弃改 prompt / 走 MCP)。**该项未确认前不要打包**。
3. **建包骨架**:`package.json`(name `dsh-plugin-mcn-suite`、version、`type: module`、`exports {".", "./client"}`、`dsh.bundle.patch`、`dsh.client.inject` = mcn 的 `[runtime, sidebar]` 并集)+ `cordis.patch.yml`(**只 1 条 `insert`**:`id: mcn-suite`)+ `lib/{index.js, client.js, host/*}`。
*验证*:`node --check` 全通过;`cordis.patch.yml` 里 insert 条目数 = 1(防 duplicate,档案 25)。
4. **移植 host 面**:把 mcn 的 9 个模块 + mcn-schedule + voxemw(+决策 1 的 social-workbench) 收进 `lib/host/`,**保持路由前缀不变**(`/mcn/api/*`、`/voxemw/api/*`)→ client 面零改动。
*验证*:路由清单 diff:改前 25+ 条 vs 改后逐条一致(只增不减)。
5. **合并 client 面**:一个 `client.js` 内注册全部 `__mcnEntries` 条目;`inject` 取并集;**确认无 `exports.default`**(红线 R3,实测 7 个原包全部合规 ✓,合并时别引入)。
*验证*:`grep -c "exports.default"` = 0。
6. **P0 适配**(§五 1–5)+ 按需加**加载标记**:`console.log("[mcn-suite] loaded …")`(把回归从"看 UI"变成"看日志",档案 26 §建议)。
*验证*:`grep -n "homedir()" lib/**/*.js` = 0;`grep -n "powershell"` = 0;`grep -rn "\.dsh/skills"` = 0。
7. **本地 smoke**:参考 `dsh-plugin-voxemw-cloud/tests/smoke.mjs` 的写法,跑一遍 host 面(路由注册、DB 建表幂等、无 env 时降级不崩)。
*验证*:smoke 全绿;无 env 情况下进程不抛。
8. **打包**:产出 `dist/dsh-plugin-mcn-suite-<ver>.tgz`,**内含 `package/` 前缀**,**不含 `.bak` / `node_modules`**。
*验证*:`tar tzf` 清单人工核对(顶层只有 `package/`);体积与 `du` 对照;`md5sum` 记档。
9. **扫描 dry-run**:按 §五#6 规则跑一遍 → **P0 必须为 0**;P1 告警逐条判断是否误报。
10. **上传**:admin 门户 `#/plugins` 上传 tgz → 进候选池(默认禁用)。**产物不要复制进用户 ws**(档案 28 平台污染教训)。
*验证*:候选池列表出现该包 + 版本号正确。
11. **切换(关键,防并存)**:① 先在候选池**下架旧 7 包** ② 用户实例「功能管理」启用新包 ③ 确认弹窗 → 重启实例 ④ 走 `restartAndProbe` 探活(档案 34)。
*验证*:§八 C–F。
12. **实测验收 + 资源**:逐功能页点开;用 `scripts/probe-instance-mem.cjs` 量**私有内存与启动时间**(档案 58 预算:`MemoryMax 384 MiB` / 堆 256)。
*验证*:§八 D、G。
13. **归档**:新建档案(**原子占号** `mkdir 04-调整方案/.lock-<NN>`)→ 更新 `INDEX §二` / `03-路线图` / `交接单/README §一` → 本单 `git mv` 到 `archive/交接单-已完成/`。
*验证*:`docs-audit.py` 退出码 0。
### 6.1 技能随包的额外步骤(并入上面第 3、6、8、10 步,不另起编号)
- **建包阶段(并入第 3 步)**:按 §4.1 **引用驱动**裁剪(不是"看着像资料就删")→ 放整合包 `skills/<name>/`;生成 `skills/.manifest.json`(名称 / 版本 / 文件数 / sha256);剔完跑 §4.1 的**死引用扫描**。
*验证*:`du -sh skills/` ≈ **4.3 MB**(而不是 11 MB);`tar tzf` 确认**没有** `参考skills/`、`browser-harness/`、`nuwa-skill-main/examples/trump-perspective/`;**确认有** `mcn-dou-analysis/references/样例/`(含 994 KB 长图)、`nuwa-skill-main/SKILL.md`、**`mcn-data-insight/subskills/douyin-top-account/`**(这三块都是被引用的功能件,不能剔)。
- **改码阶段(并入第 6 步)**:实现 `ensureSkills()`(§4.1 五步)+ 加载标记;把 `mcn/lib/index.js` 的 13+ 处**技能绝对路径改成技能名**。
*验证*:`grep -rn "\.dsh/skills" lib/` = 0;`grep -rc "ensureSkills" lib/` ≥ 1。
- **打包阶段(并入第 8 步)**:`skills/` 整体进包(`package.json` 的 `files` 白名单必须包含 `skills`)。
*验证*:`tar tzf dist/*.tgz | grep -c "package/skills/"` ≈ 技能文件总数(数百)。
- **上线阶段(并入第 10 步)**:上传 → 用户启用 → 重启 → 按 §八 J/K 验技能就位。
### 6.2 收尾顺带(D 组:两处文档漂移 —— 用户已定「并进 T03 收尾」)
- **`BRIEF.md:20`**:`scope(512M / 150% / TasksMax 128)` → **`384M`**,并补 `NODE_OPTIONS=--max-old-space-size=256` 与 `NODE_COMPILE_CACHE`(档案 58 实测)。
- **`03-路线图与待办.md`**:头部仍是 09-11 21:20 合并版、**档案 57-61 完全未登记** → 补登记 + 刷新头部状态行;`BRIEF §3` 待办同步。
*验证*:`grep -n "384M" BRIEF.md` 命中;`grep -nE "档案 (60|61)" 03-路线图与待办.md` 命中(**写成 `(60|61)` 而不是字符类**:`档案\s*6[01]` 这种写法会被 `docs-audit.py` 的引用正则匹配成裸旧号 `6` → 永久悬空,T02 已踩过一次)。
## 七、风险与红线
**🔴 头号风险:新旧包并存 → 实例崩**
若某用户同时在位"旧 7 包"与"新整合包",会出现:`/mcn/api/*` 路由**重复注册**、`sidebar.footer.action` **重复注册**、`__mcnEntries` **重复条目**;严重时 `duplicate loader entry id` → **崩溃循环**(档案 25 有真实前例,靠档案 20 熔断才拦住,但用户会看到死页面)。**四道防线必须全做**:
1. 新包用**全新 loader id**(不复用 `mcn-nav` 等 7 个旧 id);
2. 门户候选池**下架旧 7 包**(或标 deprecated,并在包描述写明替代关系);
3. 切换按 §六 步骤 11 的顺序执行(先禁用旧 → 再启用新);
4. 新包 host 面加**防御**:启动时检测旧 id 是否同时在位 → 命中则**拒绝启动并写明确日志**(宁可这个包不工作,也不要崩实例)。
**其余**
- 内存/启动(档案 58):合并后单实例加载模块更多,**必须实测**,超预算就按需懒加载或砍 social-workbench。
- 网络(档案 38a):实例出网只记日志不拦(元数据端点已封)→ MCP `npx`、voxemw 云调用可达性要用**实测**确认,别假设。
- **技能同名互斥**(§4.1):插件投的技能、平台共享技能层、用户自建技能**三方可能同名** → ① 本单**不投共享技能层**;② 用户自建同名时插件**跳过并提示**;③ 若日后要改投共享层,必须先撤掉插件内投放。
- **技能随包的体积成本**:约 1.5–2 MB × 每用户一份(用户已明确"不分开管理",接受该冗余);日后要省可改软链方案(§4.1 备选)。
- 红线:R2 不改官方 dsh 包(只走 bundle + patch)✓;R3 client 禁 `exports.default` ✓;R5 本单不扩大平台权限 ✓;不自动升级 dsh ✓;**未获明确授权不 commit / push**。
## 八、验收
| # | 命令 / 动作 | 期望 |
|---|---|---|
| A | `tar tzf dist/*.tgz \| head` ;`md5sum` | 顶层只有 `package/`;无 `.bak`、无 `node_modules`;md5 记档 |
| B | 上传扫描 dry-run | **P0 = 0**;P1 逐条有结论 |
| C | 门户 `#/plugins` 上传 | 进入候选池、默认禁用、版本号正确 |
| D | 全新用户:启用 → 确认弹窗 → 重启 | 侧边栏入口出现;工作台首屏功能入口**逐个可打开** |
| E | `journalctl -u dshs --since '10 min ago' \| grep -E "mcn-suite\|duplicate"` | 有 `[mcn-suite] loaded`;**无** `duplicate loader entry id`;无崩溃重启 |
| F | 启用探活(档案 34) | 探活通过;失败时快照回滚不误伤好插件 |
| G | `scripts/probe-instance-mem.cjs` | 私有内存 + 启动时间在档案 58 预算内(写实测值) |
| H | 候选池 | 旧 7 包已下架/标 deprecated;**不存在"新旧同时可启用"的状态** |
| I | **技能随包就位**(§4.1) | 全新用户启用 + 重启后:`ls $DSH_HOME/skills/` 出现 `mcn-short-video` 与 4 个业务子技能;`journalctl` 有 `[mcn-suite] skills ensured: …` |
| J | 技能可用(agent 侧) | 实例内让 agent **按技能名**加载(如「调用 mcn-dou-analysis」)→ 能解析到,**无需给绝对路径** |
| K | 幂等 / 不覆盖 | 再重启一次 → 技能**不被重复写**(`.installed.json` 版本一致则跳过);人为放一个同名自建技能 → 插件**跳过并提示**,不覆盖 |
| L | 凭据(§五#7) | `skills/` 凭据扫描:**真密钥 0 命中**;app id / 私域域名的处置有明确结论并写进档案 |
| M | D 组文档(§6.2) | `BRIEF.md` 已更正为 384M;档案 57-61 已在 `03-路线图` 登记 |
| N | **裁剪后引用完整性**(§4.1 死引用扫描) | 只剩「已移除 / 已改走 X」的说明行;**无一条指向已删文件的活引用**(`browser-harness` 相关的"浏览器优先"指令必须已改写) |
| O | 包体积与技能树 | `skills/` ≈ **4.3 MB**(已剔 6.7 MB);含全部 **36 个 `SKILL.md`**(主技能 + 5 一级子技能 + `mcn-data-insight` 12 个二级子技能);**保留了** `references/样例/`(992 KB)、`nuwa-skill-main/SKILL.md`、`mcn-data-insight/subskills/douyin-top-account/` |
| P | 技能落地方式(§4.2) | 按 B(软链)先实测;不通则用 A(复制)。**两种都必须能被 skill 机制按名加载**(不是靠 prompt 给绝对路径) |
| Q | 撞名处理(§4.2) | 用户自建同名 → 让位并提示 ✅;平台共享层同名 → 互斥并提示 ✅;别的插件已占 → **拒绝写并报出 owner** ✅(验证:人为造这三种场景各跑一次) |
## 九、回滚
- **包级**:候选池删新包 → 重新上架旧 7 包 → 用户逐个启用 → 重启(**这是为什么原包源码与 tgz 都要留档**)。
- **实例级**:走档案 34 的 `restartAndProbe` 快照回滚;或直接禁用新包重启。
- **本单不改平台代码** → 无平台侧回滚;若为铺开改了 `ensure-*` 脚本,按 `.bak-<ts>` 还原 + 重启。
## 十、回报格式(执行会话填)
```
## T03 执行回报
- 决策 1–5 选定:<逐条,谁确认的>
- 整合包:name/version = ? 体积 = ? 路由数 25+N 入口数 = ?
- P0 适配:homedir() 残留 0 ✅ / powershell 残留 0 ✅ / .dsh/skills 硬编码 0 ✅ / .bak 已剔 ✅
- 打包与扫描:tgz md5 = ? P0 = 0 P1 = ?(逐条结论)
- 上传与切换:候选池版本 ? 旧包下架 = 是/否 启用+重启 ✅ 探活 ✅
- 实测:私有内存 ? MiB / 启动 ? s(对照档案 58 预算)
- 验收:A–I 逐项 ✅(未过项写现象)
- 证据:<journald loaded 标记 + duplicate 检查输出 + 内存采样>
- 偏离:<…;无则写"无">
```
## 十一、不在本单
- 业务技能内容本身的维护/改版(只做"投放到位")
- voxemw 云链路接线(档案 12 的 M1 后段)
- Easel / social-workbench 的平台化改造(决策 1 选 C 才需要,另立单)
- 平台「功能管理」分区的其它 UI 调整(档案 57/60 已闭环)
---
## 九、验收结果(2026-09-13 09:5x · **只读取证,服务器实测**)
> 口径:§八 逐项核对;能只读证的全跑了,需要浏览器/造场景的明确标出。
| # | 项 | 结果 | 实测证据 |
|---|---|---|---|
| A | 包结构 | ✅ | 池内 tgz md5 `2ff1a2fa775891085949d50452394fa0` / 1,953,267 B / **v0.3.9**;顶层仅 `package/`;`node_modules` **0**、`.bak` **0** |
| B | 上传扫描 | ✅(P0) | `[upload-scan] blocked=0 warnings=118`(**warnings 逐条结论未做**) |
| C | 候选池上传 | ✅ | 池内 `dsh-plugin-mcn-suite @0.3.9`(09-13 00:02) |
| D | 启用→重启→入口 | ⚠️ **未验**(需浏览器逐个点开入口) | — |
| E | 加载日志 | ✅ | `[mcn-suite] loaded v0.3.0(host 面 3 个:mcn, mcn-schedule, voxemw-cloud)`;**`duplicate loader entry id` = 0**;同期无崩溃 |
| F | 启用探活 | ⚠️ 未单独验(但**平台已自动回滚过一次** ⇒ 该链路是活的,见 07:42–07:46 事故) | — |
| G | 内存 | ⚠️ 偏紧 | guest 峰值 **382 / 上限 384 MiB**;admin 峰值 321 / 384(档案 74 采样) |
| H | 旧 7 包下架 | ✅ | 池内只剩 `mcn-suite` + `univer-office` 两个;无新旧并存 |
| I | 技能随包就位 | ✅ | 落点 = **`<home>/skills/mcn-short-video`**(4.6 MB,含 4 个一级子技能);journal `[mcn-suite] skills ensured …` |
| J | agent 按技能名加载 | ⚠️ 未验(需实例内跑一次 agent) | — |
| K | 幂等/不覆盖 | ✅ | `skills ensured: 跳过 [email protected](版本一致)` |
| L | 凭据扫描 | ⚠️ 未验 | — |
| M | D 组文档 | ✅ | `BRIEF.md` 已是 384M(档案 58 收尾时改) |
| N | 裁剪后引用完整性 | ⚠️ 未验(需跑死引用扫描) | — |
| O | 包体积与技能树 | ⚠️ **口径待核** | 实测 **4.4 MB / 18 个 SKILL.md**(主 1 + 一级 4 + `mcn-data-insight` 下 13);§八 原文期望 **36 个 SKILL.md** ⇒ 差异需确认为"包被裁剪"还是"口径过时" |
| P/Q | 软链落地 / 三种撞名场景 | ⚠️ 未验(需造场景) | — |
**结论**:**主体已通过(8 项实证)**;未验的 6 项(D/J/L/N/P·Q)都属"需浏览器或需造场景",可以另找时间补,**不阻塞投放**。
**⚠️ 一条必须记的事实(guest 那次尝试)**:2026-09-13 **07:42:52** guest 在「功能管理」提交启用 →
实例 **07:43–07:45 连崩 5 次**(`exitCode=134`,栈 = V8 `JsonParser`/`HeapAllocator` ⇒ **V8 堆打满**)→ 07:46 熔断 →
平台**自动回滚**(`package.json.bak-rollback-20260913T0748`)⇒ 现在 guest bundles 里**没有** mcn-suite。
根因指向 `--max-old-space-size=160` 太紧(档案 74)+ guest cgroup 峰值贴顶 ⇒ 详见 `04-调整方案/74` 文末「现场反证」。
**admin 侧**自 09-13 00:02 起一直启用且各项实证通过(上表 A/E/I/K 均取自 admin)。
---
## 十、收尾记录(2026-09-13 18:0x)
### 本轮补齐的 gap
| 项 | 结果 |
|---|---|
| **guest 启用**(原「剩余」第 1 条,单子长期卡点) | ✅ **已完成** —— `POST /api/plugins/mine/apply` `{id: dsh-plugin-mcn-suite, enabled: true}` → 任务 **`c20739a6dbe8975c`** → `status=success` / `stage=完成` / **`restarted=true`**;启用后 bundles 含 `dsh-plugin-mcn-suite` ✓ |
| 容量前置(07:4x 崩溃的根因) | ✅ **已解除** —— 档案 79 两处 P0 已修;R1-④ 动态配额生效(guest 装前 672 MiB → 装后 **800 MiB**)⇒ 「V8 堆打满 abort → 自动回滚」不再复现 |
| §八 **L** 凭据扫描 | ✅ **通过** —— `ak_`+32hex **0**、`sk-` **0**、`app_id=` **0**、`*.alotbuy.com` **0**;125 处命中均为变量名/文档说明;唯一 32+hex 是 `.manifest.json` 的**文件哈希** |
| §八 **O** 包体积与技能树 | ✅ **通过(口径澄清)** —— SKILL.md 实测 **18** 个;§八 原文期望「36」系**笔误**(主 1 + 一级 4 + `mcn-data-insight` 下二级 13 = 18,与文字描述完全吻合)。包总 4.27 MB / skills 3.55 MB |
| §八 **P** 技能落地形态 | ✅ **通过** —— admin 实例 `skills/mcn-short-video` = **实体目录**(18 个 SKILL.md / 4.6 MB)⇒ 走 §4.2 的 **A(复制)兜底路径** |
| §八 **N** 死引用扫描 | ⚠️ **未定判** —— 自动扫描出 63 处「包内路径对不上」,但抽样 8 个里 **5 个是误报**(引用省略 `references/` 前缀,文件其实在);**内容基本齐全**(16 个 `scripts/` 目录、27 个 `.py`、263 个 `.md`,知识库 12 类齐全)⇒ **不能判不合格**,需人工逐条读;**不阻塞投放** |
| §八 **D / J / Q** | ⏸ **未验**(需浏览器或造场景);按 §九 口径 **不阻塞投放** |
### 归档
- 本单主体目标已达成 ⇒ 按流程移入 `archive/交接单-已完成/`,并在 `INDEX.md §四` 与 `交接单/README.md §一`、`03-路线图与待办.md §二` 三处同步状态。
- **遗留(可选,不阻塞)**:D(实例内入口逐个点开)/ J(agent 按技能名加载)/ Q(三种撞名场景)/ N(死引用逐条人工判定)。