Files
dsh_ai1net_server/交付物/MCN数据面接入-阶段二-第一批按表切换-20260925.md
admin 318430c9e9 chore(工作区): 插件投放与分库线收口入库(第 35 棒 + 22:44 拍板落盘)
范围 = 本线(插件投放与分库线)产物 + 记忆类,共 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/规则与载体/、归档/、接续包_*。
2026-09-25 23:06:29 +08:00

14 KiB
Raw Permalink Blame History

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 真机读数 → 切档)。


一 结论

两件都做完了,且拿到了真机读数。 关键判定如下(逐条可复现,见 §二 判据表):

  1. 写源档落地 —— 读写两档共用一个配置文件(<DSH_HOME>/.dsh/data-plane.json), 单测 31/31 通过(旧 24 + 新增 7)。默认档 = local ⇒ 零平台写请求(可断言,非"应该不会")。
  2. 🔴 补了一个交接单没写、但不做就不成立的东西:按表档。全局单档表达不了逐表切换 (一张表迁完、其余没迁时切全局 ⇒ 未迁的表读到空)。现支持 {"mode":"local","tables":{"account_persona":{"mode":"platform","writeMode":"platform"}}}; 解析顺序 = env(全局)> 该表覆盖 > 全局 file > local。
  3. 第一批两张表的全部读写点已接线(含删除),逐处落点见 §三。
  4. 存量迁移真机完成:本地 account_persona 4 行 → 平台 4 行,逐账号集合 onlyLocal=[] / onlyPlatform=[] / match=true;幂等重跑 ins=0 skip=4;旧库 sha256 迁移前后逐字一致 (b37f8aab…)。creative_log 本地 0 行 ⇒ 空迁移(无写请求)。
  5. shadow 档真机读数:shadowReads=4、shadowMismatch=0、三账号全 match=true、 degraded 一次都没出现;平台调用 27 次全 ok / 0 失败。
  6. 已切档(按表):account_persona + creative_log 两表 = read/write 双 platform; 全局仍 local、hot_accounts 仍 local(⛔ 没有"顺手全切")。回滚 = 删该文件或 clearPlaneMode()。
  7. ⛔ 未出包 / 未投放 / 未重启任何服务 ⇒ 实例内(插件路由)端到端未取证,本期不宣称通过 (实例仍载 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 并注明"直连平台、不看档位"。)


四 本棒的三个技术判定(自决 · 可推翻)

  1. 写路径 ⛔ 不回落本地(与读路径的 degraded 回落方向相反)。读回落只是"看到旧数据"; 写回落是"写到了没人读的地方"(调用方拿到成功、界面不报、平台侧永远是旧值)⇒ 宁可失败可见。 单测 ㉑ 钉住:"local 回调一次都不许跑"。
  2. json 列按附录 C 的 A 案(值清洗)落码,且放在适配层唯一咽喉(normalizeRow): 字符串是合法 JSON ⇒ 原样(⛔ 不二次包装);非法(Markdown 正文)⇒ 包成 JSON 字符串; 非字符串 ⇒ 原样。被清洗的列名具名进 coerced[](⛔ 不静默改数据)。 本棒真机:4/4 行被清洗,原文一字不丢(personaKeyOf 两侧算得出同一个键即反证)。 ⚠️ 案 A 的已知代价(读侧两形态)如实保留;案 B(改声明 text)仍⛔ 未执行(须删库 ⇒ 待拍板 ㈢/㈣)。
  3. 级联删除的顺序: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); node fetch 改不了 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 主战场)留第三批。