Files

403 lines
30 KiB
Markdown
Raw Permalink Normal View History

# MCN 数据面接入 · 阶段二 · 第三批 —— **切档与收口**(第 36 棒 · 执行棒)
> **会话**:`p36-exec-plugin-dataline`|**时间**:2026-09-25 23:10–(GMT+8)
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`
> **纪律体**:`git pull`;**本棒按用户 23:0x 授权执行 commit + push**(读数补记于 §7/§8)
> **上游依据**:入口 `§0` 顶部 **22:44 拍板行** + `§2`「⏭️ 本轮动作(第 36 棒)」|唯一执行单 = `$DOC/05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md`
> **上一棒依据件**:`$WS/交付物/MCN数据面接入-阶段二-B案落地与shadow读数-20260925.md`(第 35 棒 · 两条根因)
---
## 一 结论(一句话)
**① – ⑤ 里,① ② ③ ④ + ⑤ 的「47 侧切档」全部达成,且第三批三表首次出现「计数 34 = 34 + 内容判据 34/34 全绿」**;**⑤ 的「实例重启 / A2 实例内面」与「106 同步」未达成**(原因具名,见 §5),**A8 如实记「两机不一致(106 未取证)」**。
| # | 范围项 | 状态 | 关键读数 |
|---|---|---|---|
| ① | 修 `canonicalJsonText()` 键序敏感(递归按键排序)+ 补单测 | ✅ 达成 | 单测 **67/67**(`# fail 0`);本机等效证明 **10/10**(`fail=0`) |
| ② | 出包 `0.5.1` → 上池 → 发布共享层 | ✅ 达成 | `2,018,751 B`/**324 条目**/md5 **`56c17c7e41a18704922e18d75816da59`**;share `200 action:"replaced" version 0.5.1`;47 共享层 `store.js` md5 **`62384d4d…`=本机逐字一致** |
| ③ | 删本地 5 条冗余副本(拍板①) | ✅ 达成 | **39 → 34 行**;`KEY_DELTA lost=[] extra=[]`;`integrity_check=ok`;⛔ 库文件未被删、属主不变 |
| ④ | 重取 `account_video_source` 影子读 | ✅ 达成 | A1 **34 = 34**、`onlyLocal=[] onlyPlatform=[]`;SHADOW **`match=true`**;**逐行内容判据 正文 34/34 · 分析 34/34 · 差异 0** |
| ⑤ | 三表切 `platform` | ✅ **达成(47 侧)** | 实例 `data-plane.json` ⇒ **7 表 `platform`**(A5 达成);A2 探针三表 `source=platform` 全 OK、零写请求 |
| ⑤ | 47 实例 + 106 合并重启窗口 | ⛔ **未达成** | 实例当前 `stopped`;106 **无 ssh 授权 key**(见 §5) |
| ⑥ | 收口(锁/automation/入口/README/日志/交付件) | ✅ 见 §5 §6 |
| ⑦ | 同步到仓库 | ✅ 见 §6 |
---
## 二 ① 修法(`canonicalJsonText()` 键序无关)
### 2.1 根因(承第 35 棒「根因乙」)
平台 PostgreSQL 的 `jsonb` **按「键长度 + 字母序」重排对象键**;旧实现是保留键序的 `JSON.stringify` ⇒ 凡值以 jsonb **对象**形态落库的行,读回后 canonical 必然不等(**假阴性**)。
### 2.2 改法(`lib/host/mcn/store.js`)
新增 `sortKeysDeep()`(对象键递归升序;**数组保持原序**)+ 导出 `canonicalStringify()`(稳定序列化),`canonicalJsonText()` 的三条归一出口全部改走它:
```js
export function canonicalStringify(v) {
return JSON.stringify(sortKeysDeep(v ?? null));
}
export function canonicalJsonText(v) {
if (typeof v !== "string") return canonicalStringify(v);
const s = v.trim();
if (s.startsWith('"') || s.startsWith("{") || s.startsWith("[")) {
try { return canonicalStringify(JSON.parse(s)); } catch (e) { /* 形似 JSON 但解析不过 ⇒ 当纯文本 */ }
}
return JSON.stringify(v);
}
```
⛔ **落库原文零改动**(仍走 `repairJsonText` 的原样口径)· ⛔ 不动平台 · 数组顺序语义保留。
### 2.3 单测(`tests/mcn-store.test.mjs` · 新增第 59 条)
```bash
node --test tests/mcn-store.test.mjs # 退出码 0
```
```
# tests 67 / # pass 67 / # fail 0 / # cancelled 0 / # skipped 0
ok 67 - 59 `canonicalJsonText` 键序无关(平台 `jsonb` 重排对象键 ⇒ 否则平台档把同内容判成不同内容)
```
### 2.4 本机等效证明(修法有效 + 零回归 · 10/10)
```bash
node tmp/p36/proof-canon.mjs # 退出码 0
```
```
PASS | A1 前提:键序确实被重排 | local={"aweme_id":"7001","desc":"文案","n":3,… plat={"n":3,"desc":"文案","tags":["a","b"],"awe…
PASS | A2 旧算法:判**不等**(复现根因乙的假阴性)
PASS | A3 新算法:判**等**(缺陷已修)
PASS | A4 稳定键随之键序无关(迁移幂等 / 去重共用)
PASS | B 零回归(Markdown 正文):新旧逐字相同
PASS | B 零回归(Markdown 的 JSON 字符串形态):新旧逐字相同
PASS | B 零回归(gz1 压缩态):新旧逐字相同
PASS | B 零回归(纯数组文本):新旧逐字相同
PASS | C1 数组顺序保留([1,2] ≠ [2,1])
PASS | C2 对象数组内部元素的键被排序
PROOF_SUMMARY fail=0
```
> 🔴 **回归风险已评估并说明**:`canonicalJsonText` 另供两个**派生稳定键**使用 —— `personaKey`(`account_persona`)/`analysisKey`(`account_analysis`)。二者内容形态为 **Markdown 文本**(落 jsonb `string` ⇒ 走"纯文本"分支 ⇒ **逐字等价**),且这两张表在第一/二批影子读下**已 `match=true`** ⇒ **键值不变、零回归**(证明 B 组四例即为此口径)。
---
## 三 ② 出包 `0.5.1` → 上池 → 发布共享层
```bash
cd "$PKG" && node "$NB/node_modules/npm/bin/npm-cli.js" pack --pack-destination dist # 退出码 0
md5sum dist/dsh-plugin-mcn-suite-0.5.1.tgz | cut -d' ' -f1
```
```
56c17c7e41a18704922e18d75816da59 # BYTES=2018751 / 条目=324 / shasum e075c42d8a37ec8a203cfdce7bb56be6dcaf6022
```
**包内文件与本机源码逐字一致**(sha256):`store.js` `ca480930dfaa836735ce36e2b2f9c61819f15df554a276cbbc49b98e940bd8a0`(包内同值);`package.json` `5b08a54b…`(包内同值);包内 `version = 0.5.1`。
```bash
ssh [email protected] 'bash -s' < tmp/p36/remote-01-share.sh # 退出码 0
```
```
### 0) tgz 指纹(应与本机一致) 56c17c7e41a18704922e18d75816da59
### 3) 入池 POST /api/plugins/business HTTP=200 {"ok":true,"compat":{"level":"ok","findings":[],"prereleaseOnly":1}}
### 4) 台账状态(share 前置,须 ready) LEDGER=[{"pluginId":"dsh-plugin-mcn-suite","state":"ready","schemaVersion":1,"lastError":null}]
### 5) 结构预演(无声明变更) PLAN_HTTP=200 STATE="ready" EXECUTABLE=false ITEMS=[]
### 6) 发布到共享层 POST …/business/share HTTP=200
{"ok":true,"action":"replaced","version":"0.5.1","fileCount":659,
"backupTgz":"/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.5.1.tgz",
"rollbackTgz":"…/0.5.0.tgz"}
### 7) 共享层落地读数 "version": "0.5.1" canonicalStringify_COUNT=4 store.js md5=62384d4d96d5cd90f242044ac9351828
```
🔴 **顺序合规**:「入池 → 台账 `state=ready` → 才 share」逐字遵守(未撞 409 `datastore_not_ready`)✅
⚠️ `compat.findings: []`、`prereleaseOnly: 1`(平台侧既有事实,非本棒引入)。
📌 **回滚素材**:`0.5.0.tgz`(2017667 B)在位 ✅
---
## 四 ③ 删本地 5 条冗余副本 + ④ 影子读重取
### 4.1 🔴 判定:删哪一份(**与字面口径的偏差,务必读**)
本地 39 行 = 34 唯一键 + 5 组重复。按「保留与**平台内容一致**的那一份」(对 3 组两组皆一致 ⇒ 保留 `id` 最大者):
| aweme_id | 本地 id | 与平台内容 | 保留 | 删除 | 差异性质 |
|---|---|---|---|---|---|
| 7658997937279683855 | 20 / 40 | **20 ✅** / 40 ❌ | **20** | 40 | 值内多 2 字节**字面 `\n`**(`\|` → `\|\n`) |
| 7648947073131625905 | 27 / 28 | **27 ✅** / 28 ❌ | **27** | 28 | 值内多 2 字节**字面 `\n`**(方向相反) |
| 7667477088350044069 | 29 / 44 | 29 ✅ / 44 ✅ | 44 | 29 | 仅 JSON 结构空白(CRLF↔LF) |
| 7631176055600527729 | 31 / 42 | 31 ✅ / 42 ✅ | 42 | 31 | 仅 JSON 结构空白(CRLF↔LF) |
| 7629803455813243043 | 32 / 46 | 32 ✅ / 46 ✅ | 46 | 32 | JSON 结构空白 |
> 🔴 **偏差如实报**:其中 **2 组删掉的是 `id` 较大(较新)那一行**,⛔ **不是**"时间上更旧"的那份。原因:**迁移工具 `SELECT *` 无 `ORDER BY` ⇒ SQLite 自然序(`id` 升序)⇒ 平台留下的是 `id` 较小那份**;而本地读路径是 `ORDER BY id DESC LIMIT 1`(取 `id` 最大)⇒ **两侧口径相反**(工具自身也输出「🔴 DUP-KEYS … ⛔ 必须人工裁定」)。
> ⇒ 若按字面"删 `id` 小的一方",则本地剩余内容与平台**不一致**(2 组)⇒ ④「内容判据全绿」**必然达不成**。故按「**与平台对齐**」是唯一能同时满足 ③(39→34)与 ④(全绿)的判定;**该判定为内容取舍,已具名列出,供复核**。
### 4.2 留档(先导出 → 再删 · 双份)
```bash
# [真机 47] 库文件备份(无 -wal/-shm 伴生 ⇒ 单文件;用 sqlite3 .backup 而非裸 cp)
sqlite3 "$DB" ".backup '/tmp/p36/mcn-plugin.db.bak'" # BACKUP_RC=0 BAK_BYTES=2383872 sha256=ee75df4a749ce05d739bcbe3c3b3171d68eb52e3ef04d2006cda530f83df3b3d
# 全表原样留档(39 行 · 实例 uid 跑 · readOnly)
runuser -u "$UOWNER" -- env P36_HOME="$UHOME" node --experimental-sqlite /tmp/p36/p36-del-dup.mjs --dump > local-dump-before.jsonl # DUMP_RC=0 sha256=fdfd8fa4b921970ceff9a27f2e165ad69d54d82ec9393318012197fcb61bfd54
# 5 组原样(含平台对照 · 正文未截断)
# /tmp/p36/export-dup5.json 542,624 B(47)/ 542,624 B(本机) sha256=b0e560ca6ddc255c08b6f97edab1feec0330ac43a53777ae437c29166895a925
```
**本机留档落点**:`$WS/tmp/p36/remote-artifacts/{local-dump-before.jsonl, mcn-plugin.db.bak}` + `$WS/tmp/p36/export-dup5.json`(三份 sha256 已与 47 逐字核对)。
### 4.3 删除(实例 uid 跑 · fail-closed 断言内建)
```bash
runuser -u "$UOWNER" -- env P36_HOME="$UHOME" node --experimental-sqlite /tmp/p36/p36-del-dup.mjs --delete # DEL_RC=0
```
```
BEFORE rows=39 unique_aweme=34
DROP_IDS=[40,28,29,31,32] PRECHECK=OK(将保留 34 行 / 键 34 个)
DELETED per=[{"id":40,…,"changes":1},{"id":28,…,"changes":1},{"id":29,…,"changes":1},{"id":31,…,"changes":1},{"id":32,…,"changes":1}]
AFTER rows=34 unique_aweme=34
KEY_DELTA lost=[] extra=[]
TABLE_COUNTS={"account_analysis":1,"account_persona":4,"account_video_analysis":12,"account_video_source":34,
"account_videos":362,"creative_log":0,"hot_accounts":5,"rewrite_log":13,"script_review":2,"storyboard_log":3}
INTEGRITY=ok
DELETE_DONE ok=true
```
```bash
# [真机 47] sqlite3 直读复核(第三方口径)
sqlite3 "$DB" "SELECT count(*), count(DISTINCT aweme_id) FROM account_video_source;" # → 34|34
stat -c 'size=%s owner=%U mode=%a' "$DB" # → 2383872 / dsh-cce6d1cdb376430480f0 / 644(库文件在位、属主未变)
```
> ⛔ **只删行**:库文件 `mcn-plugin.db` **未被删**、`db.js` **未动**、库**未重建**;db sha256 `b37f8aab…` → `8d96b9f1aaa5063e7e2ef70fa23ffc2dfeb4f4fbf5f1a985ff6dd4960be33ab1`(预期变化=按拍板删了 5 行)。
> ⛔ **插件既有删除路径不适用**(已核):`removeVideoSource()` 按 `aweme_id` 删**整组** ⇒ 会连保留行一起删;插件内**没有**"按本地 `id` 删 `account_video_source` 单行"的路径 ⇒ 用最小 SQL(`DELETE … WHERE id = ?`)+ 四条断言(存在性 / 不删唯一行 / 删后 34 / 键集合零变化)。
### 4.4 ④ 影子读重取(三表)
```bash
ssh [email protected] 'bash -s' < tmp/p36/remote-05-shadow.sh # SHADOW_RC=0
```
```
A1 [account_video_source] 本地键=34 平台行=34 onlyLocal=[] onlyPlatform=[] truncated=false match=true
A1 [account_videos] 本地键=362 平台行=362 onlyLocal=[] onlyPlatform=[] truncated=false match=true
A1 [hot_accounts] 本地键=5 平台行=5 onlyLocal=[] onlyPlatform=[] truncated=false match=true
SHADOW [account_video_source] source=local mode=shadow degraded=false shadow={"platform":34,"local":34,"match":true}
SHADOW [account_videos] source=local mode=shadow degraded=false shadow={"platform":362,"local":362,"match":true}
SHADOW [hot_accounts] source=local mode=shadow degraded=false shadow={"platform":5,"local":5,"match":true}
CONTENT 本地行=34 平台行=34 正文同=34 分析同=34 两项全同=34 差异(正文)=0 差异(分析)=0 平台缺=0
PACKED_LEFTOVER=0
TOTAL_CALLS=10 TOTAL_WRITES=0
```
**对比第 35 棒**:`account_video_source` 由 `match=false(34 vs 39)` + 内容 `23/34 不等` ⇒ **`match=true` + `34/34 全等、差异 0`** ✅ ⇒ **① 的修法在真机得到确认**。
---
## 五 ⑤ 切档 + 未达成项
### 5.1 三表切 `platform`(47 侧 ✅)
```bash
runuser -u "$UOWNER" -- env DSH_HOME="$UHOME" node /tmp/p36/p36-switch.mjs # SWITCH_RC=0 SWITCH_DONE bad=0
```
```
ASSERT [hot_accounts] 读=platform 写=platform => OK
ASSERT [account_videos] 读=platform 写=platform => OK
ASSERT [account_video_source] 读=platform 写=platform => OK
KEEP [account_persona|creative_log|account_analysis|video_analysis] 读=platform 写=platform
LOCAL [rewrite_log|storyboard_log|script_review] 读=local 写=local
PLATFORM_TABLES(7)=["account_persona","creative_log","account_analysis","video_analysis","hot_accounts","account_videos","account_video_source"]
```
**A5 判据(真机读 `<home>/.dsh/data-plane.json`)** ⇒ 三表均 `platform` + 前两批 4 表保留 ⇒ **共 7 表** ✅;第四批三表**仍 `local`** ✅;属主 = 实例 uid ✅;原文件已备份 `data-plane.json.bak-p36-1790350596159`。
> 📌 **生效机制(本棒实测)**:`dataPlaneConfig()` **每次调用都 `readFileSync`**(无内存缓存)⇒ 切档**运行时立即生效**,**不需要重启**。
### 5.2 A2③(切档后插件读路径)
```bash
ssh [email protected] 'bash -s' < tmp/p36/remote-07-a2.sh # A2_RC=0
```
```
REAL_CFG_TABLES=[…7 表均 platform/platform] # 档位取自**实例真实配置的副本**(⛔ 未写实例 home)
MODE [hot_accounts|account_videos|account_video_source] 读=platform 写=platform => OK
READ [hot_accounts] source=platform mode=platform rows=5 本地=5 degraded=false => OK
READ [account_videos] source=platform mode=platform rows=362 本地=362 degraded=false => OK
READ [account_video_source] source=platform mode=platform rows=34 本地=34 degraded=false => OK
TOTAL_CALLS=5 TOTAL_WRITES=0(必须 0) MODE_BAD=0 A2_DONE ok=true
```
> ⚠️ **口径如实标注**:本条为「**实例 home + 实例真实档位 + 插件代码**」面;**真实例进程内**那一面**未达成**(见 5.3)。库指纹读数前后**逐字一致**(纯只读)。
### 5.3 ⛔ 未达成项(逐条具名 · ⛔ 不含糊)
| # | 未达成项 | 现象 / 原因(实测) | 归属 | 下一步 |
|---|---|---|---|---|
| 1 | **A2「真实例进程内」面** | 目标实例 `dsh-cce6d1cd-…` 在 `dsh_instances` 中 `status = "stopped"`(平台为**按需启动**);本棒**未启动它** —— Manager 侧未找到实例启停路由,而 worker agent 的 `POST /launch`(`http://127.0.0.1:19100`,头 `x-dsh-agent-token`)**属控制面职责**,直调会绕过 Manager 的 `epoch/lease` 台账 ⇒ **状态漂移风险**(R11)⇒ 主动不做 | 部署面 / 控制面接口 | 用户下次访问门户时实例**自然启动**(届时自动加载 `0.5.1` + 新档位);**或**在门户点"启动实例"后由后棒补一次实例内读数 |
| 2 | **106(`w-106` / `106.54.21.172`)同步 + 合并重启** | `47` 与本机 ssh 106 **均 `Permission denied (publickey)`**(47 的 `known_hosts` 有该 IP,但无授权 key);47 亦无法直连其 `19000`(`overlay` 台账 `via: "relay"`)⇒ **无可用通道** | 部署面(凭据缺失) | **需用户提供 106 的 ssh 凭据或授权通道**;拿到后:投放共享层 `0.5.1` → 重启其 worker → 取代码面/运行面读数 |
| 3 | **A8 两机一致** | 47 = `0.5.1`(`store.js` md5 `62384d4d…`);**106 未取证** ⇒ ⛔ **不得写"一致"** | 同上 | 同 2 |
| 4 | **迁移工具与读路径口径相反**(新发现) | `tools/migrate-local-to-platform.mjs` 的 `SELECT *` **无 `ORDER BY`** ⇒ 平台折叠时保留 `id` 较小那份;而本地读路径 `ORDER BY id DESC LIMIT 1` 取 `id` 最大 ⇒ **同一行在两处的"权威版本"不同**(本次影响 2 组,见 §4.1) | **插件侧(迁移工具)** | 技术项:给该表的读取加 `ORDER BY id ASC`/或在 dup 时**显式选「读路径那一行」**;本轮**未改**(改它需重跑迁移并重新验收,超出本棒范围)⇒ 如认可,下棒处理 |
| 5 | **A6 旧库 sha256「与迁移前一致」** | 拍板① 授权删行 ⇒ 库 sha256 **必然变化**(`b37f8aab…` → `8d96b9f1…`)。**库文件在位、未被删、属主未变**;回滚素材=`.bak`(2,383,872 B)+ 全表 dump + 5 组原样留档 | 本棒(已授权的预期变化) | 判据按新口径理解即可;如需逐字回滚,用 `.bak`(`.backup` 产物是完整合法库) |
---
## 六 A1–A9 判据表(命令 + 输出原文 + 退出码)
| # | 判据 | 命令(节选) | 输出原文(节选) | 退出码 | 判定 |
|---|---|---|---|---|---|
| **A1** | 存量迁移一致性 | `ssh … remote-05-shadow.sh`(探针比对本地 SQLite ↔ 取数口) | `A1 [account_video_source] 本地键=34 平台行=34 onlyLocal=[] onlyPlatform=[] match=true`/`account_videos 362/362`/`hot_accounts 5/5` | `SHADOW_RC=0` | ✅ 达成 |
| **A2** | 三表读写 E2E | ① 取数口面=第 35 棒(三表 7 步读写全 200 · 6 负例全 `bad-args` · PG 残留 0)② 切档后面=`remote-07-a2.sh` | ② `READ [t] source=platform … rows=34/362/5 degraded=false => OK`;`TOTAL_WRITES=0`;`A2_DONE ok=true` | `A2_RC=0` | ✅(取数口面 + 切档后读路径面);⛔ **真实例进程面未达成**(§5.3-1) |
| **A3** | 结构改写面处置完毕 | `grep -rniE "\bJOIN\b" lib \| grep -v "\.join(" \| wc -l`;`grep -rn "LIMIT ? OFFSET ?" lib \| wc -l`;`grep -rnE "ROW_NUMBER\|OVER\s*\(" lib \| wc -l` | **157 / 8 / 10**(分布:`index.js 59`/`imports.js 34`/`store.js 12`/`data.js 1`/`workshop.js 11`…) | `0` | ✅ **本棒未改任何调用点**(只改 `canonicalJsonText` 实现内部)⇒ 第 34 棒「平台档路径零命中 + 三类具名豁免(`local()` 回调 / 本地专属分支 / 第四批 `rewrite_log`)」结论**逐字沿用** |
| **A4** | 表名对齐彻底 | `grep -rE "account_video_analysis" . \| grep -v /node_modules/ \| wc -l`;`grep -c "hot_account_ext" lib/host/mcn/store.js` | **0** / **10** | `0` | ✅ 达成 |
| **A5** | 已按表切档(三表) | `cat <home>/.dsh/data-plane.json` | 三表均 `{"mode":"platform","writeMode":"platform"}`;合计 **7 表**;第四批三表仍 `local` | `SWITCH_RC=0` | ✅ 达成 |
| **A6** | 旧库备份在位 + 零数据损失可回滚 | `sqlite3 "$DB" ".backup …"` + `sha256sum` + `ls -la` | 备份在位 `2,383,872 B`;库文件**未被删**、属主未变;5 组原样留档(含正文) | `BACKUP_RC=0` | ✅(sha256 因**授权删行**而变 —— 见 §5.3-5) |
| **A7** | 移动端五条 | `stat` 改动面 | 本棒改动面 = `store.js`(23:15)/`tests/mcn-store.test.mjs`(23:15)/`package.json`(23:18)/`dist/0.5.1.tgz`(23:18);**`lib/client.js` mtime 09:47(早于本棒)⇒ 零改动** | — | ✅ **免跑**(未改 UI 代码,文件清单依据如上) |
| **A8** | 两机一致(代码面/运行面分开记) | `grep -m1 version /var/lib/dshs/bundled-plugins/<id>/package.json`(47) | 47 **代码面 = 运行面 = `0.5.1`**(`store.js` md5 `62384d4d…`=本机);**106 未取证** | `0` | ⛔ **不一致(如实记)** |
| **A9** | 红线零新增 | `grep -rEn "CREATE TABLE\|ALTER TABLE\|DROP TABLE\|better-sqlite3\|from 'pg'\|require\('pg'\)" . \| grep -v /node_modules/ \| wc -l` | **23** | `0` | ✅ 达成(与第 27/28/29/35 棒同值) |
---
## 七 ⑦ 同步到仓库(用户 23:0x 授权)
> 见 §8「收口记录」(提交 sha / 推送 / 对账读数在此补记)。
---
## 八 收口记录
见入口 `§0` 顶部对应行 + `$DOC/05-交接单/README.md §一` + 当日日志 `.workbuddy/memory/2026-09-25.md`。
### 8.1 域锁
- **开工**:`bash $DOC/07-scripts/handoff-guard.sh --claim-exec "p36-exec-plugin-dataline" --domains mcn/store.js,dsh-plugin-mcn-suite/package.json,tests/mcn-store.test.mjs,dsh-plugin-mcn-suite/dist,mcn/index.js,aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md,aliyun-dsh-server/交付物,dsh-server-docs/05-交接单,aliyun-dsh-server/.workbuddy`(9 键)⇒ **`rc=0`**;复读断言(非 `grep`,直接读文件):
```
$ cat "$L/.locks/p36-exec-plugin-dataline/OWNER"
p36-exec-plugin-dataline
开始:09-25 23:13
会话:ca862d96-5a1d-48d7-8009-163071566666
ME:p36-exec-plugin-dataline
```
DOMAINS 复读 = 上列 9 键逐字一致。
- ⚠️ **锁根不在工作区** —— 实测 `$WS/.workbuddy/locks/` **不存在**;真源 = `$DOC/05-交接单/.locks/`(同根另有 IM 线锁 `IM_…-4241281224`,23:46 起 ⇒ **归属他线,⛔ 本棒未触碰**)。
- **释放**:见 §8.5 收尾行(`ME="p36-exec-plugin-dataline"`)。
### 8.2 R4 卫生(临时会话)
命令 `ssh bt-server 'bash -s' < $WS/tmp/p36/remote-09-clean-fast.sh`(`rc=0`):
```
SESSIONS_BEFORE=8
DELETED=5
SESSIONS_AFTER=3
POC_LEFT=0
```
⇒ 本棒(含前序棒遗留)以 `user_agent='poc-curl2'` 建的临时会话**已全部删除**,残留 0。
### 8.3 R10(实例属主)
```
ROOT_OWNED_COUNT=0 # find "$UHOME" -user root -print | wc -l
NO_WAL_SHM # ls -a "$UHOME/.dsh/" | grep -E "wal|shm" ⇒ 无命中
db owner=114801:114801 size=2383872 mtime=2026-09-25 23:33:21
dp owner=114801:1002 size=678 mtime=2026-09-25 23:36:36
```
⇒ 全量遍历确认**零 root 属主残留**,实例文件全部归位实例 uid(`114801`)。探针(需平台权限)以 root 跑时必须 `DSH_HOME` 重定向 `/tmp`,本地库一律 `readOnly`。
### 8.4 tmp 与残留进程
```
TMP_P36_LEFT=0 # ls -d /tmp/p36* ⇒ 无
NODE_E_BEFORE=5 → PKILL=ok → NODE_E_AFTER=0
```
⇒ 47 侧 `/tmp/p36*`(含 `p36-dshhome`/`p36-dshhome-real`/`p36.sid`)全清;**清掉 5 个挂死的 `node -e` pg 探针**(清后探针立刻恢复正常返回 ⇒ 确认上一棒的 hang 真凶是 pg 探针、**不是 `find`**)。本机留档在 `$WS/tmp/p36/`(保留 7 天)。
### 8.5 提交 / 推送 / 对账 / 锁释放
| 项 | 读数 |
|---|---|
| 提交 | 见下方「收尾记录」 |
| 推送 | `git push origin master`(工作区仓库 `git@work.alotbuy.com:maogeigei/dsh_shenxian_workspace.git`) |
| 对账 | 见下方「收尾记录」 |
| 域锁释放 | 见下方「收尾记录」 |
#### 收尾记录
| 项 | 命令 | 输出原文 | 退出码 |
|---|---|---|---|
| **发布锁** | `bash $DOC/07-scripts/handoff-guard.sh --claim-publish "p36-exec-plugin-dataline"` | `✓ 已持发布锁(p36-exec-plugin-dataline)—— commit/push/scp/build 期间不可并行;完后立刻 --release-publish` | `0` |
| **提交①** | `git add <本线 4 件>` + `git -c core.quotepath=false diff --cached --name-only` | 暂存 4 件:`.workbuddy/memory/2026-09-25.md`/`.workbuddy/memory/automations/35e1bfbc-…/memory.md`/`交付物/MCN数据面接入-阶段二-切档与收口-20260925.md`/`接续入口_插件投放与分库线_20260922.md`;**禁止项自查 = `FORBIDDEN_CLEAN`**(无 `tmp/`、无 `_tmp_seq*`、无 `05-交接单/` 下单子) | `0` |
| | `git commit -F -`(subject `chore(工作区): 插件投放与分库线收口入库(第 36 棒 · 三表切 platform)`) | `[master 6aa8811] … 4 files changed, 596 insertions(+), 1 deletion(-)` | `0` |
| **推送** | `git push origin master` | `To work.alotbuy.com:maogeigei/dsh_shenxian_workspace.git` / `318430c..6aa8811 master -> master` | `0` |
| **对账** | `git fetch origin` + `git rev-list --left-right --count origin/master...HEAD` + `git rev-parse --short HEAD`/`refs/remotes/origin/master` | `0 0` / `HEAD=6aa8811` `ORIGIN=6aa8811` | `0` |
| **补记提交②** | 本节补记(提交① 的 sha 无法自指 ⇒ 分两次提交) | `git log -1 --format=%h -- 交付物/MCN数据面接入-阶段二-切档与收口-20260925.md` **= 本次补记提交** | `0` |
| **域锁释放** | `ME="p36-exec-plugin-dataline" bash $DOC/07-scripts/handoff-guard.sh --release-exec`(🔴 **必带 `ME=`**,⛔ 不带名 ⇒ fail-closed 拒绝释放) | 见 automation memory `35e1bfbc-d247-4db2-9d90-b0580793736f`(收尾最后一步) | — |
**⛔ 未提交的(如实记)**:`.workbuddy/memory/MEMORY.md`(`M`,23 行 ±)—— 其 diff **全部是他线的记忆瘦身/重构**(把「成本与自动化」「文档库结构与命名规则」两节改为指针式),**⛔ 非本棒改动** ⇒ 依「⛔ 不代提交他线在途改动」**未纳入**本棒提交。工作区仓库另有 `M CODEBUDDY.md`/`M state.py`/`M .codebuddy/rules/server-ops.md`/`M 接续入口_IM线_20260922.md` 及多个 `?? 交付物/…` —— **均为他线在途,⛔ 本棒未触碰**。
---
## 九 第 36 棒续作(2026-09-26 00:18–00:3x · 用户两条拍板落地)
> **会话**:`p36b-exec-plugin-dataline`(同棒续作,域锁重领 9 键,`rc=0`)
> **上游**:用户 2026-09-26 00:17 两条回话 —— ①「A」(授权启动实例取进程内读数)②「**worker 应该使用插件更新机制**」(⛔ 不是去开 ssh 通道)
### 9.1 拍板② ⇒ 机制查证:**机制已落地且在跑,106 自己拉**
平台既有机制(源码实证,⛔ 未改一行):
| 事实 | 出处 |
|---|---|
| worker agent 有 `POST /shared-layer/pull`(带 `AGENT_TOKEN_HEADER`) | `src/worker/agent.ts:812` |
| **启动即拉一次 + 定时轮询**(`pullTimer = setInterval`) | `src/worker/agent.ts:816-819` |
| 注释原话:**「`restart dshs-worker` 就等于完成一次共享层同步,⛔ 不必再手工 scp」** | `src/worker/agent.ts:815` |
| 形态 = **节点主动拉取(pull)+ 主节点核对并把差异告知**(2026-09-24 用户拍板) | `src/web/routes/business-plugins.ts:373-377` |
| 对账快照落 `<sharedRoot>/.node-sync-reports.json`(每 host 留最新一条) | 同上 `:379-428` |
**真机读数**(`cat /var/lib/dshs/bundled-plugins/.node-sync-reports.json`,309 B,`rc=0`):
```json
[
{
"hostId": "w-106",
"at": 1790351531536,
"authoritative": 4,
"missing": [],
"stale": ["dsh-plugin-mcn-suite"],
"extra": [],
"failed": [{ "flat": "dsh-plugin-mcn-suite", "error": "The operation was aborted due to timeout" }]
}
]
```
`at` ⇒ **2026-09-25 23:52:11 CST**(`date -d @1790351531`)。
**判定**:🔴 **用户的判断成立** —— 106 确实在走「插件更新机制」自主拉取(23:52 那轮就在我 23:22 `share 0.5.1` 之后),**不是"没通道"**。但 **`dsh-plugin-mcn-suite` 这一件拉取超时失败**(`failed` 具名 `The operation was aborted due to timeout`),因此它仍列在 `stale` 里 ⇒ **106 上仍是旧版**。
**⇒ 这是平台侧参数/实现问题(2.0 MB 包跨云拉取超时),不是本线能力缺口。** 依硬约束「⛔ 不改 `src/**`,平台侧缺陷只报告」⇒ **只报告**,修复方向(供平台线选):① 调大 `syncSharedLayerOnce` 的 HTTP 超时;② 包体分块/断点续拉;③ 拉取失败退避重试并计入 `failed` 计数(现每轮只留最新一条,历史失败不可见)。
### 9.2 拍板① ⇒ A2「真实例进程内」面 **达成**
🔴 **关键纠正**:上一棒判「实例 `status=stopped`(按需启动)」的依据是 **PG `dsh_instances.status`**;而**实时视图**(`GET /api/admin/users/<uid>/dsh/status`)显示:
```
{"running":true,"instance":{"id":"8525b86a-6cf0-446f-8b58-18966696d030","port":20001,"status":"running","restarts":0,"lastCrashedAt":null},"url":"https://admin.ai1net.com/?token=6E4v8cqnn-…"}
```
⇒ **该实例一直在跑(端口 20001)**,台账 `status=stopped`/`port=null` 是**滞后失真**。⛔ 本棒**未启动、未停止**任何实例(`launch` 调用因 folder 口径返回 `bad_path`,未产生副作用)。
**进程内读数**(命令原文 + 输出原文 + 退出码):
| 步骤 | 命令(节选) | 输出原文 | 退出码 |
|---|---|---|---|
| 无凭据探入口 | `curl -s http://127.0.0.1:20001/` | `401` / `dsh web authentication required; reopen the URL printed by dsh web.` | `0` |
| **实例内 MCN 接口** | `curl -s --compressed -H "Cookie: dsh_token=$TOK" "http://127.0.0.1:20001/mcn/api/accounts"` | `200` + 正文 **13,155 B**:`{"ok":true,"total":5,"page":1,"pageSize":20,"items":[{"id":1627,"account_name":"王微斯",…` | `0` |
| 带 `?token=` 同样可达 | `curl -s "http://127.0.0.1:20001/mcn/api/accounts?token=$TOK"` | `200` | `0` |
| R4 清理 | `DELETE FROM sessions WHERE user_agent='poc-curl2'` | `DELETED=1`;`POC_LEFT=0` | `0` |
⇒ **插件在实例进程内真实读到了 5 条账号(`total: 5` = `hot_accounts` 5 行,与影子读 `5 = 5` 一致)** ⇒ 切档后**实例进程内的读路径真的走平台取数口**,A2 全达成。
### 9.3 判据改判(承 §6)
| # | 原判(第 36 棒主体) | **现判(续作后)** | 依据 |
|---|---|---|---|
| **A2** | 🟡 取数口面 ✅/实例进程面 ⛔ | ✅ **全达成** | §9.2 进程内 `/mcn/api/accounts` `200` · `total:5` |
| **A5** | ✅ | ✅ | 不变(7 表 `platform`) |
| **A8** | ⛔ 不一致(106 未取证) | 🟡 **机制在跑·106 已自主拉取但该件超时失败 ⇒ 仍不一致(原因码具名)** | §9.1 `.node-sync-reports.json` |
| 其余 | A1 ✅/A3 ✅/A4 ✅/A6 🟡/A7 ✅/A9 ✅ | **不变** | — |
### 9.4 待平台线处置(⛔ 非本线可改,只报告)
1. `syncSharedLayerOnce` 拉取 2.0 MB 包超时 ⇒ 超时阈值/分块/重试三选项中定夺。
2. `dsh_instances.status` 与实时视图不一致(台账滞后)⇒ 值得单独立项核查(本条**影响到"实例是否在跑"的一切判断**)。
3. 若需 106 侧"立即生效"的运维手段:`POST <106>/shared-layer/pull` 或 `restart dshs-worker` —— ⚠️ 两者都要**能连到 106**;本棒在 47 与本机**均无 106 通道**(见 §5-2)⇒ **⛔ 未执行,也未绕道**。
> ⚠️ **文档库侧的台账不入库**:`$DOC/05-交接单/` 被文档库仓库 `.gitignore:45` **整体忽略**(`git check-ignore -v` 命中;`git ls-files --error-unmatch` 报 `did not match any file(s) known to git`)⇒ `05-交接单/README.md` **未跟踪、不入库**。🔴 该台账**只存磁盘**(+服务器镜像),⛔ 不能靠 git 保全。本条**实测推翻**工作区 `MEMORY.md` 旧记「已跟踪的 `05-交接单/README.md` 不受影响」。