# 2026-09-25 ## 插件投放与分库线 · 第 14 棒(执行棒)—— 跨节点内容分发落地 + 修 `verify-static` 红灯 > 会话 `6c208f94-28b1-4da2-a528-eed7396a1541`|域锁 5 域 + `package.json` 骨架锁,**收口后已反序释放**|口径门禁 `ae2fe58f6040e153a34017959a90df87` 一致。 ### 交付(5 改 1 新;+1060 / −75 行) - `src/worker/agent.ts`(+481):`hashTreeAt`(与 Manager 侧 `hashTree` 跨实现同值)|`syncSharedLayerOnce`(自报实况 → 取差异 → 逐包拉 → **解包后自算指纹自校验** → 原子换盘)|`POST /shared-layer/pull`(**带 token,与 `/fence` 同级**)|启动一轮 + 30 min 定时(`.unref()`)。失败语义三条:**取不到对账 ⇒ 不动任何本地文件**|单包失败只记账继续|`extra` 只报不删。 - `src/web/routes/business-plugins.ts`(+474):`sharedLayerDiff`(纯函数;🔴 **指纹 `null` 判 `stale` 而非"没有"** ⇒ 触发自重拉)|`.node-sync-reports.json` 对账面|三只**仅认节点身份**的口(`x-dsh-agent-token` + `x-dsh-host-id`,401 不区分原因;非清单内 `flat` ⇒ 404 `not_here`)|`/api/plugins/shared` 加 `nodes`。 - `web/portal.html`(2 行):修「去痕迹」红灯。 - `test/shared-layer-sync.test.mjs`(新建 · 16 用例)。 - `dsh-server-docs/DEPLOY-本部署.md`(+109):新增 §6.6。 ### 真机结论(逐机读数) - 三包逐机指纹 47 ⇄ 106 **全等**:`storyforge` `1c45c841…`(117)|`mcn-suite` `cb1c73fb…`(657)|`im-conversation-tabs` `678e0f36…`(5)。 - 🔴 **106 `storyforge` 此前是内容漂移的**(`8a19709c…`,**文件数同为 117** ⇒ 只有逐字节指纹看得出);启用本项后被自动纠正。 - 第二轮 `missing/stale/extra` 全空(收敛);`.pull-stage-*` 零残留;106 在跑实例 scope `dsh-100002-9648cef4.scope` 未受影响。 - 三门:build rc=0|test **543/541 过/0 败/2 跳过**|layering ✅ 无新增违规|verify-static 全部合格。 ### 值得记住的三条 1. **106 上根本没有 `/etc/systemd/system/dshs-worker.service.d/`** —— 节点 env 落点是单元的 `EnvironmentFile` = `/etc/dshs-worker.env`。生效判据 = `/proc//environ` 真读到("文件写了" ≠ "生效了");有 drop-in 的机器 drop-in 优先、写错地方静默失效。 2. **`POST` 空 body + `content-type: application/json` ⇒ Fastify 400**(`FST_ERR_CTP_EMPTY_JSON_BODY`)—— 看着像接口坏了,其实只是调用方式问题。 3. **启动轮拉取偶发 `sync_unreachable: fetch failed` 属瞬时时序**(同刻同机 `curl`/`node fetch` 正常;第二次重启的启动轮直接成功)⇒ 失败即不动文件、≤30 min 自愈,未加代码级重试(不在本项范围)。 ### 收口 入口 §0 / §5 已回填;⛔ 未 commit / 未 push;待拍板 **0 条**;**下一棒未登记**(本线 §5 的 1–8 条已全部闭合,余项只有"需外部条件"(106 侧 `link:` 跨机腿)与"可选打磨"(启动重试)⇒ 按入口「⛔ 本线不预登记队列」待新需求再排)。 ### 口径纠正(⛔ 别再照着做) 第 13 棒/接续包记的去痕迹命中串 `DSHS_SHARED_CATALOG_PUBLIC` **不成立** —— 判据 `s.includes('dshs')` **区分大小写**,那个串是大写。**真命中 = `dshs_pl_*`**(`web/portal.html:609` 的 `title` 建库说明文案;worktree 与 `HEAD` 同命中 1 处 ⇒ 第 10 棒遗留)。 ## 05:0x|查证「平台面向插件的数据库接入面未建成 · 取数链路不通」—— **属实**(纯只读取证 · 零改动) > 触发 = 用户转述插件会话的结论,要求判定真伪。会话 `aliyun-dsh-server`(未抢锁 —— 全程只读)。 ### 分两层判(这是本条最有价值的部分:⛔ 别再说"数据面已落地"这种含糊话) - ✅ **建库面=管理面:已建成并有真机取证** —— `src/db/plugin-data/` 三文件(`schema.ts` 静态声明解析 / `diff.ts` 差异预演 / `datastore.ts` 建库·迁移·备份·对账);六条路由 `datastore(plan|migrate|audit|s)` + `datastores/reconcile` 全 `requireAdmin`。47 真机:建库 `created` 71 ms、S5-E ①–⑤ 全 PASS。 - 🔴 **取数面=运行时接入面:零实现**(本次新查实)。`ImDataPort` 全仓仅 **4 处命中** = `src/im/sdk/types.ts:434`(接口定义,5 方法 insert/update/remove/find/count)+ `host.ts` 引入 + 两个 `data?:` 可选字段。**无任何 `implements`/实例化**;`new ImSdkHost()` 只在 `test/im-sdk.test.mjs:152,944` 出现(生产代码一次都没实例化);**全仓无一处传 `data:`** ⇒ 缺省即 `host.ts:319` 注释所写「插件拿到的 `insert` 等一律 `not-registered`」。 - 🔴 **平台无替代通路**:`datastore` 系列全是管理动作,**没有任何业务数据读写 HTTP 口**;跨节点「实例 → 控制面」身份通道仍为 0(`DB-03 §六` 自认)。 ### 缺口的真身 = **接缝两不管**(写在两处文档里,谁都没做) - IM 线 D 单原话:「**`im.data` 是 09-20 规划态命名 —— 落成的是 `ImDataPort` 注入式端口(真库端口属建库面,本棒不越界)**」⇒ 运行时实现被**显式推给建库面**。 - 建库面(插件投放与分库线)做的是 `src/db/plugin-data/` **管理面**,⛔ 未做运行时端口 ⇒ 两边都以为对方做。 - ⚠️ **连带副作用(须修)**:`01-规范/08-插件开发与对接规范.md:103` 要求插件「读写一律走内核数据 API:`im.data.table(name).insert/…`」,而该 API 零实现 ⇒ **插件会话照规范开发必撞空**,规范当前在误导插件侧。 ### 取数命令(复核用) `grep -rn "ImDataPort" src/`(期望 4 处、无 implements)|`grep -rn "new ImSdkHost" src/ test/`(期望仅测试)|`grep -n "requireAdmin" src/web/routes/business-plugins.ts`(datastore 路由全 admin)。 ## 05:4x|把取数口做出来(用户令「把相关功能开发完成,更新插件接入文档,让插件会话知道如何接入」) > 会话 `aliyun-dsh-server-插件数据面`|域锁 8 域(`src/db`·`src/im`·`src/web`·`test/`+`01-规范`·`02-架构设计`·`03-数据库`·`交付物`)+ `package.json` 骨架锁|**收口后已反序释放**。 ### 交付(5 新 + 2 改;代码仓 `D:\github\dsh_shenxian`) - `src/db/plugin-data/runtime.ts`(新):`PluginDataRuntime` —— `ImDataPort` 的真实现。SQL 生成(标识符一律引用)+**归属强制注入**(`user` 恒 `user_id=调用者`;`room` 必带 `room_id` 且过成员判定,未注入判定即 **fail-closed**)+游标分页(按主键列)+配额(`DEFAULT_QUOTA`)+逐次审计+失败具名。 - `src/db/plugin-data/registry.ts`(新):装配层。声明**现解现用**(TTL 缓存、解 tgz)、**库名三处一致**校验(台账 vs 声明换算,不一致即 `db-name-mismatch` 拒)、每插件库池(`PLUGIN_POOL_MAX=2`,照定稿 §五「默认 1–2」)、`availabilityOf` 就绪判读。 - `src/web/routes/plugin-data.ts`(新):实例可达 HTTP 面 `/api/im/plugin-data/status` + `/api/im/data/{:table}`(GET/POST/PATCH/DELETE + /count)。**契约失败一律 200 + `{ok:false,reason,detail}`**;4xx 只留给身份/就绪。 - `sdk/im-plugin-client/`(新 · `index.mjs` + `package.json`):插件侧 SDK v1(REST 面)—— 零依赖、可 vendoring、⛔ 不含平台实现与凭据;`im.data.table(n).{insert,update,remove,find,count}` + `rooms.*`。 - `test/plugin-data-runtime.test.mjs`(18 例)+ `test/im-plugin-client.test.mjs`(6 例)+ `package.json` 两处清单登记;`src/web/server.ts` 注册 1 行。 ### 🔴 本单最重要的三条判断(别的会话只要记住这三条) 1. **`id` = 声明里的第一列** —— 主键是 `(归属列, 声明第一列)` 的**复合键、无代理 id 列**(`createTableSql` 实测)⇒ 插件侧 `update/remove` 的 `id` 就是这个值、且该列不可被 patch 改。**此前任何文档都没写过这条**,已补进 `DB-03 §五`(插件侧第一列必须选稳定唯一业务键)。 2. **⛔ 不改 `src/web/routes/im.ts`** —— IM 线 D 单验收判据 2(`test/im-sdk.test.mjs`)用 `git diff` 断言该文件零改动 ⇒ 往里加路由会**永久打红**那条判据(且健康环境下真红)。故本单自带 `plugin-data.ts`,鉴权口径独立实现但**共用同一本 `app.imInstanceTokens`**、头名与原因码取自 `instance-token.ts`。⚠️ 长期成本:IM 线改鉴权语义须同步本文件;正解是将来提 `src/web/auth` 共用(要先解 D 单冻结判据)。 3. **代码就绪 ≠ 可用**:**未部署 47/106、未真机验收**;且**跨节点形态仍不通**(缺"实例→控制面"身份通道)⇒ 当前可用形态 = **同机/本机**。清单在 `交付物/插件数据面-运行时取数口落地-20260925.md §六`。 ### 环境坑(本机 · ⛔ 别再当代码问题查) **`spawnSync EBUSY`** —— 本会话里凡是 `execFileSync('git'|'tar')` 的用例**稳定失败**(`test/im-sdk.test.mjs` 3 例 + `test/shared-layer-sync.test.mjs` 4 例,共 7 例),而复跑**仍是 EBUSY**(不是 diff 断言失败、不是代码退化)。判据 = 看 `error: 'spawnSync git EBUSY'`:**子进程起不来**,与判据内容无关。⇒ 读全量测试结果时先按这条排掉,别去改代码。 ### 收口 build rc=0|新测试 24/24|layering ✅ 无新增违规|⛔ 未 commit/未 push/未部署|入口 §0 已回填|**下一棒建议 = 部署+真机验收(未登记 automation)**。 ## 05:5x|接入静态验收(真插件)+ MCN 工作台移动端独占 + 移动端硬规范 > 会话 `aliyun-dsh-server-插件接入验收`|域锁 6 域(`src/db`·`src/web`·`test/`·`01-规范`·`03-数据库`·`交付物`)+骨架锁 `package.json`/`mcn-plugin-package`|**收口后已反序释放**。 ### ① 接入验收(静态面)—— 用真插件跑平台真校验器 - 脚本 `交付物/插件接入验收-静态面-20260925.mjs`(**只读**:不建库/不连 PG/不改包/不上传;可换插件参数)。 - 拿 `dsh-plugin-mcn` 实测 **全绿**:声明 3 表过校验(`origin=yaml-file`)|库名 `dshs_pl_dsh_plugin_mcn` 过白名单|复合主键 `(user_id, 首列)`|归属列补齐|8 条 DDL 正确。 - 🔴 **两条会返工的结论(已回写 08 规范)**: 1. **只把 `dsh.data.yaml` 放进包 = 不生效** ⇒ 第一跑 `origin=none`。必须 `package.json` 的 `dsh.data.schema` 指向,且 `files` 要带上该文件(否则 `npm pack` 不打包)。⚠️ 08 §3-1 本来就写了这条 —— **我是没先读文档才踩的,反证文档是对的**。 2. **存量表的自增 `id` 不能照搬**:数据面主键 = `(归属列, 声明第一列)`、**无代理 id 列** ⇒ 必须换业务键(本次用 `account_key` / `aweme_id` / `analysis_key`,规则 `<账号名>[:<时间戳>]`)。 - **接入前置读数**:MCN 包红线扫描**命中 21 处**(全在 `lib/db.js` 的 SQLite 建表/迁移)⇒ 当前形态不合规(DB-03 红线 6)。接入 = 把读写换成 `/api/im/data/*` ⇒ **插件侧改造,未做**。 ### ② MCN 工作台移动端独占(用户报障) - 根因(代码级):`.mcnNav_split` 是 `flex-direction:row` + 面板宽度是**行内 `style.width = 50%`**,插件**无任何视口断点** ⇒ 手机上两栏各半,而表格内容宽 720px ⇒ 两侧都不可用。 - 改法:`D:/dshworkspace/plugin_package/dsh-plugin-mcn/lib/client.js` 新增**独立样式段** `data-plugin-css="mcn-mobile"`(⛔ 不碰既有三段),单一 `@media (max-width:900px)` 四组规则:① 独占(`.mcnNav_split{display:block}`+会话区/拖拽条 `display:none!important`+面板 `width:100%!important`)② 触摸目标 ≥44px ③ 输入 `font-size:16px`(防 iOS 聚焦放大)④ 弹窗 `calc(100vw-32px)`/宽弹窗竖排/统计条换行/表格单元格放宽。 - 🔴 **两处 `!important` 不是偷懒**:面板宽度是**行内 style**(只有 `!important` 盖得住);宿主给会话区设了 `display:contents`(隐藏也必须 `!important`)。 - **取 900px 的理由**:≤900px 两栏各 <450px、表格表单都不成立 ⇒ 桌面窄窗口与平板共用一条判据。**宽屏行为逐字不变**。 - 退出路径:复用面包屑既有关闭按钮(已放大到 44px)⇒ **不新增控件、不动 React 结构**,改动面 = 一段 CSS 数组,可单独回滚。 - 校验:`node --check lib/client.js` ✅(⚠️ 中途踩了"多插一个 `}`"和"嵌套反引号"两个自伤,均当场修掉 —— 教训:**模板串里别嵌反引号**)。 - ⚠️ **未做真机/浏览器视觉验收**。 ### ③ 文档 `01-规范/08-插件开发与对接规范.md`:**新增 §4-补 移动端 / 窄屏兼容(硬要求)**(断点 ≤900px 表 + 四条硬判据 + 三条⛔〔禁用 `innerWidth` 判布局 / 禁 `100vh` / 窄屏禁拖拽调宽〕+ 六条自测 + 参考实现指针到 `mcn-mobile` 段)+ `§7` 补移动端专项 + 头部状态块 + `§11` 变更记录。⚠️ **改 §5 标题时误删过一次(Edit 的 old_string 未包含标题)⇒ 已补回**:教训 = **用标题当锚点插入时,new_string 必须把标题带回去**。 ### 收口 ⛔ 未 commit / 未 push / 未部署|入口 §0 已回填|交付件 `交付物/插件接入验收与移动端独占-20260925.md`|**下一棒建议(未登记)= 部署 47 → 数据面真机自测(四拒一空+insert/find 回读)+ MCN 移动端视觉验收**。🔴 **投放顺序硬约束**:MCN 带声明的新版**必须先建库再投放**(否则平台判"有数据面"⇒ 未建库时**所有用户都启不了**)。 ### ⚠️ 待办:`MEMORY.md`(状态层)**已超注入上限** 实测 8964 字符(文件头自称上限 ≈7800)⇒ **尾部约 1164 字符不进注入**(本轮新增已验证落在前 7800 内,未受影响)。 按"只减不增"纪律,**下一次动该文件时必须净减**:建议合并「覆盖网络线」与「其他线/基线」两段的过时行,或把「形态元假设」的逐条拆到技能里只留指针。⛔ 别在本轮大动(预算紧、易伤其它线)。 ## 07:3x|按接续会话方式排棒(第 15 棒:部署 47 + 两项真机验收) > 会话 `插件投放与分库线-第15棒排棒`|**全局独占锁**(机制层:`state.py` 属【E】)|**已释放**。 ### 为什么用接力而不是自己硬跑 上一棒(05:3x)把代码与文档做完,**真机验证没做** —— 本机无 PG、无容器、无可用实例 ⇒ 数据面建库/取数在这台机器上**跑不了**;MCN 移动端是**视觉**行为,也要真环境。⇒ 不硬凑读数(⛔ 无真机读数不得宣称验收通过),改用**执行棒 + 一次性 automation**。 ### 做了什么 1. **入口 §2 顶部新立「🎯 本轮动作(第 15 棒)」**(4 条动作 + 不许做清单),§0 已登记 automation id。 2. **登记 automation `72f02d61-396f-4d19-9178-411f1396cfcd`**(once · `scheduledAt = 2026-09-25T07:40` = 收口 + 约 6 分钟 · `cwds = E:/ProgramData/AIProject/aliyun-dsh-server`)。prompt 走六件套骨架:只指向入口 §2、⛔ 不抄技术细节;含「抢不到锁只报告并停」「⛔ 不改 `im.ts`」「⛔ 先投放后建库」「无真机读数不算验收」「自助重挂(⛔ 不改 scheduledAt,改则新建)」。 3. **`cwds` 取实测值**(不凭记忆):`SELECT cwd,COUNT(*) FROM sessions WHERE cwd LIKE '%aliyun-dsh-server%' GROUP BY cwd` ⇒ **192 条全部 = `E:/ProgramData/AIProject/aliyun-dsh-server`(正斜杠,唯一值)** ⇒ 逐字复制 ✓(另实测:`workbuddy.db` 只读打开正常,未碰三件套)。 4. 🔧 **修掉一个"开机级"缺陷 —— `state.py` 的 §2 解析正则**:原 `r"##\s*2\.[^\n]*\n"` 只认 `## 2.`,而本工作区 **4 条入口全部** 写 `## §2 xxx` ⇒ `[入口] §2` 段**长期是「未解析出条目」**。而它是每棒开机的**第一个信号**(技能 §6 第 2 条明说"钉在 §2 顶部才有用")⇒ 等于每条线的开机信息源一直是空的。 修法:`r"##\s*(?:§)?\s*2(?=[.\s、]|$)[^\n]*\n(.*?)(?=\n##\s|\Z)"`(兼容 `## 2.` / `## §2 ` / `## 2、`;用 lookahead 卡分隔符,⛔ 防 `## 2026-…` 被误当 §2)。**自证 = 修后该段展开出 6 条**(修前 0 条)。⚠️ 未来若给别的工作区复制 `state.py`,这份已带修法。 ### 判据留档 - 锁状态 = 读 `.exec-lock/OWNER`(空 ⇒ 无并行会话)⇒ 才允许改【E】机制层 ✓ - 预检结论:`state.py` =【E】必须独占;入口文件 =【D】未归类 ⇒ 并入同一次全局独占锁处理 ✓ - automation id **只来自工具返回值**(未预写占位)✓ ### 已知风险(未处置 · 有意) 本线有 **5 条已过期但仍 `ACTIVE`** 的 once 型棒(第 11–14 棒 + IM与插件接入),`scheduledAt` 都在 12 h 补跑窗口内 ⇒ 理论上可能被调度器扫到多开。**按技能 §4③ 取"保持原样"**(不动既有配置);即便被扫到,也会因抢不到域锁而"只报告并停",不会重复开工。⛔ 不用 shell 改库。 --- ## 08:0x|插件投放与分库线 · 第 15 棒(执行棒)收口 **任务**:部署取数口到 47 + 两项真机验收。**结果**:第 1 件 **7/7 PASS**;第 2 件**未通过(前提不成立)**;第 3 件超范围未做。 **做了什么** 1. 开工门禁【D】2 个(入口 + 当日日志)⇒ 归类后抢域锁(`aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md` + `aliyun-dsh-server/.workbuddy`),持有者断言通过。 2. **部署 47**:`lib/web/server.js`(与线上原版**只差 7 行**,逐行 diff 确认)+ `lib/db/plugin-data/{runtime,registry}.js` + `lib/web/routes/plugin-data.js` + `sdk/im-plugin-client/` ⇒ 5/5 md5 逐字一致;备份 `/opt/dsh/backups/lib-p15/47-pre-p15-20260925-074345.tgz`;`restart dshs` → active / NRestarts=0 / 门户 200 / 取数口 401(fail-closed,证明路由已注册)。**投放前先比对四件既有产物**(`business-plugins.js`/`worker/agent.js`/`leased-spawner.js`/`config.js` 与本地逐字一致)⇒ 防把别棒的东西带上线。 3. **真机验收(7/7)**:夹具 = 一次性探测包 `dsh-plugin-pdprobe`(user 域 + room 域各一表,声明先过平台真校验器 `level=ok`);四拒全中 + 非成员读 room 表 0 行 + `insert`→`find` 回读一致;**归属注入直连核**(`user_id` 全等于调用者本人);迁移回显首次在真机看见 `PRIMARY KEY ("user_id","probe_key")`。 4. 🔴 **抓出并修复 P0 接线缺陷**:库 `ready` + 鉴权过,取数仍一律 `409 no-declaration`。根因 = **两个名字空间混用**(池键/台账键 = **原始包名**;取数口传给 registry 的是 **`pluginIdOf()` 归一化 id**,registry 拿它**精确查**那两张表 ⇒ 永远查不到 ⇒ **任何插件**都取不到数)。**单测抓不到**(假执行器直接构造声明,绕过路由层归一化)。修 `src/web/routes/plugin-data.ts` 两处(status 路由对外仍回归一化 `pluginId`,响应契约不变);build rc=0 + 24/24 单测 + 重投重启后 **7/7 PASS**。⛔ 未 commit / push。 5. 🔴 **第 2 件未通过(前提不成立)**:线上真身 = `dsh-plugin-mcn-suite` **v0.3.13**,其 `lib/client.js` 里 `mcn-mobile` 命中 **0**(本地 suite 同样 0);修复落在 **`dsh-plugin-mcn` v0.1.0 —— 从未投放的包** ⇒ "重投 + 实例重启"无从生效。**不做视觉验收、不宣称通过**。✅ 可移植性已核对:该段是运行时注入 `