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

16 KiB
Raw Permalink Blame History

MCN 数据面接入 · 阶段二 · 第二批按表切换(account_analysis + video_analysis)

执行会话:插件投放与分库线 · 第 29 棒(2026-09-25 15:1x–15:3x) 唯一执行单:D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-03MCN数据面接入改造-阶段二.md(§五 S4–S6 / §六 A1–A8) 变更集(7 件,全在插件包内):lib/host/mcn/{store,index,imports,workshop,data}.js + tools/migrate-local-to-platform.mjs + tests/mcn-store.test.mjs §0 前提:第一批(account_persona + creative_log)已于第 28 棒切换完毕并在位;本棒只做第二批,⛔ 未顺手切第三批。


一 结论

第二批按表切换完成,四步链路走完,真机读数全绿。

步 结果
① 该表全部读写点接线 ✅ account_analysis 6 处 + video_analysis 10 处,逐处落点见 §三;剩余命中全部是「local 回调 / schema 维护 / 具名降级」,见 §四
② 迁移(幂等) ✅ 本地 account_analysis 1 → 平台 1;本地 video_analysis 12 → 平台 12;重跑 ins=0 / skip=1、ins=0 / skip=12
③ shadow 真机读数 ✅ 4 次影子读全部 match=true、shadowMismatch=0、degraded 一次未出现、平台 44 次调用全 ok / 0 失败
④ 按表切档 ✅ 只切这两张(+保留第一批),hot_accounts / account_videos 仍 local

离线判据:单测 40/40 通过(31 → 40,新增 9 例);node --check 7 文件 rc=0;旧表名字面量 0(A4)。

🔴 未闭合(⛔ 不宣称通过):实例内端到端(需 47 实例重启 ⇒ 待拍板 ㈠)、出包/投放/两机一致(S7–S11)、红线归零(S10,属第三批收口)。逐条见 §六。


二 判据表(A1–A8 · 命令 + 输出原文 + 退出码)

# 判据 命令(在 47 上跑) 输出原文(关键行) rc
A1 存量迁移一致性(逐表) P29_SID=… P29_HOME=… node --experimental-sqlite /tmp/p29/run-p29.mjs(§四 口径) compare.account_analysis=[{account_id:"1627", local_n:1, platform_n:1, onlyLocal:[], onlyPlatform:[], match:true}];compare.video_analysis={local_n:12, platform_n:12, truncated:false, onlyLocal:[], onlyPlatform:[], match:true, local_rows_without_aweme:0} 0
A1′ 内容往返(本棒加严:A1 只判计数+主键集合,内容不判会漏掉 json 清洗问题) 同上 roundtrip_va={n:12, present:12, equal:12, mismatches:[], plat_types:["string"], plat_unwrapped_is_markdown:12};roundtrip_aa={present:true, equal:true, plat_type:"string"} 0
A2 全表读写 E2E(实例内) 需实例重启 ⛔ 未达成(见 §六 ㈠)—— 本棒只取到「平台取数口」读数 —
A3 红线归零 cd $PKG && grep -rEn "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)" . | grep -v /node_modules/ | wc -l 23(= 阶段一 22 + 第 27 棒表名对齐 ALTER 1);本棒零新增(本棒未加任何 DDL / 驱动依赖) 0
A4 表名对齐彻底 grep -rE "account_video_analysis" $PKG | grep -v /node_modules/ | wc -l 0(旧名仅以拆写常量存在于 db.js 与迁移工具,用于读本地存量旧表名) 0
A5 旧库备份在位 + 零数据损失 47:sha256sum "$DB"(迁移前后各一次)+ ls /opt/dsh/backups/plugin-data/dsh-plugin-mcn-suite/ 迁移前 b37f8aab51a90f7481eb321e50456ca347635bcf2bfce567ea4200d88c18277a;迁移后 同值;size=2383872 mtime=2026-09-20 23:05:05 未变;备份 cce6d1cd-…-mcn-plugin-db-20260925-141436.tar.gz(809,732 B)仍在位 0
A6 移动端五条 08-规范 §4-补 ④ 免跑 —— 依据 = 变更集 7 件中 无 lib/client.js(grep 改动清单可验),即未改 UI 代码 —
A7 卫生 git -C D:/github/dsh_shenxian status --short -- src/ + find src -newermt "2026-09-25 15:15" src/ 变更 0 件(最新 mtime 12:20,早于本棒 15:15 开工)|src/web/routes/im.ts 零改动|未重启任何服务|未 commit·push|临时会话 1 → 删 1 → 残留 0|47 /tmp/p29* 残留 0 0
A8 两机一致(代码面 / 运行面分开记) — 均未开工:本棒未出包、未投放(插件包仍 version 0.4.0;共享层仍 0.4.0)⇒ 代码面/运行面都还是旧的,⛔ 不得写"一致" —

