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

20 KiB
Raw Permalink Blame History

MCN 数据面接入 · 阶段二 第三批按表切换(2026-09-25 · 第 31 棒 · 执行棒)

执行单:D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md(唯一执行单) 入口:E:/ProgramData/AIProject/aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md §0 / §2 会话:p31-exec-plugin-dataline|域锁 12 个(见 §八 卫生) 结论一句话:部分完成 —— S0/S1/S2 全绿并附真机读数;S3 部分完成(20/23 处);S4–S8 未开工(⛔ 未跳步,按序停在下一条)。三张表一处未切档(切换条件不成立,理由见 §一 ②)。


一 结论

步骤 状态 摘要
S0 只读前置(P1–P9 + 4.2 三条) ✅ 完成 9 条读数全部落地,与单内预期一致;4.2 三条里 ① 与 ③ 取得定论(见 §四)
S1 store.js 扩三张表读写 ✅ 完成 新增 31 个导出(含 D16 映射、双键读、拆表写、游标、窗口归约、索引);node --check rc=0
S2 跨批衔接面 4 处 ✅ 完成 4 处(index.js:3076 + workshop.js:199/202/213)—— 只改 h.account_name 取值方式,rewrite_log 语义一字未动
S3 account_video_source 全量切换 🟡 部分完成(20/23) imports.js 15 处全改 + data.js 2 处 + workshop.js 2 处 + index.js 3 处;剩 1 处(index.js:1558 视频总表,与 S4/S5 同一条 SQL,见 ② )
S4 account_videos 全量切换 ⛔ 未开工 最大表(35 处涉及面)+ 游标/窗口/IN 的结构改写主战场
S5 hot_accounts + hot_account_ext 拆表切换 ⛔ 未开工 5 处写点 + 11 处 JOIN
S6 存量迁移 ⛔ 未开工 理由见 ②
S7 shadow 复验 ⇒ 按表切 platform ⛔ 未开工 见 ②
S8 收口 ✅ 完成 README §一 + 入口 §0/§2 + 本交付件 + 当日日志 + 释放域锁

① 为什么停在 S3 中途:S3 的验收判据是「该表调用点归零(只剩 local 回调 + 具名降级)」。剩下那 1 处(index.js:1558)是一条 SQL 同时涉及三张表(account_videos v + hot_accounts h + account_video_source s 子查询 + SUM() OVER + LIMIT ? OFFSET ?)—— 它同时属于 S3/S4/S5,只有一次改写机会,正确做法是在 S4 时连同 account_videos 一起落(⛔ 先改一半 ⇒ 后棒要在半成品上二次施工)。

② 为什么不「先切了再说」(🔴 本棒最重要的自律):account_video_source 仍有 1 处读点走本地 SQL ⇒ 此刻若把该表切 platform,写会进平台、而那一处仍读本地 ⇒ 出现「写到了没人读的地方」——正是 04 单 §五 S2 与父单反复点名的最隐蔽坏态(界面完全正常)。⇒ 切换条件不成立,本棒一处未切。同理,S6 存量迁移按 04 单 §五 顺序排在 S5 之后(⛔ 不许跳),本棒未做任何迁移、未写任何平台行。

③ 如实记账:本棒未触达任何用户可见行为变化 —— 三张表的档位仍是 local,转换过的 20 处走的仍是 local 回调里的原 SQL(逐字保留)⇒ 零回归、可回滚。


二 判据表(命令 + 输出原文 + 退出码)

A9 红线零新增(✅ 达成)

$ cd "$PKG" && grep -rEn "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)" . | grep -v /node_modules/ | wc -l
23

⇒ 23,与第 27/28/29 棒同值。本批新增 DDL = 0。(归零属阶段二收口 S10,⛔ 不属本批。)

S1 验证(✅ 达成)

$ "$NODE" --check "$PKG/lib/host/mcn/{store,data,imports,index,workshop,video-tools}.js"
store.js rc=0 · data.js rc=0 · imports.js rc=0 · index.js rc=0 · workshop.js rc=0 · video-tools.js rc=0

$ cd "$PKG" && "$NODE" --test tests/mcn-store.test.mjs
# tests 49
# pass 49
# fail 0

⇒ 旧 40 例全过 + 新增 9 例(㉞–㊷)全过 / 0 败(单内要求 ≥6 例)。

