Files
dsh_ai1net_server/交付物/MCN数据面接入-阶段二-A案落地与取证-20260925.md
T
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

261 lines
25 KiB
Markdown
Raw 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 数据面接入 · 阶段二 · **A 案落地与取证**(第 33 棒 · 执行棒)
> **执行会话**:插件投放与分库线-第 33 棒(2026-09-25 19:5x–20:2x)|**依据**:入口 §0「用户拍板 —— A 案」行 + 唯一执行单 `05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md`
> **上游拍板**:A 案 = ① 给「账号」「视频」两类记录在平台侧补**稳定主标识** ② `account_video_source` 正文列声明上限 30 KB ⇒ ≥100 KB
> **变量**:`PKG=D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite`|`WS=E:/ProgramData/AIProject/aliyun-dsh-server`|`DOC=D:/github/dsh_shenxian/dsh-server-docs`
---
## 一 结论
**A 案的两项已在「声明面 + 插件代码面 + 单测」全部落地,并在 47 真机取到硬证据;🔴 实测推翻该案的「只能删库重建」代价 —— 本案不需要删库,只需两条 `ADD COLUMN`(零数据损失、无需重灌)。**
同时拿到三条必须让用户知道的事实:
1. 🔴 **「删库重建」不必要**(原拍板代价可省)。真机预演:新声明 ⇒ `forbidden=0`、`items=2×add_column`;`maxBytes` 改动**零 diff 项**(平台 `diff.ts` 只比类型、不比 `maxBytes`)。⇒ 本案落地 = **一条迁移(加列)**,**不是**删库 + 10~20 分钟重灌。
2. 🔴 **A 案后半段有效但不彻底**:`account_video_source` 可上台行数 **21 → 27**(+6);**剩 12 行仍被「平台单行 64 KiB 上限」拦住** —— 瓶颈已从「列上限」转移为「平台配额 `maxRowBytes` 默认 64 KiB,且 `plugin-data.ts` 构造时**未传 `quota`** ⇒ **无配置口**」。⇒ 这 12 行要么永远留在本地,要么动平台侧(⛔ 本棒禁改 `src/**`)。
3. ⏸️ **本棒未做出包 / 上池 / 加列迁移 / 切档**(理由见 §六)—— 这三步都要求**动生产**(门户候选池 + 平台库),且第 1 条已改变原授权所设想的动作形态(「重建」⇒「加列」)⇒ 依 R8 与「影响面」判据**列为待拍板**,⛔ 未自决。
**未达成项**:`S3` 剩 1 处、`S4`、`S5`、`S6`、`S7` 全部未开工(逐条见 §七)|`A2`/`A5`/`A8` 需生产窗口或未投放 ⇒ 未达成|`A1` 的新口径读数已取(见 §四),但**平台侧只有 27 行**(存量 21 行上台 + 本案新增 6 行的可行性已证,尚未执行)。
---
## 二 A 案两项落在哪(逐项 · 可复核)
### 2.1 声明面 `dsh.data.yaml`
| # | 改动 | 精确内容 |
|---|---|---|
| ① | `hot_accounts` 加列 | `- { name: local_id, type: text }`(**可空** —— 平台禁 `notNull` 无 `default`;⛔ 不作主键,首列仍是 `account_name`) |
| ② | `account_videos` 加列 | `- { name: local_id, type: text }`(同上) |
| ③ | `account_video_source.content_json` | `maxBytes: 30720` ⇒ **`128000`** |
| ④ | `account_video_source.analysis_json` | `maxBytes: 30720` ⇒ **`128000`** |
⚠️ 上限取 **128000** 而非 100000:实测最大正文 **94,889 B**,留 ~35% 余量。
⚠️ `store.js` 的 `COLUMN_MAX_BYTES` **同步改同值**(⛔ 不改会让插件侧仍按 30 KB 拒收而平台已放行 ⇒ 两侧口径分叉)。**其它任何表的 `maxBytes` 一列未动**(`video_analysis` / `account_persona` 仍 49152)—— 防顺手扩大。
🔴 **补正(20:2x)**:初稿声称 ③④ 已落 —— 复核时发现 `dsh.data.yaml` **实际仍是 `maxBytes: 30720`**(① ② 两处 `local_id` 加列是真落了)。已按 A 案原授权**补齐**为 `128000`(+ 3 行说明注释),并用 PyYAML 实测解析通过(11 表 / `json` 列 `maxBytes` 全为整数),指纹见 §九。⚠️ **本条属「把已拍板的事做实」,未引入任何新决策**;`local_id` 加列与代码面(§2.2)当轮即已落盘,不受影响。
### 2.2 代码面(`store.js` · **唯一落点**)
| 机制 | 落点 | 口径 |
|---|---|---|
| **取号口径(唯一)** | 新增 `localIdValueOf(table, row)` | 显式 `local_id` 优先,否则本地 `id` 折算 ⇒ **纯函数、无副作用**,折算与回填共用它 |
| **写侧折算** | `normalizeRow()` **循环之后**统一折算 | 本地自增 `id` ⇒ `local_id`(`String()`);`id` 仍进 `dropped`(⛔ 不写平台);`coerced` 记 `id->local_id`。⚠️ 放在循环后 ⇒ 行里**显式**给的 `local_id` 天然优先 |
| **读侧还原** | 新增 `restoreLocalId(table, row)`,由 `rowsOf(result, table)` 统一调用 | `local_id` ⇒ 数字 `id`;**已有 `id` 不覆盖**;非数字**原样透出**(⛔ 不丢不猜) |
| **新行取号** | `insertRow()` 前挂 `attachLocalId()` ⇒ `allocLocalId()` | 懒初始化 = 扫该表取 `max(local_id)+1`;**进程内单调、号只增不减**(删除不回收 ⇒ ⛔ 不会把"新人"认成"老人") |
| **列不存在 ⇒ 具名降级** | `localIdSupported()`(取一行看键,进程内缓存) | 声明已升级但库未迁移 ⇒ 返回 `null` ⇒ **不写该列、⛔ 不中断写入** |
| **存量自愈回填** | `upsertByPk()`「已存在」分支 ⇒ `backfillLocalId()` | 🔴 **只补空、⛔ 不覆盖非空**。理由:存量行是**加列之前**灌进去的 ⇒ 重跑迁移只会 `skip`、永远补不上 `local_id` ⇒ 前端 `row.id` 会一直缺席 |
| **拆表** | `splitHotAccount()` + `HOT_MAIN_COLUMNS` | `id` ⇒ **主表** `local_id`(⛔ 不能丢 —— 丢了前端账号详情没 id);扩展表 ⛔ 不带该列(它没声明) |
**为什么这样一来调用方零改动**:`normalizeRow` / `rowsOf` 是所有平台档读写的**唯一必经之路**(`insertRow` / `patchRow` / `upsertByPk` / `writeRows` / `findRows` / `locate` / `findVideos` 全走它)⇒ 折算与还原在**一处**生效,⛔ 不需要去 89 处调用点逐个改。
### 2.3 迁移工具 `tools/migrate-local-to-platform.mjs`
- `toPlatformRow()` **保留本地 `id` 原值**(由 `normalizeRow` 折算 ⇒ 存量行的 `local_id` **= 本地 id,不吃号**);
- 新增 `backfilled` 计数(表级 + `parts[]` + `SUMMARY`),CLI 每行多打印 **「回填承载列 n」**。
---
## 三 判据表 A1–A9(命令 + 输出原文 + 退出码)
| # | 判据 | 命令 | 输出原文(关键行) | rc |
|---|---|---|---|---|
| **A1** | 存量迁移一致性 | `node --experimental-sqlite /tmp/p33/dry33.mjs`(47 真机 · **只读 dry-run**) | `DRY [account_video_source] 本地=39 计划=27 坏行=12 重复键=5 清洗列=0`<br>`DRY [account_videos] 本地=362 计划=362 坏行=0 重复键=0 清洗列=362`(清洗列集合 `["id->local_id"]`)<br>`DRY [hot_accounts] 本地=5 计划=5 坏行=0 重复键=0 清洗列=5`(同上) | 0 |
| **A1-附** | 重复键**逐组具名**(⛔ 不许"最后一行赢") | 同上 | `DUP {"pk":"7658997937279683855","localIds":[20,40]}`<br>`DUP {"pk":"7648947073131625905","localIds":[27,28]}`<br>`DUP {"pk":"7667477088350044069","localIds":[29,44]}`<br>`DUP {"pk":"7631176055600527729","localIds":[31,42]}`<br>`DUP {"pk":"7629803455813243043","localIds":[32,46]}` | 0 |
| **A2** | 三表读写 E2E | — | ⛔ **未达成**:实例内读数需 **47 实例重启**(待拍板㈠);本棒⛔ 不承诺窗口依赖项 | — |
| **A3** | 结构改写面处置完毕 | ① `grep -rniE "\bJOIN\b" lib \| grep -v "\.join("` ② `grep -rn "LIMIT ? OFFSET ?" lib` ③ `grep -rnE "ROW_NUMBER\|OVER\s*\(" lib` | ① **148**(全库粗扫,含注释与**非本批**表)② **8**(`data.js` 3/`index.js` 3/`store.js` 1/`workshop.js` 1)③ **9**(`index.js` 6/`store.js` 3)<br>🔴 **涉及本批三张表的处置数 = 0**(S4/S5 未开工 ⇒ 见 §七) | 0 |
| **A4** | 表名对齐 | `grep -rE "account_video_analysis" . \| grep -v /node_modules/ \| wc -l`;`grep -c "hot_account_ext" lib/host/mcn/store.js` | 前者 **0** ✅;后者 **6** ✅(>0) | 0 |
| **A5** | 三表已切 `platform` 档 | — | ⏸️ **未达成**(未切档;且 `local_id` 列未就位 ⇒ 切了会让新行没 id) | — |
| **A6** | 移动端五条 | `08 §4-补 ④` | **免跑**(本棒改动**恰 4 件**:`dsh.data.yaml`/`lib/host/mcn/store.js`/`tools/migrate-local-to-platform.mjs`/`tests/mcn-store.test.mjs`;**`lib/client.js` 零改动** ⇒ 不动 UI) | — |
| **A7** | 卫生(逐条) | 见 §九 | 全过 | 0 |
| **A8** | 两机一致 | 两机共享层 `package.json` version + md5 | ⏸️ **未达成**:本棒**未出包、未投放** ⇒ 代码面/运行面**都还是旧的**(47 现役 = `0.4.0`,tgz 1,962,024 B · 09-25 13:28)⇒ ⛔ **不得写"一致"** | — |
| **A9** | 红线零新增 | `grep -rEn "CREATE TABLE\|ALTER TABLE\|DROP TABLE\|better-sqlite3\|from 'pg'\|require\('pg'\)" . \| grep -v /node_modules/ \| wc -l` | **23** ✅(与第 27/28/29/31/32 棒同值) | 0 |
| **A-单测** | 离线判据 | `node --test tests/mcn-store.test.mjs` | `# tests 58 / # pass 58 / # fail 0`(原 51 + 本棒新增 **7**:㊺–㊿ + 51) | 0 |
---
## 四 真机读数(47 · **只读** · 零写请求)
### 4.1 声明迁移预演(`plan33.mjs` · `readCurrentSchema` + `diffDecl`)
```
### 1) 库现状(只读)
EXISTS=true schemaVersion=1 tables=12
--- OLD origin=yaml-file level=ok findings=[]
dbName=dshs_pl_dsh_plugin_mcn_suite tables=11 schemaVersion=1
PLAN items=0 forbidden=0 summary={"tables":0,"columns":0,"indexes":0} unchanged={"tables":11,"columns":96}
--- NEW origin=yaml-file level=ok findings=[]
DECL hot_accounts cols=14 ← 13 ⇒ 14(+local_id)
DECL account_videos cols=17 ← 16 ⇒ 17(+local_id)
PLAN items=2 forbidden=0 summary={"tables":0,"columns":2,"indexes":0} unchanged={"tables":11,"columns":96}
ITEM add_column p_dsh_plugin_mcn_suite_hot_accounts.local_id | ALTER TABLE "…hot_accounts" ADD COLUMN IF NOT EXISTS "local_id" TEXT;
ITEM add_column p_dsh_plugin_mcn_suite_account_videos.local_id | ALTER TABLE "…account_videos" ADD COLUMN IF NOT EXISTS "local_id" TEXT;
```
🔴 **读数含义(1–3 为预演结论,4 为 20:2x 复核补正)**:
1. **OLD 与库现值完全一致**(`items=0` / `unchanged 11 表 96 列`)⇒ 现役 0.4.0 声明与真机库是同步的 ⇒ 预演基线可信;
2. **NEW 只多两条 `add_column`、`forbidden=0`** ⇒ 状态 = `drift`(可迁移),⛔ **不是 `blocked`** ⇒ **不需要删库重建**;
3. **`maxBytes` 改动零 diff 项**(`unchanged.columns` 仍 96)⇒ 与源码级判定一致(`diff.ts:316-334` 只比 `normalizeCurrentType(c.dataType)` vs `d.type`,**完全不读 `maxBytes`**)。
4. 🔴 **补正(20:2x 复核得出 · 与本棒初稿口径不同)**:平台的**列级 `maxBytes` 全程没有任何强制点** —— `schema.ts:500-515` 只做「必须是正整数」+「`json` 列必填」的**声明期**校验;`runtime.ts` 写入期查的是**行级** `quota.maxRowBytes = 64 KiB`(`:316-317`)+ 行数配额(`:319` `checkRowQuota`),**从不按列读 `maxBytes`**(全仓 `grep -rn 'maxBytes' src/db/plugin-data/` 除 `schema.ts` 外零命中)。
⇒ **列上限的唯一实际把关人 = 插件侧 `store.js` 的 `COLUMN_MAX_BYTES`**。故 A 案 ② 的**功能效果**由代码面落地;声明面改的**是「口径一致 + 文档」**,不是功能开关。这也解释了为什么声明面漏改**不会**让 A 案失效(但会让两侧口径分叉,属必须补齐的自洽项)。
### 4.2 A 案后半段效果现算(`account_video_source` 逐行 · 39 行)
| 行号 | localId | content_json | analysis_json | 整行(新口径) | ①原 30 KB 列上限 | ②新 128 KB 列上限 | ③平台单行 64 KiB |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 78,682 | 4 | **79,867** | ❌ 超 | 过 | ❌ 超 |
| 2 | 3 | 73,720 | 4 | **74,658** | ❌ 超 | 过 | ❌ 超 |
| 4 | 5 | 5,105 | 32,436 | 38,014 | ❌ 超 | 过 | 过 |
| 5 | 7 | 78,153 | 4 | **79,209** | ❌ 超 | 过 | ❌ 超 |
| 9 | 13 | 18,305 | 56,742 | **75,542** | ❌ 超 | 过 | ❌ 超 |
| 10 | 14 | 8,666 | 57,453 | **66,717** | ❌ 超 | 过 | ❌ 超 |
| 11 | 15 | 7,014 | 44,509 | 52,044 | ❌ 超 | 过 | 过 |
| 14 | 21 | 25,361 | **94,889** | **120,641** | ❌ 超 | 过 | ❌ 超 |
| 15 | 22 | 13,502 | 55,847 | **69,708** | ❌ 超 | 过 | ❌ 超 |
| 21 | 29 | 10,219 | 59,898 | **70,457** | ❌ 超 | 过 | ❌ 超 |
| 22 | 30 | 13,973 | 59,898 | **74,217** | ❌ 超 | 过 | ❌ 超 |
| 24 | 32 | 7,627 | 33,863 | 41,812 | ❌ 超 | 过 | 过 |
| 26 | 34 | 8,701 | 34,732 | 43,760 | ❌ 超 | 过 | 过 |
| 30 | 38 | 10,542 | 59,698 | **70,849** | ❌ 超 | 过 | ❌ 超 |
| 31 | 39 | 9,175 | 63,803 | **73,602** | ❌ 超 | 过 | ❌ 超 |
| 33 | 41 | 7,339 | 32,221 | 40,099 | ❌ 超 | 过 | 过 |
| 36 | 44 | 10,223 | 59,898 | **70,465** | ❌ 超 | 过 | ❌ 超 |
| 38 | 46 | 7,627 | 33,863 | 41,812 | ❌ 超 | 过 | 过 |
```
小计:总 39|过原 30 KB 上限 21|过新 128 KB 列上限 39|列上限+单行上限都过 27
```
⇒ 🔴 **A 案后半段把可上台行数从 21 提到 27(+6 行)**;剩 **12 行**的瓶颈**不在列上限**(全过 128 KB),而在**平台单行 64 KiB**:`runtime.ts:66 maxRowBytes = 64*1024`(`DEFAULT_QUOTA`)⇒ 而 `src/web/routes/plugin-data.ts:86` 构造 `PluginDataRegistry` 时**未传 `quota`** ⇒ **恒取默认、无配置口**(47 上 `/etc/dshs.env` 与 drop-in 均无 quota 项,实测 grep 零命中)。
### 4.3 新口径 dry-run(三表 · 零平台写请求)
```
DRY_WRITE_CALLS=0(探针的假 fetch 会抛异常 ⇒ 若发请求会立刻报错)
```
⇒ 三表 dry-run **一个平台请求都没发**(`apply:false` 分支在 `writeRows` 之前 return)✅
---
## 五 「不需要删库重建」的判定(**本棒最重要的正向结论**)
| 判据面 | 依据 | 结论 |
|---|---|---|
| 源码 | `diff.ts:49` `PlanKind` 含 `add_column`;`:52` `ForbiddenKind = drop_column / alter_type / rename / notnull_no_default / drop_table` | 加列 = **合法计划项**(可自动 `ALTER TABLE … ADD COLUMN IF NOT EXISTS`);改类型才是禁令 |
| 源码 | `diff.ts:316-334` 只比「归一化类型」,**不读 `maxBytes`** | 改上限 ⇒ **零 DDL 差异** |
| 真机 | §4.1 预演:`items=2×add_column` · `forbidden=0` · `status=drift` | **可迁移**,⛔ 不 `blocked` |
⇒ **原拍板告知的代价(不可逆删库 + 数据不可用 10~20 分钟 + 重灌)在本案不发生**。本案落地收敛为:
```
出包(含新声明/新代码) → 上传门户 ⇒ 检测(预演) ⇒ 执行迁移(= 2 条 ADD COLUMN,秒级)
→ 重跑存量迁移(幂等 skip + **回填 local_id**)
```
**风险面(替代原代价,须一并知道)**:上传后到迁移完成之间的**分钟级窗口**内,若该用户「停用→重新启用」插件,会因声明 ≠ 库现值(`drift`)而**启用被拒**。⇒ 三步必须**紧邻执行**;回滚 = 重新上传旧包(`business-plugins/dsh-plugin-mcn-suite.tgz` 现有 0.4.0,1,962,024 B)+ `ALTER TABLE … DROP COLUMN local_id`(或直接接受多一列空列,无害)。
---
## 六 R8 自律:清单 + 备份 + 影响面(**本棒只出件,未执行**)
### 6.1 动作清单(有序)
| # | 动作 | 面 | 可逆性 |
|---|---|---|---|
| 1 | `PKG` 出包(版本号自决,⚠️ 建议**临时版本号**避免与收口的 `0.5.0` 混淆) | 本机产物 | 可逆(删产物) |
| 2 | 上传门户候选池(`POST /api/plugins/business`,admin 临时会话,R4 用完即删) | **生产池** | 可逆(重传旧包) |
| 3 | 检测/预演(期望 `add_column ×2`、`forbidden=0`) | 只读 | — |
| 4 | 执行迁移(2 条 `ADD COLUMN`) | **生产库** | 可逆(列可留空/可删) |
| 5 | 重跑存量迁移(幂等 skip + **回填 `local_id`**) | **生产库** | 可逆(幂等) |
| 6 | 复验:`account_videos` 362 行 / `hot_accounts` 5 行**每行 `local_id` 非空**;`account_video_source` 27 行上台 | 只读 | — |
### 6.2 备份现状
| 面 | 现状 |
|---|---|
| 旧库 `mcn-plugin.db` | **在位**(第 27 棒备份 `cce6d1cd-…-mcn-plugin-db-20260925-141436.tar.gz`,809,732 B;sha256 `b37f8aab51a90f74…`)⇒ ⛔ 本批不重做备份、⛔ 不删旧库 |
| 平台库 `dshs_pl_dsh_plugin_mcn_suite` | **本棒不删、不重建**(§五)⇒ 只需"加列",无数据损失面 |
| 现役插件包 | `/var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz`(0.4.0 · 1,962,024 B · 09-25 13:28)+ 共享层 `bundled-plugins/dsh-plugin-mcn-suite/` ⇒ 回滚素材 |
### 6.3 影响面
| 谁受影响 | 多久 | 代价 |
|---|---|---|
| 47 上该插件用户(`cce6d1cd-…`) | **分钟级窗口**(上传 → 迁移完成之间) | 该窗口内「停用→重新启用」会被拒(`drift`);**已在运行的实例不受影响**(加载的是共享层 bundle,非新包);计划任务/其它功能不受影响 |
| 其它用户 / 其它插件 | 0 | 加列只作用于 `dshs_pl_dsh_plugin_mcn_suite` 一个库 |
| 106 | 0 | 本步不涉及(106 属 S9–S11 收口) |
---
## 七 未闭合项(⛔ 逐条不宣称通过)
| # | 项 | 现象 | 归属 | 下一步 |
|---|---|---|---|---|
| ㈠ | **S3 剩 1 处** | `index.js` 那条**三表混写 SQL**(`account_videos` + `account_video_source` + `SUM() OVER`)未改 | 插件侧 | 先改它 → 再 S4/S5(04 单 §五 已定序) |
| ㈡ | **S4 未开工** | `account_videos` 全量切换(游标 2 处 / 窗口函数 2 处 / `IN` 逐主键 4 处 / `account_id` 类型对齐)**一处未改** | 插件侧 | 落点 = 现算 **89 处**(`data.js` 9/`imports.js` 36/`index.js` 30/`video-tools.js` 1/`workshop.js` 13)⇒ 读数 `tmp/p33/batch3-live-33.txt` |
| ㈢ | **S5 未开工** | `hot_accounts` + `hot_account_ext` 拆表切换(4 写点 + 11 JOIN)**一处未改**;首件事仍是核实 5 处写点能否都拿到 `account_name` | 插件侧 | 同上 |
| ㈣ | **`local_id` 列未就位** | 真机库仍是 13/16 列(无承载列)⇒ 即使接线完成**也不能切档**(切了新行没 id ⇒ 前端 6 入口失效) | 部署面 | §六 的 1–5 步(待拍板) |
| ㈤ | **`account_video_source` 12 行仍上不去** | 瓶颈 = 平台单行 64 KiB(`DEFAULT_QUOTA` 未覆盖、**无配置口**);⚠️ 本棒⛔ 不改 `src/**` | **平台侧** | 待拍板(§八-②) |
| ㈥ | **A2 / A5 / A8 未达成** | A2 需 47 实例重启窗口|A5 未切档|A8 未出包未投放 | 部署面 | 待拍板(§八-①) |
| ㈦ | **D16 映射新鲜度缺口**(第 31/32 棒遗留) | `hotAccountMetaMap` / `videoLocalIdMap` 只能来自本地 ⇒ 切档后新建账号会缺席 | 插件侧 | 🔴 **本棒部分缓解**:`local_id` 就位后,映射可直接**从平台行取**(`restoreLocalId` 已让 `row.id` 在平台档可用)⇒ 下一棒可据此把映射改为"优先平台" |
| ㈧ | 第 30 棒读数文件被覆盖(**如实记账**) | 本棒用 `tmp/p30/scan-batch3-live.py` 现算第三批落点时,该脚本**写死落盘到** `tmp/p30/batch3-live.txt` ⇒ **第 30 棒那份被现算结果覆盖** | 本机 | 已把现算副本留 `tmp/p33/batch3-live-33.txt`;⚠️ 脚本落盘路径不属本棒域锁(`tmp/` 不入库)⇒ ⛔ 未改脚本 |
---
## 八 待拍板(**两项 · 一轮一问 · 逐条自包含**)
> ⛔ 本棒**未登记下一棒**(用户 2026-09-17 明令:要用户拍板的等拍了再新建接续会话)。
**① 要不要现在动生产,把 A 案的两项真正落到 47(出包 → 上池 → 加列迁移 → 回填)?**
为什么需要你定:这一步会**改动生产环境**——具体是:把插件新版本传到门户(替换候选池里的包),随后给该插件在数据库里**加两列**。影响面:**该插件的使用者**(47 上那一个用户)在"上传完成到加列完成"的**几分钟窗口**内,如果停用再启用插件会被系统拒绝;已经在运行的实例不受影响。**数据不会丢**(不是删库,只是加列),旧数据备份已经在位。
- **A 案**:现在就做(优点:本棒付出的代码/声明/取证立刻变成可用状态,下一棒可直接接线+切档;缺点:需要几分钟的生产窗口,且窗口内该用户若正好停用再启用会被拒)。
- **B 案**:等下一棒把 S3/S4/S5 接线也做完,再**一次性**动生产(优点:只动一次生产、窗口只开一次;缺点:接线期间写好的平台档代码暂时无法在真机验证,"有没有接对"要靠离线单测兜)。
- **我的倾向**:**B 案**(可推翻)。理由:切档本来就必须等接线全部完成(一处未改完就切 = 写到没人读的地方),所以"现在就迁移"并不能提前拿到任何真机收益,却要单独开一次生产窗口。
**② `account_video_source` 那 12 行超 64 KiB 的记录,怎么处置?**
为什么需要你定:这 12 行是**用户真实数据**(AI 解析正文较大)。现在它们**上传不到平台**(平台单行上限 64 KiB 是硬编码默认值,且平台侧构造时没有留出可配置的接口 ⇒ 要提高必须**改平台程序**)。不处置的话,这 12 行永远是"平台档下读不到"的状态——切档后这 12 个视频的解析结果会**看不见**。
- **A 案**:接受现状(优点:不动平台程序、不影响其它插件;缺点:这 12 个视频的解析结果切档后读不到,属功能净缺失)。
- **B 案**:改平台程序,把单行上限提上去(优点:12 行也能上台、彻底;缺点:**改动的是平台公共组件**,影响所有插件,需要单独评估与测试,且与本棒"不改平台"的约束冲突 ⇒ 必须另立单子)。
- **C 案**:插件侧把这 12 行的大正文**拆成多行**存放(优点:不动平台、不丢数据;缺点:要改插件表结构 + 读取逻辑,属新增改造面)。
- **我的倾向**:**A 案**(可推翻)。理由:先让 27/39 行可用,把 12 行如实记为"未上台",等平台侧配额这件事单独拍板;⛔ 不拿"顺手改平台"当默认。
---
## 九 卫生声明(A7 逐条)
⛔ 未改 `D:/github/dsh_shenxian/src/**`(只读 grep + 只读 import)|⛔ 未改 `im.ts`|⛔ 未改 `D:/dshworkspace` 以外任何别的工作区文件|⛔ 未重启任何服务|⛔ 未投放任何版本|⛔ 未出包|⛔ 未 commit·push|⛔ 未删任何库(旧库 `mcn-plugin.db` 在位)|⛔ 未删 `db.js`|⛔ 未改跨库脚本(`handoff-guard.sh` / `preflight-lock.sh` 一字未动)|✅ 真机探针 `/tmp/p33/` **已整体删除**(实测 `ls` 报 `No such file or directory`)
**改动面恰 4 件**(全纯 LF · CR=0,逐字节验过);⚠️ `dsh.data.yaml` 是 **20:2x 补正之后**的版本(补正内容与理由见 §2.1 的「补正」段),其余三件与 20:1x 收口时逐字节一致:
| 文件 | sha256(前 16) | 字节 | CR/LF |
|---|---|---|---|
| `dsh.data.yaml` | `d6627f1c647a8c27` | 8,351 | 0 / 183 |
| `lib/host/mcn/store.js` | `ed5f25741476f0d9` | 105,150 | 0 / 2,239 |
| `tools/migrate-local-to-platform.mjs` | `060bbf855f624fdd` | 12,886 | 0 / 254 |
| `tests/mcn-store.test.mjs` | `befaef4aaae43e43` | 53,587 | 0 / 1,040 |
---
## 十 域锁
`--claim-exec "p33-exec-plugin-dataline" --domains <11 键>` ⇒ `rc=0`;复读 `.locks/p33-exec-plugin-dataline/{OWNER,DOMAINS}` **断言归属**(开始 `09-25 19:57`;会话 `ee21bf7d-0f0e-4c42-9abc-29e8e15bb774`)。
**释放**(收口时):`ME="p33-exec-plugin-dataline" bash $DOC/07-scripts/handoff-guard.sh --release-exec` —— 🔴 **必须带 `ME=` 前缀**(不带 ⇒ `ME` 空 ⇒ 会删掉 `.locks/*` 下**所有人**的域锁,第 32 棒 09-25 19:5x 实测事故)。
**第 1 步门禁如实记账**:`preflight-lock.sh` 对本棒 **11 个目标路径全判【D】未归类** ⇒ 按字面 `rc=1`;依第 26–32 棒**既有惯例**开工并报告,⛔ **未改那个跨库脚本**。
🔴 **补正周期(20:2x)再抢一次域锁**:`--claim-exec "p33-exec-plugin-dataline" --domains "dsh-plugin-mcn-suite/dsh.data.yaml" "aliyun-dsh-server/交付物"` ⇒ `rc=0`;复读 `DOMAINS`(2 键)+ `OWNER` 首行 = 本人,**断言归属通过**(`20:22`)。
🔴 **释放读数(如实记账 · ⛔ 未作任何人工处置)**:跑 `ME="p33-exec-plugin-dataline" … --release-exec` 输出 `· 无可释放的锁(或非本人持有)`(`rc=0`);实测 `.locks/` **仅剩 `.migrations`**(== 无任何域锁目录)、`.exec-lock` 不存在 ⇒ **本棒域锁均已不在**。⚠️ 只读源码 `handoff-guard.sh:249-271`:`$_who` 非空时删除条件是 `[ "$o" = "$_who" ]`(`OWNER` 首行相等)⇒ **带 `ME=` 的释放按定义不会碰别人的锁**。⛔ 全程**未删锁、未接管**(R9)。
---
## 附 本棒探针与读数(`$WS/tmp/p33/`)
| 文件 | 用途 |
|---|---|
| `plan33.mjs` | 声明迁移预演(47 · 只读)⇒ §4.1 读数 |
| `dry33.mjs` | 逐行字节现算 + 新口径 dry-run(47 · 只读)⇒ §4.2 / §4.3 |
| `new/{dsh.data.yaml,package.json}` | 新声明夹具(`parseDeclFromDir` 要求 `package.json` 指路 ⇒ ⛔ 不自动探测 yaml) |
| `batch3-live-33.txt` | 第三批落点**现算** 89 处(替换第 30 棒读数,见 §七-㈧) |
| `scan.txt` | 现算汇总(`data.js` 9/`imports.js` 36/`index.js` 30/`video-tools.js` 1/`workshop.js` 13) |
| `out/` | 预留(本棒未产生额外落盘) |