⚠️ ⛔ 不许只报"全部通过":A2 / A8 明确未达成,理由与归属已逐条写明(§六)。


三 逐处落点表(本棒接线面)

3.1 account_analysis(6 处 · 与 account_persona 同构)

# 位置 原形态 改后
1 imports.js importExternalDir 的 else(账号数据分析.md) SELECT id … + INSERT … store.insertAnalysis(row, { local })——local 档回调里是原样两条 SQL
2 imports.js importExternalData 的 an.type === "account" 同上 同上
3 imports.js previewExternalDir 的 checkMd(…, "account_analysis") SELECT id … content_json=? store.analysisExists({table, accountId, content})(平台档按内容比对;读失败具名回落本地)
4 index.js taskDone 的 dsh_acctana- 分支 SELECT COUNT(*) … account_id=? store.readCount({ table:"account_analysis", platform: countAnalysis, local: 原 SQL })
5 index.js /mcn/api/account/analysis SELECT content_json … ORDER BY id DESC LIMIT 1 store.readRows + store.latestAnalysis()(平台拉一页后插件内按 analysis_time 归约);content 过 store.jsonTextToRaw()(案 A 读侧兼容)
6 workshop.js batchDeleteCascade(账号连坐) DELETE FROM account_analysis WHERE account_id=? 平台侧 store.removeAnalysisByAccount()(D9 两步)先执行,本地事务里那条 SQL 保留(本地保持一致、保回滚面)

3.2 video_analysis(10 处 · 主键 = 天然键 aweme_id)

# 位置 原形态 改后
1 imports.js importExternalDir(拆解分析.md) SELECT dup + INSERT store.writeVideoAnalysis(row, { local }),语义 = 去重
2 imports.js importExternalData 的 an.type === "video" SELECT dup + DELETE + INSERT 同上但 { replace: true },语义 = 删旧保新
3 imports.js previewExternalDir(拆解分析.md) SELECT id … content_json=? store.analysisExists({table:"video_analysis", awemeId, videoId, content})
4 index.js taskDone 的 dsh_video-analyze- 分支 COUNT(account_video_source) + COUNT(video_analysis) 并列 account_video_source 那半照旧本地(第三批);video_analysis 那半走 store.countVideoAnalysis()
5 index.js 视频总表的 parsed 过滤条件(进 WHERE) v.id IN (SELECT video_id FROM video_analysis) 平台档:loadVideoAnalysisIndex() 拉全量 ⇒ 拼 v.id IN (<字面 int 列表>);索引不可用 ⇒ 具名降级回本地子查询
6 index.js 视频总表的 has_analysis 输出列 同一条 SQL 里的子查询 平台档下该列不进 SQL(0),取回本页后由索引在插件内补(与第 28 棒 has_content 同法)
7 index.js /mcn/api/video/detail SELECT … WHERE aweme_id=? OR video_id=? ORDER BY id DESC LIMIT 1 store.readRows + store.latestVideoAnalysis()(先按 aweme_id 直取,再退 video_id 归约)
8 index.js 桌面统计(analyses) SELECT COUNT(*) FROM video_analysis store.readCount({ table:"video_analysis", platform: countRows, local: 原 SQL })
9 workshop.js deepVideos DELETE … WHERE aweme_id=? + WHERE video_id=?(本地事务内) 平台侧在 BEGIN 之前先按两 scope store.removeVideoAnalysis();本地两条 SQL 保留
10 data.js 账号视频列表的平台档补列 平台档下 has_analysis 走本地 COUNT 平台档改用 loadVideoAnalysisIndex()+videoAnalysisCountOf()(has_source 仍本地,第三批)

