范围 = 本线(插件投放与分库线)产物 + 记忆类,共 27 件:
- 接续入口_插件投放与分库线(1 件):§0 新增 22:44 拍板行;§2 第 36 棒范围改 5 步、条件步转无条件
- 交付物(22 件):MCN 数据面接入阶段一/阶段二系列(含 B 案落地与 shadow 读数)、
P0 修通与移动端真机验收、pnpm-EPERM、两机 lib 差异、共享层台账语义、
基础插件身份与回滚、插件接入验收、插件数据面取数口、移动端迁包与字号扩面、跨机错误消息
- 记忆(4 件):2026-09-24 / 2026-09-25 日志、MEMORY.md、本棒 automation 执行记录
⛔ 未含他线在途改动(只报告、不代提交):机制层 CODEBUDDY.md / state.py /
.codebuddy/rules/server-ops.md、接续入口_IM线、接续入口_StoryForge验收线、
其余 29 个 automation 目录、docs/规则与载体/、归档/、接续包_*。
14 KiB
MCN 数据面接入改造 · 阶段二 · 第一批按表切换(第 28 棒)
执行会话:插件投放与分库线-28棒(2026-09-25 14:43–15:0x)|工作区:
E:/ProgramData/AIProject/aliyun-dsh-server(WS) 唯一执行单:DOC/05-交接单/插件投放与分库线-03MCN数据面接入改造-阶段二.md(DOC=D:/github/dsh_shenxian/dsh-server-docs) 上一棒:第 27 棒(交付物/MCN数据面接入-阶段二-20260925.md,S0/S1/S2/S3 完成) 本棒范围(入口 §2「🎯 本轮动作」):① 给store.js加写源档(local默认 /platform),与读源档同源同文件 + 单测;② 按**「表」**(⛔ 不是按文件)推进第一批全量切换:account_persona+creative_log跑端到端样板(全部读写点 → 迁移 →shadow真机读数 → 切档)。
一 结论
两件都做完了,且拿到了真机读数。 关键判定如下(逐条可复现,见 §二 判据表):
- 写源档落地 —— 读写两档共用一个配置文件(
<DSH_HOME>/.dsh/data-plane.json), 单测 31/31 通过(旧 24 + 新增 7)。默认档 =local⇒ 零平台写请求(可断言,非"应该不会")。 - 🔴 补了一个交接单没写、但不做就不成立的东西:按表档。全局单档表达不了逐表切换
(一张表迁完、其余没迁时切全局 ⇒ 未迁的表读到空)。现支持
{"mode":"local","tables":{"account_persona":{"mode":"platform","writeMode":"platform"}}}; 解析顺序 = env(全局)> 该表覆盖 > 全局 file >local。 - 第一批两张表的全部读写点已接线(含删除),逐处落点见 §三。
- 存量迁移真机完成:本地
account_persona4 行 → 平台 4 行,逐账号集合 onlyLocal=[] / onlyPlatform=[] / match=true;幂等重跑ins=0 skip=4;旧库 sha256 迁移前后逐字一致 (b37f8aab…)。creative_log本地 0 行 ⇒ 空迁移(无写请求)。 shadow档真机读数:shadowReads=4、shadowMismatch=0、三账号全match=true、degraded一次都没出现;平台调用 27 次全 ok / 0 失败。- 已切档(按表):
account_persona+creative_log两表 = read/write 双platform; 全局仍local、hot_accounts仍local(⛔ 没有"顺手全切")。回滚 = 删该文件或clearPlaneMode()。 - ⛔ 未出包 / 未投放 / 未重启任何服务 ⇒ 实例内(插件路由)端到端未取证,本期不宣称通过
(实例仍载
0.3.15,即尚无本棒代码;生效需 47 实例重启 ⇒ 交接单 §四 4.3 ㈠ 待拍板)。
二 判据表(命令 + 实际输出摘要 + 退出码)
| # | 判据 | 命令 | 实际输出 | 退出码 |
|---|---|---|---|---|
| C1 | 写档单测(默认档零写请求 / 非法值折回 / platform 真发写请求 / 不回落 / 按表档 / 同源同文件) |
node --test tests/mcn-store.test.mjs |
# tests 31 / pass 31 / fail 0(旧 24 全过 + 新增 7 例 ⑱–㉔) |
0 |
| C2 | 语法自检(改动文件) | node --check <f> × store/index/imports/workshop/data |
5/5 OK | 0 |
| C3 | 包内 smoke 未回归 | node tests/smoke.mjs |
全部通过(16 组断言) |
0 |
| C4 | 迁移工具本机自证(假 fetch) | node --experimental-sqlite tmp/p28/local-migrate-check.mjs |
dry-run 平台调用 0;apply ins=2/1;重跑 skip=2/1;coerced=["0:content_json","1:content_json"] |
0 |
| C5 | 静态改(本地库指纹不变) | sha256sum $DB(迁移前 / 迁移后) |
前后逐字一致 b37f8aab51a9…(2,383,872 B,mtime 仍 09-20 23:05:05) |
0 |
| A1 | 存量迁移一致性(逐表 + 逐账号) | 47 真机探针 P28RESULT 的 per_account |
1627: 1/1、1628: 1/1、1629: 2/2,onlyLocal=[] onlyPlatform=[] match=true |
0 |
| A1b | 幂等(可重跑) | 同上 rerun |
account_persona: ins=0, skip=4, fail=0|creative_log: ins=0, skip=0 |
0 |
| A2 | 全表读写 E2E(实例内) | — | ⛔ 未达成(需 47 实例重启 ⇒ 待拍板 ㈠;本轮⛔ 未重启、⛔ 未投放) | — |
| A3 | 红线归零(删 db.js) |
— | ⛔ 未开工(附属录 D 前置 = A1+A2 全绿;A2 未达成) | — |
| A4 | 表名对齐彻底 | 第 27 棒已达成(包内字面 0 命中) | 本轮未回退:本轮改动未引入旧表名(改动面 6 件全文无命中) | 0 |
| A5 | 旧库备份在位 + 零数据损失 | 47:ls -la /opt/dsh/backups/plugin-data/dsh-plugin-mcn-suite/ |
备份 cce6d1cd-…-20260925-141436.tar.gz(809,732 B)在位;旧库原样在盘(sha256 不变) |
0 |
| A6 | 移动端五条 | 08 §4-补 ④ |
未改 UI ⇒ 免跑。文件清单依据:本轮改动面恰 6 件 = lib/host/mcn/{store,index,imports,workshop}.js、tests/mcn-store.test.mjs、tools/migrate-local-to-platform.mjs —— lib/client.js / lib/host/mcn/data.js 零改动 |
— |
| A7 | 卫生 | 见 §五 | ⛔ 未改 src/**|⛔ im.ts 零改动|⛔ 未手改用户 home 下 profile/.pnpm|⛔ 未重启 106|⛔ 未 commit·push|✅ 临时会话 1 → 删 1 → 残留 0 |
0 |
| A8 | 两机一致 | — | ⛔ 未做(本轮不投放 ⇒ 与两机部署面无交集;共享层仍 0.4.0,实例仍 0.3.15) |
— |
§二-补 · shadow 档真机读数(交接单 §五 S6 的验证项)
[mcn-store] shadow what=account_persona.account=1627 platform=1 local=1 match=true
[mcn-store] shadow what=account_persona.account=1628 platform=1 local=1 match=true
[mcn-store] shadow what=account_persona.account=1629 platform=2 local=2 match=true
[mcn-store] shadow what=account_persona.count.shadow platform=4 local=4 match=true
stats: shadowReads=4 shadowMismatch=0 platformCalls=27 platformOk=27 platformFailed=0 writeCalls=0
- 无
degraded(即一次都没有"平台不可用回落本地") - 影子读一律
source:"local"(影子档必须返回本地 ⇒ 零回归),mode:"shadow"✅
§二-补 2 · 前置读数(迁移前后)
| 项 | 迁移前 | 迁移后 |
|---|---|---|
平台 account_persona 行数 |
0 | 4 |
平台 creative_log 行数 |
0 | 0(本地本来 0 行) |
本地 account_persona 行数 |
4(account_id = 1627/1628/1629) |
4(⛔ 未动) |
本地 creative_log 行数 |
0 | 0 |
三 逐处落点(两张表 · 全部读写点)
| # | 文件:行(本棒现算) | 原形态 | 现形态 | 档位归属 |
|---|---|---|---|---|
| 1 | lib/host/mcn/imports.js(importExternalDir 账号设定.md) |
SELECT id…去重 + INSERT INTO account_persona |
await store.insertPersona(row, { local }) —— local 回调里原样那条去重 SQL |
读写档 |
| 2 | lib/host/mcn/imports.js(importExternalData persona 分支) |
同上 | 同上 | 读写档 |
| 3 | lib/host/mcn/workshop.js(batchDeleteCascade 账号级) |
DELETE FROM account_persona WHERE account_id=?(在本地事务里) |
平台侧先删(store.removePersonaByAccount)+ 本地那一条照旧保留 |
写档 |
| 4 | lib/host/mcn/index.js(taskDone 的 dsh_persona-*) |
SELECT COUNT(*) … account_persona |
await store.readCount({table:"account_persona", …}) |
读档 |
| 5 | lib/host/mcn/index.js(视频列表 has_content) |
EXISTS(SELECT 1 FROM account_persona …) 写在同一条大 SQL 里 |
platform 档下该子查询换成 0,取回本页后插件内补(store.accountIdsWithPersona);shadow/local 档仍走本地子查询(零回归) |
读档 |
| 6 | lib/host/mcn/index.js(creative_log 计数) |
INSERT INTO creative_log (created_time, source) |
await store.insertCreativeLog(row, { local }) |
写档 |
| 7–9 | lib/host/mcn/index.js ×3(/mcn/api/account/persona、脚本列表、账号详情) |
阶段一已接 store.readRows,但 keyOf 是 persona_id ?? analysis_time ⇒ 本地行算不出(本地无 persona_id) |
keyOf = store.personaKeyOf,并给三处本地 SELECT 补上 account_id(两侧共用同一键函数,影子读这才比得齐) |
读档(复核) |
新增/改动的导出(store.js):WRITE_MODES · dataPlaneWriteMode(table?) · setDataPlaneWriteMode(mode, table?) ·
setDataPlaneMode(mode, table?)(合并写,修掉了原来"写一次覆盖整份文件"的隐患) · clearPlaneMode(table?) ·
planeSummary() · readsPlatform/writesPlatform(table) · writeThrough({what,table,platform,local}) ·
insertPersona · removePersonaByAccount · insertCreativeLog · personaKey/personaKeyOf ·
accountIdsWithPersona · JSON_COLUMNS · repairJsonText · canonicalJsonText。
(insertPersona 原为"夹具口",改名 insertPersonaDirect 并注明"直连平台、不看档位"。)
四 本棒的三个技术判定(自决 · 可推翻)
- 写路径 ⛔ 不回落本地(与读路径的
degraded回落方向相反)。读回落只是"看到旧数据"; 写回落是"写到了没人读的地方"(调用方拿到成功、界面不报、平台侧永远是旧值)⇒ 宁可失败可见。 单测 ㉑ 钉住:"local 回调一次都不许跑"。 json列按附录 C 的 A 案(值清洗)落码,且放在适配层唯一咽喉(normalizeRow): 字符串是合法 JSON ⇒ 原样(⛔ 不二次包装);非法(Markdown 正文)⇒ 包成 JSON 字符串; 非字符串 ⇒ 原样。被清洗的列名具名进coerced[](⛔ 不静默改数据)。 本棒真机:4/4 行被清洗,原文一字不丢(personaKeyOf两侧算得出同一个键即反证)。 ⚠️ 案 A 的已知代价(读侧两形态)如实保留;案 B(改声明text)仍⛔ 未执行(须删库 ⇒ 待拍板 ㈢/㈣)。- 级联删除的顺序:
account_persona的平台删除放在本地事务之前。理由:本地事务管不了平台写, "先本地后平台"一旦平台失败 ⇒ "本地已删、平台还在"(平台档下读回来看起来没删); 反过来先删平台、本地再失败 ⇒ 重试一次即收敛(removeWhere幂等)。逐账号具名收集进platform[](ok/matched/removed/failed[]),⛔ 不中断整批。
五 卫生(A7 逐条)
- ⛔ 未改
D:/github/dsh_shenxian/src/**;⛔src/web/routes/im.ts零改动(本棒未碰src/) - ⛔ 未手改用户
home下的 profile /.pnpm(唯一写入 = 该用户自己的插件数据目录<home>/.dsh/data-plane.json—— 这是阶段一设计的插件自有档位文件,也是"一键回滚"的落点) - ⛔ 未重启任何服务(47 实例 / 106 / nginx 全未动);⛔ 未 commit / push
- ⛔ 未投放任何版本到候选池或共享层(共享层仍
0.4.0) - ✅ 临时 admin 会话:建 1 → 删 1 → 残留 0(
SESS_DELETED=1 SESS_LEFT=0) - ✅ 47 旧库零改动(sha256 前后逐字一致);✅ 未删任何库(⛔ 全程无
DROP) - ✅ 探针落
/tmp/p28/**(收口清理,见 §七)
六 未闭合项(逐条:现象 / 归属 / 下一步)
| # | 现象 | 归属 | 下一步 |
|---|---|---|---|
| 1 | 实例内端到端未取证(插件路由 /mcn/api/account/persona 仍跑 0.3.15 旧码) |
部署面 | 需 47 实例重启(待拍板 ㈠);无窗口则 A2 只能取"取数口"读数 |
| 2 | 出包 0.5.0 / 建库 / 投放 / 两机一致(S7–S11)全未开工 |
部署面 | ⛔ 投放会让新版对所有登录用户可见可启(R5 扩面)⇒ 与 ㈠ 一起排 |
| 3 | creative_log 无任何读点 ⇒ 切档后无法用"读数"证明,只能证明"写请求真到了平台" |
插件侧 | 后续批次给该表补一个统计读口,或接受"写侧单边取证"(本棒如实声明) |
| 4 | 其余 9 张表(hot_accounts 拆表 + 12 处 JOIN + 4 处 OFFSET + 2 处聚合、account_videos 362 行)未切换 |
插件侧 | 排后续批次,按「表」推进(见入口 §2) |
| 5 | json 案 B(声明改 text)仍待拍板 ⇒ 读侧两形态长期存在 |
平台侧 / 决策 | 待拍板 ㈢㈣;⛔ 未拍板一律不执行删库 |
七 探针与证据落点
| 件 | 位置 |
|---|---|
| 本机:迁移工具自证(假 fetch) | WS/tmp/p28/local-migrate-check.mjs |
| 47:真机探针(fetch 垫片 + 迁移 + 影子读 + 切档) | WS/tmp/p28/run-p28.mjs → 47 /tmp/p28/run-p28.mjs |
| 47:编排脚本(指纹 / mksess / 清理) | WS/tmp/p28/orchestrate.sh |
| 47:探针上传的包副本(md5 已核对逐字一致) | 47 /tmp/p28/pkg/**(store.js b6b48b9b…、工具 950e8a5f…) |
| 47:只读取证 | WS/tmp/p28/probe-a.sh、probe-b.sh |
🔴 探针用的是 Phase 25 已验证的合法通道(会话 Cookie +
x-dsh-im-plugin-id); nodefetch改不了Host头(会静默丢 ⇒ 假 404)⇒ 垫片改走http.request并剥掉实例 token 头。 本探针跑的是包内真实代码(store.js/tools/migrate-local-to-platform.mjs,md5 与本机逐字一致), 不是"另写一份等价逻辑"。
八 下一棒建议(唯一)
第 29 棒 · 执行棒 = 第二批按表切换:account_analysis(与 account_persona 同构 ⇒ 直接复制本轮口径:
2 写 + 1 删 + 计数读,可复用 personaKey 的写法)+ video_analysis(补写/删 + 迁移;12/12 行非法 JSON
⇒ 正好检验 A 案清洗的规模面)。链路同本棒四步:全部读写点 → 迁移(幂等)→ shadow 真机读数 → 切档。
⚠️ 仍⛔ 不出包、⛔ 不投放、⛔ 不动删库候选;account_videos / hot_accounts(JOIN/OFFSET 主战场)留第三批。