A3 结构改写面处置(🟡 部分:S3 相关已处置,S4/S5 相关未动)

$ grep -rniE "\bJOIN\b" "$PKG/lib" | grep -v "\.join(" ; grep -rn "LIMIT ? OFFSET ?" "$PKG/lib" ; grep -rnE "ROW_NUMBER|OVER\s*\(" "$PKG/lib"
类型 全包 第三批 本棒处置
SQL JOIN 137 行(含四批全部) 11(含 hot_accounts) 0(属 S4/S5,⛔ 未开工)
LIMIT ? OFFSET ? 6 3 0(属 S4,未开工)
窗口函数 3 3 0(属 S4;插件内归约能力已在 S1 落地并由单测 ㊴ 钉住)

⇒ ❌ A3 未达成(判据要求"涉及三张表的 JOIN / OFFSET / 窗口函数全部处置")。

A1/A2/A4/A6/A8(❌ 未达成 —— 逐条写明理由,⛔ 不含糊过关)

# 判据 本棒读数 理由 / 归属
A1 存量迁移一致性 ❌ 未达成 本棒未执行任何迁移(S6 未开工)。⇒ 三表本地/平台计数无可比读数。归属:插件侧未做,非缺陷
A2 三表读写 E2E(含实例内) ❌ 未达成 ① 本棒未切档(见 §一 ②)⇒ 插件路由仍走本地;② 实例内读数需 47 实例重启 ⇒ 属待拍板㈠(影响 1 个在线用户 cce6d1cd-…,约 1–3 分钟)⇒ 本棒不承诺
A4 表名对齐彻底 🟡 一半达成 grep -rE "account_video_analysis" "$PKG" | grep -v /node_modules/ | wc -l ⇒ 0 ✅(第 28 棒成果,本棒复核);grep -c "hot_account_ext" "$PKG/lib" ⇒ > 0 ✅(S1 新增 TABLES/拆表四函数引用)
A5 已按表切档(三张表) ❌ 未达成 真机读 data-plane.json = 4 表 platform(第一二批)⇒ 三张目标表仍 local(本棒未切,理由见 §一 ②)
A6 移动端五条 ✅ 免跑(有文件清单依据) 改动面恰 6 件:lib/host/mcn/{store,data,imports,index,workshop}.js + tests/mcn-store.test.mjs ⇒ lib/client.js 零改动
A7 卫生 ✅ 达成 见 §八
A8 两机一致 ⚠️ 未达成(如实) 本批未出包、未投放 ⇒ 共享层仍 0.4.0、实例仍载 0.3.15 ⇒ 代码面/运行面都还是旧的;⛔ 不得写"一致"
A4 旧库备份在位 ✅ 达成 见 §四 P7

三 S2 跨批衔接面(4 处 · ✅ 完成)

对象:index.js:3076(/mcn/api/recent-scripts)+ workshop.js:199/202(listCustomScripts 计数+列表)/213(getCustomScriptRow)。 4 处 SQL 共同形如 … FROM rewrite_log r LEFT JOIN hot_accounts h ON h.id = r.account_id ⇒ 取 h.account_name。

处 改前 改后
workshop.js:202(列表) export function listCustomScripts(d, …) 直接返回 items export async function listCustomScripts(…) ⇒ 拿到行后 await applyCurrentAccountNames(d, items) 覆盖显示名;SQL 逐字保留
workshop.js:213 export function getCustomScriptRow(d, id) 直接返回 row 同上(改 async + 一行覆盖)
index.js:3076 json(res, 200, { ok: true, items }) const named = await applyCurrentAccountNames(d, items); json(res, 200, { ok: true, items: named.rows, staleNames: named.staleNames, nameMode: named.mode })
workshop.js:199(计数) SELECT COUNT(*) c … LEFT JOIN hot_accounts h … ⚠️ 未改(有意):这一处 h.account_name 只出现在 account 过滤条件里、不产出任何显示值 ⇒ 不属"账号名显示为旧值"那类;其风险(新账号名缺席)与 §三 ③ 的口径缺口同源,一并记账

为什么不能留给第四批:本批把 hot_accounts 的写切平台后,本地 hot_accounts 不再被写 ⇒ 这 4 处若仍只读本地 JOIN,账号名会永久停在切档那一刻(R11 净变差,且界面看起来完全正常)。 ⛔ 未动 rewrite_log:它的读写点、ORDER BY r.id DESC、WHERE 条件全部一字未改(仍留本地 / 留第四批)。