3.3 顺带闭合的两处(⛔ 不是扩大范围,是不修就留坏态)

  1. 🔴 videoAnalysisCountOf 的 OR 语义(本棒自证时抓到):初版写成 byAweme.get(a) + byVideo.get(v)(两路相加)——而本地语义是 WHERE video_id=? OR aweme_id=? ⇒ 同一行两键都命中时被计两次(本地 1、这里 2)。已改为逐行 OR 判定,并以单测 ㉚ 钉住({awemeId, videoId} 同视频 ⇒ 1,两个不同视频 ⇒ 2)。
  2. 🔴 previewExternalDir 的三处「是否已存在」判据:其中 account_persona 那处是第 28 棒的遗留读点(该表已切,但预览判据仍读本地 SQL ⇒ "平台已写、预览还说不重复")。本棒把三处统一走 store.analysisExists() 并让该函数 async。

3.4 一处按表档连贯性修复

data.js 的视频列表读(account_videos)阶段一没接按表档(当时还没有按表档)⇒ 该表的档位覆盖对它不生效。本棒补上 table: "account_videos"(全局档仍是 local ⇒ 行为不变)。


四 具名豁免(剩余命中为什么不算"没切")

类别 位置 为什么豁免
local 回调 imports.js 4 处、index.js 2 处、workshop.js 3 处的 d.prepare(...) 它们就是写源档 local(默认)那一半:按设计保留原 SQL 逐字不变 ⇒ 默认档行为与 0.3.15 完全一致(可单测)
schema 维护 db.js 的 sqlite_master 查询 / ALTER TABLE RENAME / PRAGMA / 一次性 UPDATE 回填 属本地 SQLite 生命周期,不是"按表的数据面读写点";db.js 的删除是阶段二 S10(须 A1+A2 全绿后)⇒ 届时该文件整体退场
具名降级 imports.js 预览的本地回退、index.js 索引不可用时的本地子查询、data.js 的本地补列 只在平台读失败 / 索引 truncated 时触发,且每次都 console.warn 具名(⛔ 不静默)—— 这是"不拿半份数据冒充全量"的口径,不是遗漏
第三批 account_video_source / hot_accounts / account_videos / rewrite_log / script_review / storyboard_log / hot_account_ext 的全部落点 ⛔ 本棒按接续入口只做第二批;第三批 = hot_accounts 拆表 + 12 处 JOIN + 4 处 OFFSET + 2 处聚合(主战场)

4.1 一个必须记档的实测事实

🔴 本地库的表名还是旧名:sqlite_master 现算 = …,account_video_analysis,…(22 字符旧名),没有 video_analysis。 原因:第 27 棒在 db.js 的 initDb() 里加的是幂等改名,而它只在插件新代码跑起来之后才生效 —— 实例仍载 0.3.15 ⇒ 改名尚未发生。 影响:迁移工具必须两种名字都能读 ⇒ 已加 resolveLocalTable()(按 sqlite_master 现算,平台名不存在才回退旧名;旧名按拆写常量书写以守住 A4 字面判据)。 ⚠️ 后棒注意:实例一旦跑起新版,该表会被改名;resolveLocalTable 两种形态都能读,⛔ 不要写死。


五 迁移读数(逐表)

表 本地 n 计划 n 首跑插入 重跑 坏行 被剥列 清洗列 平台 n(前 → 后)
account_analysis 1 1 1 ins=0, skip=1 0 0:id 1(content_json) 0 → 1
video_analysis 12 12 12 ins=0, skip=12 0 0..11:id 12(content_json) 0 → 12
  • 稳定键:account_analysis = analysisKey(账号, 正文)(UUIDv5,可复算;实测 e5b7d9a1-826f-55c0-9de6-9eb429070b0b);video_analysis = 天然键 aweme_id(⛔ 不生成)。
  • video_analysis 的平台主键不可为空:aweme_id 为空的行平台侧落不了 ⇒ 迁移会具名拒收(badRows),⛔ 不静默丢。本地实测 0 行(local_rows_without_aweme=0)⇒ 本次无人被拒。
  • json 列按附录 C 的 A 案清洗(13 行全部被包成 JSON 字符串,具名进 coerced[]);⛔ 未执行任何删库。
  • 迁移守「先备份 + 先出清单」(第 27 棒已在位,本棒未重做);本地库 sha256 迁移前后逐字一致。

