范围 = 本线(插件投放与分库线)产物 + 记忆类,共 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/规则与载体/、归档/、接续包_*。
20 KiB
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)。⛔ 无混行尾。
七 未闭合项(逐条:现象 / 归属 / 下一步)
- S3 剩 1 处未改:
index.js:1558(视频总表,三表混写 + 窗口 + OFFSET)。归属 = 插件侧未做。下一步 = 与 S4 一起改(S4 首个动作)。 - S4 / S5 / S6 / S7 全未开工。归属 = 插件侧。下一步 = 按 04 单 §五 顺序继续(第 32 棒)。
- D16 映射的"新鲜度上限"(§三):平台档下切档之后新建的账号拿不到本地
id ↔ account_name映射 ⇒ 依赖该映射的读点会走具名降级(staleNames/mapping-missing)。归属 = 口径缺口(规划侧定 D16 时未闭合)。下一步 = 需在 S5 落码前拍一次口径:① 维持 D16(接受新账号缺席),或 ② 让hot_accounts的写顺带把(id, account_name)落一张本地键注册表**(⛔ 不是双写业务数据 ⇒ 无"以谁为准"歧义;好处:映射恒新鲜、account_videos.account_id恒可解析;代价:本地仍保留一张极小表)。⛔ 本棒未擅自改口径,按 D16 落码并留下具名降级。 - S5 前置未取证:
hot_accounts的 5 处写点是否全部能拿到account_name(拆表同键不可为空)。归属 = 插件侧未取证。下一步 = S5 第一步。 - A2 / A8 未达成:A2 需 47 实例重启窗口(待拍板㈠)+ 本批切档;A8 需出包投放(属阶段二收口 S9–S11)。归属 = 部署面 / 待拍板。
- 平台侧缺陷面(⛔ 本棒未改
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 棒同一情形)开工并报告,⛔ 未改那个跨库脚本 |