🔴 本棒实测发现的一条口径缺口(如实报告 · 未擅自改口径):04 单 D16 定的「插件内映射(本地 id ↔ account_name)」只能来自本地(平台侧没有自增 id 列)⇒ 映射的新鲜度上限 = 本地 hot_accounts 最后一次被写的时刻。本棒的落地形态因此加了对账:覆盖值再与平台 hot_accounts 的现存键集合比对,对不上者进 staleNames[] 具名报告(⛔ 不静默、⛔ 不假装是最新名)。单测 ㊷ 钉住三态(local 零变化 / 映射名 + 具名 / 映射名命中平台 ⇒ applied)。


四 S0 只读前置读数(9 条 · 逐条对上期望)

# 事实 命令(本机 / 真机 47) 输出原文(摘要) 对期望
P1 红线现状 见 §二 A9 23 ✅
P2 第三批逐处落点(现算) "$PY" "$WS/tmp/p30/scan-batch3-live.py" 82 处:data.js9/imports.js36/index.js24/video-tools.js1/workshop.js12 ✅
P3 结构改写面三种 见 §二 A3 全包 JOIN 137 / OFFSET 6 / 窗口 3 ✅(口径修正见单内附录 D)
P4 hot_accounts 列面 PRAGMA table_info('hot_accounts') 25 列(建表 14 + ALTER 11)—— 逐列已列印(id…is_ai) ✅ 与单内定论一致
P5 空键 / 重复键分布 COUNT(*) WHERE aweme_id IS NULL OR aweme_id='' 等 account_video_source 空键 0|account_videos 空键 0|hot_accounts 空名 0|跨 account_id 重复 aweme_id 0 组 ✅ ⇒ 本批 badRows 预期为零(代码仍须拒收)
P5b 逐表行数 SELECT COUNT(*) hot_accounts 5/account_videos 362/account_video_source 39(=第 27 棒清单) ✅
P6 account_id 类型 PRAGMA table_info('account_videos') account_id **INTEGER**(声明面 = text) ✅ ⇒ D20 成立(写入一律 String(),单测 ㊵ 钉住)
P7 旧库备份在位 ls + tar -tzf + sha256sum …-mcn-plugin-db-20260925-141436.tar.gz 809,732 B/sha256 6b09e368…/可解出 mcn-plugin.db;源库 sha256 b37f8aab… · 2,383,872 B · mtime 2026-09-20 23:05:05 ✅ ⇒ ⛔ 本批未重做备份(单内要求)
P8 按表档现状 读 <home>/.dsh/data-plane.json tables = account_persona/creative_log/account_analysis/video_analysis 四项均 platform;updatedAt 2026-09-25T07:29:58.942Z ✅ ⇒ 三张目标表仍 local
P9 🔴 平台 find 谓词能力(4.2 ① · 本棒给结论) 读 src/db/plugin-data/runtime.ts:172-211(只读,⛔ 未改) buildWhere() 现算:归属列外只生成两种谓词 —— col = $n(eq 多列 AND 可用 ✅)与 col IS NULL(传 null 时);无 neq/IN/范围/LIKE/OR;未知列 ⇒ bad-args ✅ 与单内"按只支持等值设计"一致 ⇒ ⛔ 未简化任何落点

§四 4.2 三条的结论:

  • ① P9 谓词能力 ⇒ 有结论:eq 支持多列 AND(比单内假设略宽,但未据此简化任何改写;⛔ 单内明确"简化 = 改动面变化 = 需重过落点表");
  • ② P5 三个存量分布 ⇒ 有结论:三处全为零 ⇒ 本批无需"人工裁定";
  • ③ hot_accounts 的 5 处写点是否都拿得到 account_name ⇒ 未取得完整结论:imports.js:49/53、data.js:24、index.js:2796、workshop.js:335 逐处未做运行时取证(属 S5 的前置,本棒未开工该步)。⇒ 列为未闭合项。

五 S3 逐处落点(20/23 · 改前 → 改后)

统一形态:store.readRows/readCount/writeThrough({ what, table: "account_video_source", platform: () => store.xxx(…), local: () => <原 SQL 逐字保留>, keyOf })。