六 未闭合项(逐条:现象 / 归属 / 下一步)

# 现象 归属 下一步
㈠ 实例内端到端未取证:实例仍载 0.3.15(实例启动早于本棒改动);本棒 host 代码虽 lib/client.js 零改动,但 lib/host/mcn/** 已是新版 —— 实例里跑的还是旧 host 码 部署面 需 47 实例重启(影响 1 个在线用户 cce6d1cd-…,约 1–3 分钟)⇒ 待拍板 ㈠
㈡ 出包 0.5.0 / 投放 / 两机一致(S7–S11)全未开工(包仍 version 0.4.0;共享层仍 0.4.0) 部署面 投放会让新版对所有登录用户可见可启(扩面)⇒ 与 ㈠ 一起排
㈢ 红线仍 23(未归零) 插件侧 属阶段二 S10(删 db.js),前置 = A1+A2 全绿
㈣ 第三批未动:account_videos(362 行)+ hot_accounts(拆表 + 12 处 JOIN + 4 处 OFFSET + 2 处聚合=主战场)+ account_video_source/rewrite_log/script_review/storyboard_log/hot_account_ext 插件侧 排第三棒(见入口 §2「⏭️ 本线后续」)
㈤ json 列案 B(改声明 text,须删库重建)仍待拍板 待用户拍板 越晚做代价越高(迁完再删 = 丢真实数据)
㈥ 平台侧观察项(沿用第 25 棒):非法 json 值返回 reason:"disabled",语义误导(应为 bad-args/invalid-value) 平台侧(⛔ 本棒不改 src/**) 另线立项

七 卫生声明(A7 逐条)

  • ⛔ 未改 D:/github/dsh_shenxian/src/** —— find src -newermt "2026-09-25 15:15" = 0 件(最新 mtime 12:20);src/web/routes/im.ts 零改动。
  • ⛔ 未改用户 home 下的 profile / .pnpm —— 唯一写入 = 该用户自己的插件档位文件 <home>/.dsh/data-plane.json(即阶段一设计的回滚落点);本地库 mcn-plugin.db 只读打开(readOnly: true)。
  • ⛔ 未重启任何服务(47 实例、106、dshs 均未动);⛔ 未 commit / push;⛔ 未投放任何版本(共享层仍 0.4.0,包仍 0.4.0)。
  • ⛔ 未删任何库;⛔ 未删旧库 mcn-plugin.db。
  • ✅ 临时 admin 会话 1 → 删 1 → 残留 0(user_agent='poc-curl2' 清理)。
  • ✅ 47 /tmp/p29* 残留 0(探针目录已在收口时 rm -rf)。
  • ✅ 改动面恰 7 件(见头部变更集);lib/client.js 零改动 ⇒ A6 免跑。

八 下一个棒(唯一)

第三批按表切换(account_video_source + hot_accounts 拆表读写 + account_videos)—— ⚠️ 这批是主战场(12 处 JOIN / 4 处 OFFSET / 2 处聚合),且 hot_accounts 必须拆表(本地 25 列 > 平台 MAX_COLUMNS=20)⇒ 建议先出一份规划棒把「读改写(两次单表查 + 插件内合)+ 写改写(两表分写、无事务、具名 partial)」的口径定死再动手。

本棒落地件:本文件|探针 = tmp/p29/{pre-a.sh,pre-b.sh,run-p29.mjs,orchestrate.sh,local-migrate-check.mjs}(本机副本;run-p29.mjs 亦曾落 47 /tmp/p29/,已清)。


变更记录:2026-09-25 15:1x 立(第 29 棒 · 执行棒)。判据命令与输出原文均取自本轮真机执行,未转述。