Files
dsh_ai1net_server/交付物/MCN数据面接入-阶段二-切档与收口-20260925.md
T
admin df876aec90 chore(工作区): 第 36 棒续作收口(A2 实例进程内面达成 · A8 改判)
- 拍板①「A」:实例一直在跑(实时视图 running@20001);进程内 /mcn/api/accounts 200 · total:5
  ⇒ A2 实例进程内面达成。纠正上一棒:其判「stopped」所据 PG dsh_instances.status 是滞后失真
- 拍板②「worker 用插件更新机制」:机制已落地且在跑(agent POST /shared-layer/pull + 定时轮询)
  真机对账快照 w-106 @23:52:11:stale:[mcn-suite] + failed:[timeout]
  ⇒ 106 自主拉取成立、该件拉取超时失败 ⇒ A8 由「未取证」改判为「机制在跑 · 该件超时」
- 平台侧问题(拉取超时 / 台账滞后)只报告不改码
- 收尾:R4 临时会话 DELETED=1 / POC_LEFT=0;未启动未停止任何实例
2026-09-26 00:36:34 +08:00

404 lines
30 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 数据面接入 · 阶段二 · 第三批 —— **切档与收口**(第 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` 不受影响」。