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

189 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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 数据面接入 · 阶段二 **第三批:真实阻塞取证 + 存量迁移**(2026-09-25 · 第 32 棒 · 执行棒)
> **上游**:`04-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md`(唯一执行单 · §五 S0–S8 / §六 A1–A9)
> **上一棒**:`交付物/MCN数据面接入-阶段二-第三批按表切换-20260925.md`(第 31 棒 · S0/S1/S2 完成 · S3 20/23)
> **入口**:`接续入口_插件投放与分库线_20260922.md`(§0 最新行 + §2「🎯 本轮动作」)
> **变量**:`PKG=D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite`;`NODE=E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3/node.exe`;`PY=E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe`;`47=bt-server(47.77.182.89)`
---
## 一 结论(一句话)
**第三批的"四步链路"走完了它的前两步,但"逐表切 `platform`"这一步在三张表上**全部**不成立 ——
不是在等代码改完,而是撞到两处**结构性阻塞**:`account_videos` / `hot_accounts` 缺**本地自增 `id` 的承载位**(前端以 `row.id` 为主标识),`account_video_source` 有 **18/39 行正文超过自己声明的 30 KB 上限**(最大 94,889 B)。
⇒ **本棒不宣称第三批完成**;`A5`(三表切档)未达成,`A2` 未达成(需实例重启窗口),其余判据见 §三。**
---
## 二 本棒完成项
| # | 项 | 状态 |
|---|---|---|
| 1 | `index.js` `/mcn/api/videos`(**04 单点名的"本棒唯一入口"**)平台档取数路径 | ✅ 落地(平台无 JOIN/窗口/OFFSET ⇒ 插件内归约;`local` 档逐字不变) |
| 2 | `store.js` 新增 `hotAccountMetaMap()`(`id ↔ {account_name, content}`,**按表自己的档**取) | ✅ |
| 3 | `store.js` 新增 `videoLocalIdMap()`(本地 `id ↔ aweme_id` 双向映射) | ✅ |
| 4 | 单测:49 → **51 / 51**(新增 ㊸ `hotAccountMetaMap`、㊹ `videoLocalIdMap`) | ✅ |
| 5 | 迁移工具扩表:`DEFAULT_TABLES` **+3 表**、`KEY_BUILDERS` **+3 天然键**、**拆表两写**(`hot_accounts` ⇒ 主表 + `hot_account_ext`)、**重复键预检** `dupKeys` | ✅ |
| 6 | 三表**存量迁移**(47 真机 · 幂等 · 零数据损失) | ✅ 两表全 match,一表受 quota 阻塞(§四) |
| 7 | `node --check` 全绿(`store.js` / `index.js` / `migrate-local-to-platform.mjs`) | ✅ |
**未做(理由不是"没时间",而是"做了会制造死代码")**:`data.js` / `imports.js` / `workshop.js` / `index.js` 其余 ≈ 30 处第三批调用点的平台档接线 ——
它们**全部**以"该表已切 `platform`"为前提;而 §五 证明三表都切不了 ⇒ 现在写等于往生产包里塞**永不被走到**的分支(R11「可维护性」净变差)。⇒ 等 §五 的拍板落地后再一次性接线。
---
## 三 判据表(命令 + 输出原文 + 退出码)
| # | 判据 | 命令 | 输出原文(关键行) | 码 |
|---|---|---|---|---|
| **A9** | 红线零新增 | `grep -rEn "CREATE TABLE\|ALTER TABLE\|DROP TABLE\|better-sqlite3\|from 'pg'" $PKG \| grep -v /node_modules/ \| wc -l` | **23**(=第 27–31 棒同值;本棒零 DDL) | 0 |
| **S1** | 单测 | `$NODE --test tests/mcn-store.test.mjs` | `# tests 51 / # pass 51 / # fail 0` | 0 |
| **S1** | 语法 | `$NODE --check <各改动文件>` | 全 `rc=0` | 0 |
| **A4** | ⛔ 未改平台 `src/**` | 本棒**未 ssh 改任何文件**;`find $SRC -newermt "2026-09-25 16:49" -type f \| wc -l` | **0** | 0 |
| **A7** | 未改 UI | 改动面 = `store.js` / `index.js` / `tools/` / `tests/`;`lib/client.js` **零改动** | ⇒ 移动端五条**免跑** | — |
| **A1** | 存量迁移一致性 | §四(逐表 `本地 n` / `平台 n` / `onlyLocal` / `onlyPlatform`) | `account_videos` **match=true**;`hot_accounts` **match=true**;`account_video_source` **match=false** | 0 |
| **A5** | 三表切档 | 读 `<home>/.dsh/data-plane.json` | **未达成**(三表均不切;档位已回滚到原 4 表) | — |
| **A2** | 三表读写 E2E | 需 **47 实例重启**(待拍板㈠) | **未达成**(如实记) | — |
| **A6** | 旧库零损失可回滚 | `sha256sum mcn-plugin.db`(迁移前后) | 两次均 `b37f8aab51a90f74…`(2,383,872 B · mtime 未变) | 0 |
| **A8** | 两机一致 | 未出包、未投放 | 代码面/运行面**都还是旧的**(共享层仍 `0.4.0`) | — |
---
## 四 真机读数(47 · 2026-09-25 17:0x · 探针 `tmp/p32/run-p32.mjs` + `orchestrate.sh`)
### 4.1 dry-run 清单(⛔ 零平台写请求 · 实测 `DRY_WRITE_CALLS=0`)
| 表 | 本地行 | 计划 | 坏行 | 重复键组 | 清洗列 | 剥列 |
|---|---|---|---|---|---|---|
| `account_video_source` | 39 | 21 | **18**(全 `quota-exceeded`) | **5** | 0 | `id` |
| `account_videos` | 362 | 362 | 0 | 0 | 0 | `id` |
| `hot_accounts` | 5 | 5 | 0 | 0 | 0 | `id` |
### 4.2 apply + 幂等重跑
```
APPLY [account_video_source] 本地=39 计划=21 插入=18 跳过=3 失败=0 坏行=18
APPLY [account_videos] 本地=362 计划=362 插入=362 跳过=0 失败=0 坏行=0
APPLY [hot_accounts] 本地=5 计划=5 插入=10 跳过=0 失败=0 坏行=0
PART hot_accounts: ins=5 skip=0 fail=0 ← 拆表两写
PART hot_account_ext: ins=5 skip=0 fail=0 ← 扩展表已接线(A4 判据 `grep -c hot_account_ext` > 0)
IDEM [account_video_source] 插入=0 跳过=21 失败=0 ← 幂等重跑全 skip
IDEM [account_videos] 插入=0 跳过=362 失败=0
IDEM [hot_accounts] 插入=0 跳过=10 失败=0
```
### 4.3 A1 比对(本地 vs 平台 · 主键集合)
```
A1 [account_video_source] 本地行=39 平台行=18 onlyLocal=[…5 例…] onlyPlatform=[] match=false
A1 [account_videos] 本地行=362 平台行=362 onlyLocal=[] onlyPlatform=[] match=true
A1 [hot_accounts] 本地行=5 平台行=5 onlyLocal=[] onlyPlatform=[] match=true
```
### 4.4 影子读(`shadow` 档真机)
```
SHADOW [account_videos] source=local mode=shadow degraded=false {"platform":362,"local":362,"match":true}
SHADOW [hot_accounts] source=local mode=shadow degraded=false {"platform":5,"local":5,"match":true}
SHADOW [account_video_source] source=local mode=shadow degraded=false {"platform":18,"local":39,"match":false}
STATS platformCalls=1188 platformOk=1188 platformFailed=0 shadowReads=3 shadowMismatch=1
```
### 4.5 47 侧精确诊断(`sqlite3` 现算)
```
account_video_source: 行=39 空 aweme_id=0 |
content_json > 30720 B 的行=3(最大 78,682 B);analysis_json > 30720 B 的行=15(最大 94,889 B)
重复 aweme_id 组=5 组(每組 2 行):7658997937279683855(20/40) · 7648947073131625905(27/28)
· 7667477088350044069(29/44) · 7631176055600527729(31/42) · 7629803455813243043(32/46)
account_videos: 跨 account_id 重复 aweme_id 组=0 · 空 aweme_id=0
hot_accounts: 空 account_name=0
```
> ⚠️ 第 31 棒预期的 `account_video_source`「空 `aweme_id` 行」实测 **0 行** ⇒ `badRows` 实际由 **`quota-exceeded`** 触发(**预期外**,见 §五-乙)。
---
## 五 🔴 两处硬阻塞(本棒新发现 · ⛔ 未拍板前不得自决)
### 甲 · `account_videos` / `hot_accounts` 缺「本地自增 `id`」的承载位
- **现象**:平台声明面**没有 id 列** —— `dsh.data.yaml` 里 `hot_accounts` 首列 = `account_name`、`account_videos` 首列 = `aweme_id`,且文件头写明"**本地 `id INTEGER PRIMARY KEY` 一律不声明**(平台无代理自增 id)"。
- **取证**:① 声明面现读(13 / 16 列,均无 id);② 插件内核常量 `LOCAL_ONLY_COLUMNS = ["id"]`(`store.js`)⇒ 写入前剥掉 ⇒ 实测迁移 `剥列=["id"]` 362/362。
- **影响(用户可感知)**:前端**以 `row.id` 为主标识** —— `lib/client.js` 6 处:打开账号详情(`:3782`)· 打开视频详情(`:4669`)· 切收藏(`:3456`)· AI 创作(`:3714`)· AI 视频视图(`:3675`)· 删除(`:4579`)。**切档后新写入的账号/视频**在平台上没有 id(本地表也不再新增行)⇒ 这些按钮拿到 `undefined` ⇒ **功能失效**(R11「功能」维净变差)。
- **为什么不能"具名降级"了事**:D16 已规定"映射缺席 ⇒ 具名降级",但该处降级=**按钮不可用**,不是"少一列数据"。
### 乙 · `account_video_source` 的正文超过自己声明的 30 KB 上限
- **现象**:18/39 行被 `quota-exceeded` 拒收(`content_json` 3 行 / `analysis_json` 15 行),最大 **94,889 B**(≈ 上限的 3.1 倍);另 5 组重复键 ⇒ 平台最终只有 **18 键**。
- **取证**:迁移 dry-run `BY_REASON={"quota-exceeded":18}` + `sqlite3` 字节现算(§4.5);上限来源 = `dsh.data.yaml` 的 `content_json/analysis_json: maxBytes=30720`。
- **影响**:切档后这 18 条解析结果**读不到**(平台没有)⇒ 视频详情页的"解析结果/源脚本"缺一半(R11「功能」维)。
### ✅ 两处阻塞都**不在**待拍板 5 项里
04 单 §四 4.3 的 5 项(47 重启 · 106 重启 · 删库重建 · `json` 列两案 · 包源码纳管)**一项未动**;甲、乙是**本棒实测新发现**,上一棒只把甲登记为"未闭合项 3"(未给候选)。
---
## 六 待你拍板(**一轮一问** · 自包含)
> **要你定的是**:MCN 插件里"账号表 / 视频表 / 解析结果表"这三张表,还要不要继续往平台数据面搬(也就是要不要把插件改成"从平台读写这些数据")。
> **为什么需要你定**:搬过去之后,插件工作台里的**账号详情、视频详情、切收藏、删除、AI 创作**这些按钮会读不到数据而失效(因为平台侧没有它们在用的内部编号),并且**一部分解析正文(最大 94 KB)超出了当初设定的 30 KB 上限、搬不过去**。要么先补两样东西再搬,要么先不搬。
> ⛔ 不搬 = 现状不变、零风险;搬 = 需要先做下面的准备。
- **A 案 · 先补齐、再搬**:① 给账号/视频在平台侧或插件本地补一个"编号台账"(新增账号时同时登记,编号就不会丢);② 把解析正文的上限从 30 KB 提到 100 KB 以上。(优点:三张表都能真正搬完,界面功能不受影响;缺点:要动插件声明面与写入逻辑,且上限调整需先在测试库验证不会让插件变成"无法启用"状态)
- **B 案 · 只搬干净的、难的先不搬**:视频表 362 行已搬完且核对一致(这部分可以先切),账号表与解析结果表**暂不搬**。(优点:零风险推进一格,先把"视频列表"这条路走通;缺点:账号列表仍走本地,本次目标只完成三分之一)
- **C 案 · 本批到此为止**:三张表都不搬,把"是否值得搬"留给后续统一评估。(优点:完全零风险、把精力挪到别的功能;缺点:前面两批已经投入的接通工作暂时用不上,且每拖一次代价只会更高)
**我的倾向**:**A 案**(唯一能让三表都真正搬完、且不牺牲界面功能的路径);其中"编号台账"是纯新增、可回滚,风险集中在"上限调整"那一步,建议先在测试库验证。
---
## 七 未闭合项(现象 / 归属 / 下一步)
1. **`A5` 三表切档未达成** —— 归属:**口径缺口(规划侧未覆盖甲)+ 声明面配置(乙)**。下一步:等 §六 拍板。
2. **≈30 处第三批调用点的平台档接线未做** —— 归属:插件侧;**理由见 §二**(做了 = 死代码)。下一步:拍板后一次性接线。
3. **`A2` 实例内 E2E 未达成** —— 需 **47 实例重启窗口**(待拍板㈠);实例现载 `0.3.15`(**不含本棒代码**)。
4. **`A8` 两机一致未达成** —— 需出包/投放(属收口 S9–S11)。
5. **迁移到平台的行已保留**(`account_videos` 362 · `hot_accounts` 5+5 · `account_video_source` 18)—— ⛔ **未删**(04 单:不删平台库);切档前它们不参与读路径。
6. **平台侧缺陷(⛔ 未改 `src/**`,只报告)**:`src/db/plugin-data/runtime.ts` 对非法值返回的 `reason` 仍是笼统的 `disabled`(第 25 棒记档,本棒复核仍在)。
---
## 八 卫生声明(逐条点名)
| 项 | 结果 | 依据 |
|---|---|---|
| ⛔ 改 `D:/github/dsh_shenxian/src/**` | **0 件** | 本棒只读该目录(0 次) |
| ⛔ 手改用户 `home` 下 profile / `.pnpm` | **唯一写入 = 该用户自己的档位文件** `data-plane.json`(即阶段一设计的回滚落点);**已回滚到原 4 表** | §4.6 |
| ⛔ 重启任何服务 | **未重启** | 全程 `mksess.cjs` 临时会话 + 探针,未动 `systemctl` |
| ⛔ 投放任何版本 / commit / push | **均未做** | 共享层仍 `0.4.0`;两个仓库未提交 |
| ⛔ 删任何库 / 删旧库 `mcn-plugin.db` | **未删** | 迁移前后 sha256 逐字一致 `b37f8aab51a90f74…` |
| ⛔ 未拍板前执行删库 / 重建 | **未做** | `json` 列案 B 未出现在任何执行面 |
| ✅ 临时会话(R4) | 造 **2** 次 → 删 **2** 次 → 残留 **0** | `SESS_DELETED=1 SESS_LEFT=0` |
| ✅ 47 临时目录 | `/tmp/p32` **已整体删除** | `ls -d /tmp/p32` ⇒ `No such file` |
| ✅ 本机探针落点 | `$WS/tmp/p32/**` | — |
### 4.6 档位回滚证据(⛔ 本棒探针曾误把两表留在 `shadow`,已修正)
```
回滚前:…, "account_video_source":{"mode":"platform"}, "account_videos":{"mode":"shadow"}, "hot_accounts":{"mode":"shadow"}
回滚后:{account_persona, creative_log, account_analysis, video_analysis} —— 4 表 platform(与迁移前逐字同构)
```
> 🔴 **自证**:探针把"切档"写成了无条件动作,未按 `shadow` 结果分支 ⇒ 与 04 单 S7"全 `match=true` 才可切"冲突。**已当场回滚**,并记入本件以防后棒照抄该探针。
---
## 九 域锁
| 项 | 值 |
|---|---|
| 会话名 | `p32-exec-plugin-dataline` |
| 域(11 个) | `dsh-server-docs/05-交接单` · `aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md` · `aliyun-dsh-server/交付物` · `mcn/{store,data,imports,index,workshop,video-tools}.js` · `tools/migrate-local-to-platform.mjs` · `tests/mcn-store.test.mjs` |
| 复核方式 | 退出码 **rc=0** + 复读 `.locks/p32-exec-plugin-dataline/{OWNER,DOMAINS}` 断言归属(会话 `3e06798e-6379-460c-bd88-6c5ac88b767f`) |
| 门禁备注(如实) | `preflight-lock.sh` 对 11 个目标路径**全判【D】未归类**(工作区根文件 + 文档库 `05-交接单` + `D:/dshworkspace` 下的插件包路径都不在该脚本锚点覆盖内)⇒ 按脚本字面应 `rc=1`;依第 26–31 棒**既有惯例**开工并报告,⛔ **未改那个跨库脚本** |
---
**变更记录**:2026-09-25 17:0x 立件(第 32 棒 · 执行棒)。本件**不含不可逆操作**。