文件 处 原形态 改后形态
imports.js readVideoSource 库兜底 SELECT content_json AS content, analysis_time … WHERE video_id=? OR (aweme_id IS NOT NULL AND aweme_id=?) ORDER BY id DESC LIMIT 1 store.latestVideoSource({awemeId, videoId})(aweme_id 直取 → video_id 拉页归约)
补录去重+写 SELECT id … WHERE (video_id=? OR …) AND content_json=? + INSERT / UPDATE analysis_json WHERE id=? store.writeVideoSource(row, { local })(去重 + 写;replace:false)
readVideoParse 库兜底 SELECT content_json, analysis_json … store.latestVideoSource()
backfillVideoSource(3 处) WHERE (video_id IS ? OR aweme_id IS ?) AND content_json=? + INSERT + SELECT analysis_json WHERE id=?/UPDATE … WHERE id=? store.writeVideoSource();local 回调内保留 IS 形态与 added++ 语义
importExternalDir 补录 … AND content_json=? + INSERT store.writeVideoSource(),analyses++ 移入 local 回调
importExternalData 删旧保新 SELECT dup + DELETE … WHERE video_id=? OR (…) + INSERT store.writeVideoSource(row, { replace: true }) ⇒ 平台档:先双键删旧、删旧失败不插新(⛔ 不留两份)
previewExternalDir 预览判据 SELECT id … AND content_json=? store.analysisExists({ table: "account_video_source", … })(与第 29 棒同手法)
data.js 列表 has_source 列 平台行按 SELECT COUNT(*) … WHERE aweme_id=? 读本地 store.loadVideoSourceIndex() + store.videoSourceCountOf()(一次拉全量建索引,⛔ 不逐行 N 次请求;truncated ⇒ 具名降级回本地)
workshop.js deepVideos 连坐删 2 处 事务内 DELETE FROM account_video_source WHERE aweme_id=? / WHERE video_id=? 平台先删(事务前):store.removeVideoSource({awemeId, videoId}),与 video_analysis 共用一份预取行;本地 SQL 保留在事务内(local 档路径)
index.js taskDone(dsh_video-analyze-*) SELECT COUNT(*) c … WHERE video_id=? OR aweme_id=(SELECT …) 直读本地 store.readCount({ table: "account_video_source", platform: () => store.countVideoSources({awemeId, videoId}) })
/video/script SELECT content_json … ORDER BY id DESC LIMIT 1 store.readRows + store.latestVideoSource()
复盘「源脚本正文」readSource() 同上(sync 内联) 改 async + 走 store.readRows(唯一调用方 await)
index.js:1558 视频总表(account_videos + hot_accounts + account_video_source 子查询+SUM() OVER+OFFSET) ❌ 未改 与 S4/S5 同一条 SQL ⇒ 在 S4 一次改到位(理由见 §一 ①)

语义差异(如实记账):平台主键 = aweme_id 且不可为空 ⇒ backfillVideoSource 中「vid 为空(目录没匹配到视频行)」那条记录在平台档下具名拒绝(本地档照旧插入)。实测该形态 0 行 ⇒ 本批无实际影响。


六 产物指纹

文件 字节 行尾 CR sha256(前 16)
lib/host/mcn/store.js 91,585 0(LF) 2e7668c140bb592b
lib/host/mcn/data.js 8,923 0(LF) 11521eb897d91783
lib/host/mcn/imports.js 45,258 825(全 CRLF,与基线同) 1cf01e06b1db543e
lib/host/mcn/index.js 194,765 0(LF) 8105fde131a2792f
lib/host/mcn/workshop.js 22,671 0(LF) 58c9b512a0dce1c5
tests/mcn-store.test.mjs 43,458 0(LF) 8012adc6cef5be7e
dsh.data.yaml 未改(本批不动声明面) — —

