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

105 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MCN 数据面接入 · 阶段二第三批 —— **接线完成**(第 34 棒 · 执行棒 · 2026-09-25)
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server` **会话**:`p34-exec-plugin-dataline`(`d25f7943-e143-4f7f-a595-8b090fed6e3f`)
> **唯一执行单**:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md`
> **上游拍板**:用户 2026-09-25 20:38 —— 「**修改完成后 一起上服务器**」⇒ **B 案**(接线全做完再一次性动生产)。
---
## 一 结论(一句话)
**S3 剩 1 处 + S4 + S5 的插件侧接线已全部落地**:`account_videos` / `hot_accounts` + `hot_account_ext` / `account_video_source` 三张表在 `platform` 档下**处处有改写面**,`local` 档行为与原 SQL 逐字一致(零回归)。静态复扫确认**全包已无「未被 `local()` 回调包住的本地 SQL」**。单测 **62/62**、红线 **23(零新增)**、`src/**` 零改动。
⛔ **本棒未动生产**(不出包 / 不上池 / 不加列迁移 / 不回填)—— 按用户拍板属 B 案最后一步,本轮**未做**。
⛔ **未切档**:`local_id` 列在真机库**仍未就位**(切档前置)⇒ A5 未达成、A2 未达成(需实例重启窗口)。
---
## 二 改动面(6 件 · 全部在插件包内)
| # | 文件 | 字节 | 行尾 | sha256(前 16) | 本棒改了什么 |
|---|---|---|---|---|---|
| 1 | `lib/host/mcn/store.js` | 111,028 | LF | `263c245627bfadce` | 新增 `patchHotAccountSplitByName()`(**拆表 PATCH**:主表列/扩展列各一半,无事务 ⇒`partial`+`compensate`)+ `patchHotAccountSplitByLocalId()`(经 D16 映射)|
| 2 | `lib/host/mcn/index.js` | 214,801 | LF | `e0178cb16fe57f8c` | **22 处落点**:新增 6 个插件内共用原语(`nameMapOf`/`hotAccountOf`/`patchHotAccountOf`/`videoOf`/`awemeIdOfVideo`/`accountNameOf`)+ 逐处接档(见 §四) |
| 3 | `lib/host/mcn/data.js` | 16,872 | LF | `e3f6de684b59d980` | (第 34 棒前段)`queryAccountsPlatform` 等;本棒为复扫复核对象 |
| 4 | `lib/host/mcn/imports.js` | 57,547 | CRLF | `0340c97fe0713e45` | (第 34 棒前段)`findHotAccountByDelFlagEq` / `loadVideoWithAccount` + 36 处接档 |
| 5 | `lib/host/mcn/workshop.js` | 31,495 | LF | `4fc7822d5c5e17ba` | (第 34 棒前段)`accountFilterOf` / `softDeleteHotAccountPlatform` / `batchDeleteCascade` 平台面 |
| 6 | `tests/mcn-store.test.mjs` | 57,661 | LF | `e988cd97d79205ef` | **新增 4 例**(52–55):拆表 PATCH 表路由 / 两半失败 `partial` / 映射缺席 `mapping-missing` / `local` 档零请求 |
**未改**:`dsh.data.yaml`(指纹 `d6627f1c647a8c27` / 8,351 B —— 与第 33 棒逐字相同)· `tools/migrate-local-to-platform.mjs`(`060bbf855f624fdd` / 12,886 B)· `lib/client.js`(**零改动** ⇒ A7 免跑)· `D:/github/dsh_shenxian/src/**`(**零改动**)。
---
## 三 判据表(A1–A9 · 命令 + 输出原文 + 退出码)
| # | 判据 | 命令(原文) | 输出原文 | 退出码 | 结论 |
|---|---|---|---|---|---|
| **A1** | 存量迁移一致性 | ——(属第 32 棒读数,本棒**未重跑**) | `account_videos` **362/362 `match=true`** · `hot_accounts` **5/5 `match=true`** · `account_video_source` **21/39 上台 · `match=false`**(余 12 行卡平台**单行** 64 KiB) | — | **部分达成**(12 行卡 64 KiB ⇒ 由 (a) 行内压缩处方闭合,**代码未落**) |
| **A2** | 三表读写 E2E(**实例内**) | —— | 需 **47 实例重启**(待拍板 ㈠) | — | ⛔ **未达成**(属窗口项) |
| **A3** | 结构改写面处置完毕 | ① `grep -rniE "\bJOIN\b" lib \| grep -v "\.join("` ② `grep -rn "LIMIT ? OFFSET ?" lib` ③ `grep -rnE "ROW_NUMBER\|OVER\s*\(" lib` | ① 命中行**全部**落在三类:`local()` 回调内 / 本地专属分支 / **第四批主表 `rewrite_log`(具名豁免)**;平台档路径**零命中** ② `data.js:183` `data.js:239` `index.js:1796` `workshop.js:335` —— 前两条在本地专属路径、后两条在 `local()` 回调内 ③ `index.js:1722`(`where.push`,**仅本地分支使用**)· `index.js:1787`(本地 SQL)· `index.js:3245`(`local:` 回调内) | 0 / 0 / 0 | ✅ **全部处置**(归零或具名豁免) |
| **A4** | 表名对齐彻底 | `grep -rE "account_video_analysis" . \| grep -v /node_modules/ \| wc -l`;`grep -rn "hot_account_ext" lib \| wc -l` | `0`;`13`(其中 `store.js` **10**) | 0 / 0 | ✅ 通过 |
| **A5** | 已按表切档(三张表) | —— | 真机 `data-plane.json` 三表**仍 `local`** | — | ⛔ **未达成**(**有意**:`local_id` 列未就位 ⇒ 切档 = 新行没 id ⇒ 前端 6 入口失效) |
| **A6** | 旧库备份在位 + 零数据损失 | ——(第 27 棒备份已在位) | 本棒**未动** `mcn-plugin.db`(⛔ 未删 · 未改) | — | ✅ **维持**(旧库 sha256 未取 —— 本棒无文件级操作) |
| **A7** | 移动端五条 | —— | **本棒未改 UI**:`lib/client.js` 零改动;改动面 6 件全在 `lib/host/mcn/**` + `tests/**` | — | ✅ **免跑**(文件清单依据 = §二) |
| **A8** | 两机一致 | —— | **本棒未出包、未投放** ⇒ 代码面/运行面**都还是旧的** | — | ⛔ **未达成**(⛔ 不得写「一致」) |
| **A9** | 红线零新增 | `grep -rEn "CREATE TABLE\|ALTER TABLE\|DROP TABLE\|better-sqlite3\|from 'pg'\|require\('pg'\)" . \| grep -v /node_modules/ \| wc -l` | `23`(全部落在 `./lib/host/mcn/db.js`) | 0 | ✅ **23**(与第 27/28/29/33 棒同值) |
---
## 四 逐处落点(index.js 22 处「改前 → 改后」)
**口径**(入口 §2 硬约束①):切换的**最小原子单位 = 表**;`local` 档 = **原 SQL 逐字**(保留在各 `local()` 回调里)。
| 处 | 行 | 改前(原文) | 改后形态 |
|---|---|---|---|
| 1 | `:852` | `platform: () => … awemeId: d.prepare('SELECT aweme_id FROM account_videos WHERE id=?')…` | `platform: async () => … awemeId: await awemeIdOfVideo(d, videoId)` |
| 2 | `:861` | 同上(`countVideoAnalysis`) | 同上 |
| 3 | `:1573` | `UPDATE hot_accounts SET favorite=? WHERE id=?` + `r.changes===0 ⇒ 404` | `store.patchHotAccountByLocalId(…)`(`local` 回调保原 SQL);`mapping-missing` 或 `changes===0` ⇒ 404(**本地语义逐字保留**) |
| 4 | `:1811/1813` | `SELECT * FROM account_videos WHERE id=?` + `SELECT * FROM hot_accounts WHERE id=?` | `videoOf(d, videoId)` + `hotAccountOf(d, v.account_id)` |
| 5 | `:1959` | `platform` 分支内 `d.prepare('SELECT aweme_id FROM account_videos WHERE id=?')` | `await awemeIdOfVideo(d, videoId)` |
| 6 | `:2061` | `SELECT video_title FROM account_videos WHERE id=?` | `videoOf(d, videoId, { sql: 'SELECT id, aweme_id, video_title …' })`(⚠️ 唯一差异:本地 SELECT 追加 `id, aweme_id` —— `getVideoByLocalId` 平台档靠它换轨;调用方仍只用 `video_title`) |
| 7 | `:2072` | `platform` 分支内 aweme_id 子查询 | `await awemeIdOfVideo(d, videoId)` |
| 8 | `:2397` | `SELECT * FROM account_videos WHERE id=?` | `videoOf(d, rw.video_id)` |
| 9 | `:2624` | `SELECT * FROM hot_accounts WHERE id=?` | `hotAccountOf(d, accountId)` |
| 10 | `:2675` | `WHERE id=? AND (del_flag IS NULL OR del_flag <> 1)`(**非等值 ⇒ 下推不了**,D15) | `hotAccountOf(…)` + 插件内 `Number(accRow.del_flag ?? 0) !== 1`(**逐字复刻**) |
| 11 | `:2814` | `SELECT id, account_name, douyin_id, source_dir FROM hot_accounts WHERE id=?` | `hotAccountOf(d, id, { sql: <原 SQL> })` + `id: accRow.id ?? id` |
| 12 | `:2720`(按 `v.id`) | `SELECT v.*, h.account_name … LEFT JOIN hot_accounts h … WHERE v.id=?` | `store.readRows({ table:"account_videos", platform: <videoOf + 映射补名>, local: <原 JOIN SQL 逐字> })` |
| 13 | `:2727`(按 `aweme_id`) | 同形 JOIN(`WHERE v.aweme_id=?`) | 同上(`findVideoByAwemeId` + `videoLocalIdMap` 补本地 id) |
| 14 | `:2869` | `SELECT v.id, v.aweme_id, v.video_title, v.account_id, h.account_name … JOIN …` | `store.readRows({ … local: <原 SQL 逐字> })` |
| 15 | `:2939` | `SELECT COUNT(*) AS c FROM hot_accounts` | `store.countHotAccounts()`(平台档 = 平台口径总数) |
| 16 | `:2985` | `SELECT id FROM hot_accounts WHERE id=?` | `hotAccountOf(d, id, { sql: <原 SQL> })` |
| 17 | `:3003` | `UPDATE hot_accounts SET ${sets.join(", ")} WHERE id=?`(**动态 SET**) | 按 `allowed` 逆推 `patch` 对象 ⇒ `patchHotAccountOf()`(**拆表 PATCH**:`douyin_id`/`followers`/`bio`… 归 `hot_account_ext`);⛔ **`sets` 与本地 SQL 一字未动** |
| 18 | `:3075` | `SELECT COUNT(*) c FROM hot_accounts WHERE del_flag=0` | `store.readCount({ table:"hot_accounts", platform: <全量 + 插件内复刻 NULL 语义>, local: <原 SQL> })` |
| 19 | `:3077` | `ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY like_count DESC) rn<=10`(**在 `c(\`…\`)` 里**) | 同法:平台档全量 + 插件内按账号分组排序取前 10(⛔ 不是 `/count`) |
| 20 | `:3317` | `SELECT * FROM hot_accounts WHERE id=?` + `(acc.del_flag ?? 0) === 1` | `hotAccountOf(…)` + `Number(acc.del_flag ?? 0) === 1` |
**D16 换轨**:平台无自增 `id` ⇒ 账号走 `id↔account_name`(`accountNameMap`)、视频走 `id↔aweme_id`(`videoLocalIdMap` / `getVideoByLocalId` 内部换算);**映射缺席一律具名降级**(`mapping-missing` / `row:null` + `detail`),⛔ 不猜、⛔ 不拿空值冒充。
---
## 五 未闭合项(逐条:现象 / 归属 / 下一步)
1. **`local_id` 列在真机库仍未就位** —— 现象:A5 切档前置不满足;归属 **部署面**;下一步 = B 案最后一步(出包 → 上池 → **2 条 `ADD COLUMN`** → 重跑迁移回填 `local_id`)。
2. **12 行 `account_video_source` 正文超平台单行 64 KiB** —— 现象:`A1 match=false`;归属 **插件侧**(处方 (a) 行内压缩);下一步 = 落 gzip 编解码到 `normalizeRow`/`rowsOf` 两个唯一必经之路(**本棒未落**)。⚠️ 真机读数已在位:12 行**全为纯文本**(`base64runs=0` / `data:image=0`)、gzip 后最大 **41,728 B** ≤ 64 KiB ⇒ **(a) 有效,无需改走 (b)**。
3. **D16 映射新鲜度缺口** —— 现象:`hot_accounts` / `account_videos` 的**写**切平台后本地不再新增行 ⇒ 映射缺席;归属 **插件侧**(已知边界,已在每处**具名降级**);下一步 = `local_id` 就位后把映射改为「优先平台、缺则降级本地」。
4. **`account_videos` 列表页在有账号级过滤时仍走本地 SQL** —— 现象:`index.js` `/mcn/api/videos` 的平台档分支**已由第 32 棒落地**(`buildVideosPlatform()`),本棒只补 `aweme_id` 换轨;归属 **插件侧**;下一步 = `shadow` 档真机比对(S7)。
5. **`imports.js` 有一处未使用的 `findVideoByFolder` 导入** —— 现象:仅 `db.js:264` 调用(同步版,属 S10 收口);归属 **插件侧**;下一步 = S10 随 `db.js` 一并退场(⛔ 本批不动)。
6. **自动化记忆目录缺失** —— 现象:`.workbuddy/memory/automations/40d342f3-…/` 首次运行前不存在(本棒已创建);归属 **本机工具面**;无后续动作。
---
## 六 卫生声明(A7 逐条点名)
- `lib/client.js` **零改动** · `web/**` **零改动** · `skills/**` **零改动** ⇒ **未改 UI,移动端五条免跑**。
- 本棒 tmp 产物:`tmp/p34/{patch-index.py,fix-script.py,add-tests.py,triage.py,ix-slice.txt,scan12.py,scan12.out,*.bak-p34}`。
- 真机只读探针:本棒**只做只读取数**(`tmp/p34/scan12.py`,`find` + 本地 sqlite 只读),**未在实例内留下任何残留**。
---
## 七 待拍板(**不影响本棒结论**)
1. **㈠ 47 实例重启窗口** —— 要用你定:**什么时候可以重启线上实例**。为什么需要你定:重启会让**正在用平台的用户**短暂断线(分钟级)。**A 案**:约一个低峰时段(优点:影响最小;缺点:要等)。**B 案**:现在重启(优点:立刻能验完 A2/A5;缺点:在线用户会被打断)。**我的倾向**:A 案(可推翻)。
2. **㈡ 106 重启窗口** —— 同上,可合并到同一次窗口。
3. **㈢ `json` 列是否改声明** —— 不可逆;本批**不需要**(不属第三批)。
4. **㈣ 插件包源码是否纳入版本管理** —— 现状:6 件改动**无版本留痕**(第 34 棒已第 4 次累积)。