🔴 行尾判定(字节级现算,⛔ 不凭记忆):index.js/workshop.js 在 0.3.13 基线包里就是纯 LF(index.js: CRLF 0 / bareLF 3095;workshop.js: CRLF 0 / bareLF 303)⇒ 本棒未改变任何文件的行尾约定;imports.js 基线本来就是全 CRLF(0.3.13 内 CRLF 592 / bareLF 0),本棒改后仍全 CRLF(825 行 CRLF、bareLF = 0)。⛔ 无混行尾。


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

  1. S3 剩 1 处未改:index.js:1558(视频总表,三表混写 + 窗口 + OFFSET)。归属 = 插件侧未做。下一步 = 与 S4 一起改(S4 首个动作)。
  2. S4 / S5 / S6 / S7 全未开工。归属 = 插件侧。下一步 = 按 04 单 §五 顺序继续(第 32 棒)。
  3. D16 映射的"新鲜度上限"(§三):平台档下切档之后新建的账号拿不到本地 id ↔ account_name 映射 ⇒ 依赖该映射的读点会走具名降级(staleNames / mapping-missing)。归属 = 口径缺口(规划侧定 D16 时未闭合)。下一步 = 需在 S5 落码前拍一次口径:① 维持 D16(接受新账号缺席),或 ② 让 hot_accounts 的写顺带把 (id, account_name) 落一张本地键注册表**(⛔ 不是双写业务数据 ⇒ 无"以谁为准"歧义;好处:映射恒新鲜、account_videos.account_id 恒可解析;代价:本地仍保留一张极小表)。⛔ 本棒未擅自改口径,按 D16 落码并留下具名降级。
  4. S5 前置未取证:hot_accounts 的 5 处写点是否全部能拿到 account_name(拆表同键不可为空)。归属 = 插件侧未取证。下一步 = S5 第一步。
  5. A2 / A8 未达成:A2 需 47 实例重启窗口(待拍板㈠)+ 本批切档;A8 需出包投放(属阶段二收口 S9–S11)。归属 = 部署面 / 待拍板。
  6. 平台侧缺陷面(⛔ 本棒未改 src/**,只报告):src/db/plugin-data/runtime.ts 对非法值返回的 reason 仍是笼统的 disabled 语义(第 25 棒已记档,本棒复核仍在)。归属 = 平台侧。下一步 = 平台线单独排棒。

八 卫生声明(A7 逐条点名)

项 结果 依据
⛔ 改 D:/github/dsh_shenxian/src/** 0 件 find "$SRC" -newermt "2026-09-25 15:58" -type f | wc -l ⇒ 0(只读读了 runtime.ts)
⛔ 改 im.ts 零改动 同上(src/web/routes/im.ts 未在改动面)
⛔ 改 D:/dshworkspace 下其它文件 仅 6 件(本批改动面) find "$PKG" -newermt "…" -type f | sed "s|$PKG/||" ⇒ 恰 6 件
⛔ 手改用户 home 下 profile / .pnpm 未触碰 本棒全程本机,⛔ 未 ssh 改任何文件
⛔ 重启任何服务 未重启 47 只跑只读 PRAGMA / sha256sum / ls / cat;106 全程未登录
⛔ 投放任何版本 未投放 共享层仍 0.4.0;dist/ 未新增包
⛔ commit / push 未做 D:/dshworkspace 与文档库均未提交
⛔ 删任何库 未删 未执行任何迁移、未写任何平台行;旧库 mcn-plugin.db 原样在盘(sha256 b37f8aab… 与迁移前一致)
⛔ 未拍板前执行删库 / 重建 未做 json 列案 B(删库重建)未出现在任何执行面
✅ 临时库 / 残留清理 47 上 0 处写入(只读脚本经 stdin 管道执行,未落文件);本机探针落 tmp/p31/** —

九 域锁

项 值
会话名 p31-exec-plugin-dataline
域(12 个) dsh-server-docs/05-交接单 · aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md · aliyun-dsh-server/.workbuddy · aliyun-dsh-server/交付物 · mcn/store.js · mcn/data.js · mcn/imports.js · mcn/index.js · mcn/workshop.js · mcn/video-tools.js · tools/migrate-local-to-platform.mjs · tests/mcn-store.test.mjs
复核方式 退出码 rc=0 + 复读 .locks/p31-exec-plugin-dataline/{OWNER,DOMAINS} 断言归属(⛔ 不看输出、不接管道)
门禁备注(如实) preflight-lock.sh 对 11 个目标路径全判 【D】未归类(工作区根文件 + 文档库 05-交接单 + D:/dshworkspace 下的插件包路径都不在该脚本锚点覆盖范围内)⇒ 按脚本字面应 rc=1;照本线既有惯例(第 27–30 棒同一情形)开工并报告,⛔ 未改那个跨库脚本