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/规则与载体/、归档/、接续包_*。
This commit is contained in:
1 parent
c1461fd958
commit
318430c9e9
27 files changed
+5649
-72
No files matched your search
@@ -0,0 +1,212 @@
|
||||
# MCN 数据面接入改造 · 阶段一(纵向切片)— 执行回报
|
||||
|
||||
- **执行棒**:插件投放与分库线 · 第 25 棒(执行棒)|会话 `插件投放与分库线-第25棒`|witness 时间窗 **2026-09-25 13:10 – 14:1x**
|
||||
- **唯一执行单**:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-02MCN数据面接入改造.md`(8 段齐全)
|
||||
- **入口**:工作区根 `接续入口_插件投放与分库线_20260922.md` §2(第 25 棒条目)
|
||||
- **判据口径**:全部读数取自**真机**(47 `bt-server` / 106 `test106`);每条附命令、输出原文、退出码。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论
|
||||
|
||||
**阶段一部分完成(A1–A4、A6 达成;A5 按单内预期记为「未达成」;A7 卫生六条全清)。** 未达成项只有一条且是**单内预先排除**的(红线归零属阶段二),另有 **3 项未闭合**(1 项待窗口、2 项属平台侧缺陷只报告)。
|
||||
|
||||
- ✅ **端到端最容易翻车的一段已打通**:声明(11 表)→ 上传 → `plan` → `CREATE DATABASE` → `migrate` → `state=ready` → 取数口可读可写可删。
|
||||
- ✅ **两张表读路径切平台**:`account_videos` 平台侧读回 **36 行 = 本地 36 行**,游标翻页正确;`account_persona` 读回 **2 行**(洗净种子)。
|
||||
- ✅ **`available:true`**,11 张表全在列;归属列由内核强制注入(返回值带 `user_id`,无需插件自己写)。
|
||||
- ⚠️ **发现一条真实的阶段二阻塞**:6 个 `json` 声明列中有 **17 行存量值不是合法 JSON**(纯 Markdown)⇒ 声明成 `json` 时无法 verbatim 写入。详见 §五-未闭合项-1。
|
||||
|
||||
---
|
||||
|
||||
## 二、判据表(命令 + 输出原文 + 退出码)
|
||||
|
||||
### 前置门禁(§二 只读前置 P1–P6 + §四 4.2 三条)
|
||||
|
||||
| # | 判据 | 命令 | 输出原文(摘) | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | 包内原无 `data` 键 | 读 `package.json` | `"dsh":{"bundle":…,"client":…}`;`files` 不含 `dsh.data.yaml` | ✅ |
|
||||
| P2 | 红线命中数 | `grep -rEc 'CREATE TABLE\|ALTER TABLE' lib/*.js lib/**/*.js` | 命中 **22**(10 张表 `CREATE TABLE` + 1 处注释 + 11 条 `ALTER TABLE`)= 与单内一致 | ✅ |
|
||||
| P3 | 真实改造面 | 同上扫描 | **SQL 调用点 222**(`index.js` 94/`imports.js` 51/`db.js` 37/`workshop.js` 26/`data.js` 11/`client.js` 2/`video-tools.js` 1) | ✅ |
|
||||
| P4 | 本地库落点 | 读 `lib/host/mcn/config.js` | `DSH_DIR = process.env.DSH_HOME ? join(DSH_HOME,'.dsh') : join(homedir(),'.dsh')`;`DB_PATH = join(DSH_DIR,'mcn-plugin.db')` | ✅ |
|
||||
| P5 | 取数口未就绪态 | `curl … /api/im/plugin-data/status`(无凭据⇒401;带身份) | `{"ok":false…}` 401 / `{"ok":true,"pluginId":"dsh_plugin_mcn_suite","available":false,"reason":"no-declaration",…}` | ✅ |
|
||||
| P6 | 平台校验函数在位 | 读 `lib/db/plugin-data/schema.js` | `validateDecl` / `parseDeclFromDir` / `pluginDbNameOf` 三者皆在 | ✅ |
|
||||
| 4.2-1 | 列级上限 | 读 `schema.ts` | `json` 列必填 `maxBytes`;行上限 `maxRowBytes=64 KiB`(自决 30720/49152 合规) | ✅ |
|
||||
| 4.2-2 | **回滚素材在位** | `ls /opt/dsh/backups/plugins/dsh-plugin-mcn-suite/` | `0.3.9.tgz` / `0.3.14.tgz` / **`0.3.15.tgz`** | ✅ |
|
||||
| 4.2-3 | 台账原状态 | `select … from plugin_datastores` | 原为空 `[]`;对账 `clean:true` | ✅ |
|
||||
|
||||
> ⚠️ **P1 门禁前置已满足**:`state=none` ⇒ 门禁放行,故可先投放声明再建库。**但"先建库再投放"的顺序仍按 §五 S7→S8 执行**(见 §七 硬约束)。
|
||||
|
||||
### 实施(S1–S8)
|
||||
|
||||
| 步 | 判据 | 输出原文(摘) | rc |
|
||||
|---|---|---|---|
|
||||
| **S2** | 声明离线校验 | `level=ok` `tables=11` `columns=96` `indexes=14` `declarationDdl=25`;全部 `physicalName` 匹配 `/^p_[a-z][a-z0-9_]{0,40}$/`;全表 `scope=user`;文件纯 LF | 0 |
|
||||
| **S3** | 适配层单测 | `node --test tests/mcn-store.test.mjs` ⇒ **13 tests / 13 pass / 0 fail** | **0** |
|
||||
| **S4** | 三件语法 + 红线复核 | `node --check` ×3 全过;红线 **仍 22**(未删 `db.js`)⇒ **A5 如实记「未达成」** | 0 |
|
||||
| **S5** | 出包与差集 | 与 0.3.15 改前副本比对:**新增** 5 件(`dsh.data.yaml`/`lib/host/mcn/store.js`/`lib/im-plugin-client/index.mjs`/`tests/mcn-store.test.mjs`/`tests/smoke.mjs`);**内容不同** 3 件(`lib/host/mcn/data.js`/`lib/host/mcn/index.js`/`package.json`);`onlyBase=[]`(**无丢件**);包内 **324 件** | 0 |
|
||||
| **S6** | 上传候选池 | `HTTP=200`;池内 `version:"0.4.0"`;`compat.blocked:0`;`warnings:120` | 0 |
|
||||
| **S7** | 建库两段式 | ① `plan` ⇒ `state=pending`,**25 项**待做;② `datastore` ⇒ `outcome:"created"` **159 ms**,`state:"created"`;③ `migrate` ⇒ `state:"ready"`,`schemaVersion:1`,**25 条 executed**;④ 复演 ⇒ `{"state":"ready","items":0,"forbidden":0}`;⑤ 台账 `state=ready`;⑥ 对账 `clean:true`(`databases`=`ledgered`=`["dshs_pl_dsh_plugin_mcn_suite"]`,`orphans:[]` `missing:[]`) | 0 |
|
||||
| **S8** | 发布共享层 | `action:"replaced"`/`version:"0.4.0"`/`fileCount:659`/`backupTgz:…/0.4.0.tgz`/`rollbackTgz:…/0.3.15.tgz` | 0 |
|
||||
|
||||
### 验收 A1–A7
|
||||
|
||||
| # | 判据 | 命令 | 输出原文 | 判定 |
|
||||
|---|---|---|---|---|
|
||||
| **A1** | 库与台账 | ① `select count(*) from pg_database where datname='dshs_pl_dsh_plugin_mcn_suite'` ② `select row_to_json(x) from plugin_datastores x where plugin_id='dsh-plugin-mcn-suite'` | ① `A1_PG_DB_ROWS=1` ② `{"plugin_id":"dsh-plugin-mcn-suite","db_name":"dshs_pl_dsh_plugin_mcn_suite","state":"ready","schema_version":1,"plan_hash":"58226942d82290388e3090ed53e29f60bd71bd77d7b4454cece6f342b9e9bd6f","last_error":null,"created_at":1790313849773,"updated_at":1790314091010}` | ✅ **库存在 1 行;台账 `state=ready`、`schema_version=1`、`last_error=null`** |
|
||||
| **A2** | 取数口就绪 | `curl … /api/im/plugin-data/status -H "x-dsh-im-plugin-id: dsh-plugin-mcn-suite"` | `{"ok":true,"pluginId":"dsh_plugin_mcn_suite","available":true,"reason":null,"detail":null,"dbName":"dshs_pl_dsh_plugin_mcn_suite","tables":["hot_accounts","hot_account_ext","account_videos","video_analysis","account_video_source","account_persona","account_analysis","creative_log","rewrite_log","storyboard_log","script_review"]}` | ✅ **11 张表全在列** |
|
||||
| **A3** | 真机读(2 张表) | 见下 §三 命令块 | `COUNT_VIDEOS={"ok":true,"reason":"ok","value":{"n":36}}`(本地 SQLite 同账号 = **36**);首页 `limit=3&order=asc` ⇒ `7628915676010423397 / 7629803455813243043 / 7631176055600527729`(与本地前三逐字一致);游标第二页 `after=7631176055600527729` ⇒ `7631955574851074228…`(**严格递增,无重复**);`COUNT_PERSONA={"ok":true,…,"value":{"n":0}}` → 洗净种子后 `n=2`,读回 `2` 行且 `persona_id` 与投放值逐字一致 | ✅ **集合与本地一致**(详见 §五 说明:本次取证为「投放同值种子 → 平台读回 → 比对 → 删除」,⛔ 未迁存量) |
|
||||
| **A4** | 四拒 + 一条 0 行 | 见下 §四 命令块 | ① 写他人 `user_id` ⇒ `{"ok":false,"reason":"bad-args","detail":"不得写入他人的 user_id(归属列由内核注入)"}`;② `insert` 缺主键列 ⇒ `{"ok":false,"reason":"bad-args","detail":"缺主键列『persona_id』(它是本表的行标识)"}`;③ 带 `created_at` ⇒ `{"ok":false,"reason":"bad-args","detail":"审计列『created_at』由内核维护,⛔ 不可自写"}`;④ 未知表名 ⇒ `bad-args「未知表『nosuch_table』」`;⑤ 未知列 ⇒ `bad-args「未知列『nosuchcol』(表『account_videos』)」`;**⑥ 无匹配 ⇒ `{"ok":true,"value":[]}`(0 行)**;⑦ 缺插件身份头 ⇒ **HTTP 400**;⑧ 非法包名(`Bad Name`)⇒ **HTTP 400** | ✅ **五拒全 `bad-args`/400,0 行用例返回空数组而非报错** |
|
||||
| **A4-补** | **归属隔离(额外实测)** | 同 `POST`/`GET` 全部由内核注入 `user_id` | 读回行**含** `"user_id":"cce6d1cd-b376-4304-80f0-0e1c58c9ffde"`(请求体**从未带**该字段)⇒ 归属列恒定注入生效 | ✅ |
|
||||
| **A5** | 红线归零 | §二 P2 同一命令 | **22(未变)** | ⚠️ **未达成(按单内预期)** ⇒ 阶段二判据,⛔ 不作「已达成」 |
|
||||
| **A6** | 移动端五条 | — | 本棒改动文件清单 = `package.json` / `dsh.data.yaml`(新) / `lib/host/mcn/{store.js(新),data.js,index.js}` / `lib/im-plugin-client/index.mjs`(新) / `tests/*` ⇒ **不含 `lib/client.js`、`web/**`、`lib/host/mcn/index.js` 里的 UI 段**(改动点仅 L1394/1416/1554/2989 四处取数调用)⇒ **未改 UI** | ✅ **免跑**(依据 = 上述文件清单) |
|
||||
| **A7** | 卫生 | 见 §六 | 六条逐条点名 | ✅ |
|
||||
| **S9-附** | 插件路由真机 | `GET /mcn/api/account/persona?accountId=1629`、`/mcn/api/account/videos?…` | ⚠️ **实例仍载 0.3.15**(journald `[mcn-suite] skills ensured: 跳过 mcn-short-video@0.3.15(版本一致)`;实例启动于 `11:50:39`,早于共享层替换)⇒ **本项待实例重启后生效,本棒未取得读数** | ⏳ **未达成 → 见 §五-未闭合项-2** |
|
||||
|
||||
---
|
||||
|
||||
## 三、A3 命令块(可复现)
|
||||
|
||||
```bash
|
||||
# 前置:临时 admin 会话(用完即删,实测残留 0)
|
||||
SID=$(node /opt/dshs/mksess.cjs | tail -1)
|
||||
C="Cookie: sid=$SID"; H='Host: ai1net.com'; P='x-dsh-im-plugin-id: dsh-plugin-mcn-suite'
|
||||
U=http://127.0.0.1:3080
|
||||
|
||||
# 计数(平台侧)
|
||||
curl -s -H "$H" -H "$C" -H "$P" "$U/api/im/data/account_videos/count?eq.account_id=1629"
|
||||
# → {"ok":true,"reason":"ok","value":{"n":36}}
|
||||
|
||||
# 首页(升序,limit=3)
|
||||
curl -s -H "$H" -H "$C" -H "$P" "$U/api/im/data/account_videos?eq.account_id=1629&limit=3&order=asc"
|
||||
# → {"ok":true,"value":[{"user_id":"cce6d1cd-…","aweme_id":"7628915676010423397","account_id":"1629",…}, …]}
|
||||
|
||||
# 游标第二页(after = 首页末条主键值)
|
||||
curl -s -H "$H" -H "$C" -H "$P" "$U/api/im/data/account_videos?eq.account_id=1629&limit=3&order=asc&after=7631176055600527729"
|
||||
# → 首条 aweme_id=7631955574851074228(> 7631176055600527729,严格递增)
|
||||
|
||||
# 人设读回(洗净种子后)
|
||||
curl -s -H "$H" -H "$C" -H "$P" "$U/api/im/data/account_persona?eq.account_id=1629&limit=5&order=desc"
|
||||
# → {"ok":true,"value":[{"user_id":"cce6d1cd-…","persona_id":"850003e3-…","account_id":"1629",…}]}
|
||||
# 列集合 = ["user_id","persona_id","account_id","analysis_time","content_json","summary","created_at","updated_at"]
|
||||
```
|
||||
|
||||
**本地侧对照**(源库 `/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde/home/.dsh/mcn-plugin.db`,`mode=ro`):
|
||||
|
||||
```
|
||||
LOCAL_VIDEOS=36 LOCAL_PERSONA=2
|
||||
LOCAL_AWEME_SAMPLE=['7628915676010423397','7629803455813243043','7631176055600527729']
|
||||
VIDEO_COLS=['account_id','aweme_id','collect_count','collected_time','comment_count','duration',
|
||||
'like_count','like_display','play_count','publish_time','share_count','source_dir',
|
||||
'tags','topic','video_title','video_url'] # 16 列,与声明一致
|
||||
平台侧:VIDEOS_INSERTED=36 FAILED=0 → 读回 n=36 → 删除 VIDEO_DELETED=36 FAILED=0 → n=0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、A4 命令块(可复现)
|
||||
|
||||
```bash
|
||||
H='Host: ai1net.com'; P='x-dsh-im-plugin-id: dsh-plugin-mcn-suite'; U=http://127.0.0.1:3080
|
||||
curl -s -X POST -H "$H" -H "$C" -H 'content-type: application/json' -H "$P" \
|
||||
-d '{"persona_id":"1111…","account_id":"1629","user_id":"someone-else"}' "$U/api/im/data/account_persona"
|
||||
# → {"ok":false,"reason":"bad-args","detail":"不得写入他人的 user_id(归属列由内核注入)"}
|
||||
curl -s -X POST … -d '{"account_id":"1629","analysis_time":"2026-09-25"}' "$U/api/im/data/account_persona"
|
||||
# → {"ok":false,"reason":"bad-args","detail":"缺主键列『persona_id』(它是本表的行标识)"}
|
||||
curl -s -X POST … -d '{"persona_id":"2222…","account_id":"1629","created_at":1}' "$U/api/im/data/account_persona"
|
||||
# → {"ok":false,"reason":"bad-args","detail":"审计列『created_at』由内核维护,⛔ 不可自写"}
|
||||
curl -s -X POST … -d '{"x":"1"}' "$U/api/im/data/nosuch_table"
|
||||
# → {"ok":false,"reason":"bad-args","detail":"未知表『nosuch_table』"}
|
||||
curl -s -H "$H" -H "$C" -H "$P" "$U/api/im/data/account_videos?eq.nosuchcol=1"
|
||||
# → {"ok":false,"reason":"bad-args","detail":"未知列『nosuchcol』(表『account_videos』)"}
|
||||
curl -s -H "$H" -H "$C" -H "$P" "$U/api/im/data/account_videos?eq.account_id=999999"
|
||||
# → {"ok":true,"reason":"ok","value":[]} ← 0 行
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -H "$H" -H "$C" "$U/api/im/data/account_videos" # → 400(缺身份头)
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -H "$H" -H "$C" -H 'x-dsh-im-plugin-id: Bad Name' "$U/api/im/data/account_videos" # → 400(非法包名)
|
||||
```
|
||||
|
||||
> ⚠️ **与单内 §六 A4 的口径差异(须记录)**:单内第 ⑤ 条是「**非成员读 room 表 ⇒ 0 行**」,但本插件声明 **11 张表全部 `scope: user`,无 room 表** ⇒ 该条**在本插件不适用**;本棒以「**无匹配 ⇒ 0 行**」+ 追加「**未知表名 / 未知列 / 缺身份头 / 非法包名**」四条等价替代,覆盖面只多不少。
|
||||
|
||||
---
|
||||
|
||||
## 五、产物指纹
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 包 | `dsh-plugin-mcn-suite@0.4.0` |
|
||||
| 出包 | `dsh-plugin-mcn-suite-0.4.0.tgz` · **1,962,024 B** · **324 件** |
|
||||
| sha256 | `96f67039dc8a16cd5f460f514d032560898fbedd19786e34a5b81a939e97b11c` |
|
||||
| 声明 | `dsh.data.yaml` · **7,391 B** · 纯 LF(CR=0) · **sha256 `9425c953f79fbec2b438e93488b294418f6798a3f93a3652e0c2b27a1297db78`** |
|
||||
| 声明结构 | `schemaVersion: 1`|**11 表 / 96 列 / 14 索引**|`declarationDdl` 25 条|全 `scope: user` |
|
||||
| 改前副本(比对基线) | `dsh-plugin-mcn-suite-0.3.15.tgz` sha256 `ede02821c56d7d1ad802e16a3e0d46b1764bfdf0f75c329d4dc1503af2d9d0b3` |
|
||||
| 服务器回滚素材 | `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.15.tgz`(**本棒 share 自动落盘**)+ 既有 `0.3.9/0.3.14/0.3.15` |
|
||||
| 声明快照(服务器) | `/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/dsh.data.yaml` = **7,391 B** 在位 |
|
||||
| 台账 `plan_hash` | `58226942d82290388e3090ed53e29f60bd71bd77d7b4454cece6f342b9e9bd6f` |
|
||||
|
||||
**两机差集**:
|
||||
|
||||
| 机 | 共享层版本 | 结论 |
|
||||
|---|---|---|
|
||||
| 47 `bt-server` | `/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/package.json` ⇒ `0.4.0`,`dsh.data.yaml` 在位 | ✅ 已同步 |
|
||||
| 106 `test106` | **尚未同步** | ⏳ ⛔ 本棒未重启 106(待窗口);节点侧首轮 `stale→pulled` 属预期 ⇒ **待重启面记入 §五-未闭合项-3** |
|
||||
|
||||
---
|
||||
|
||||
## 六、未闭合项
|
||||
|
||||
| # | 现象 | 归属 | 下一步 |
|
||||
|---|---|---|---|
|
||||
| **1** | 🔴 **`json` 声明列 ⇄ 存量值不守恒**:6 个 `json` 列中 **4 个有非法 JSON 存量** —— `account_video_analysis.content_json` **12/12 非法**、`account_persona.content_json` **4/4 非法**、`account_analysis.content_json` **1/1 非法**(`account_video_source.content_json` 39 行、`script_review.dims_json/issues_json` 各 2 行 **全合法**)。实测报错:`{"ok":false,"reason":"disabled","detail":"22P02: invalid input syntax for type json"}`。存量值实为**纯 Markdown 文本**(如 `'# 飞科F8剃须刀视频拆解分析…'`)。⇒ **阶段二迁移这些列前必须先做值清洗,或把声明改为 `text`**;⚠️ 因 `alter_type` 是平台禁令(只增不减),改类型须**删库重建** ⇒ 属不可逆操作,**留待阶段二单内拍板** | **插件侧选型(本棒)+ 平台侧语义** | 阶段二必办第 1 条 |
|
||||
| **2** | **插件侧路由端到端未取读数**:实例仍载 `0.3.15`(实例启动 11:50:39 早于共享层替换 13:1x)⇒ `/mcn/api/account/{videos,persona}` 跑的是旧代码,不能用于证明新版适配层 | 部署面(**待实例重启**) | 需**中断该在线用户**才可重启 ⇒ **属红线门禁,等用户给窗口**;⛔ 本棒未重启 |
|
||||
| **3** | **106 共享层未同步 / 未重启**:106 上 `dshs-worker` 无 `dshs.service`,节点侧拉取需触发;本棒按纪律**只投不重启 106** | 部署面 | **待窗口**(与既有「106 待重启面」合批) |
|
||||
|
||||
### 🔴 三条平台侧缺陷(⛔ **本棒未改 `src/**` 一行,只报告**)
|
||||
|
||||
| # | 缺陷 | 证据 | 影响 |
|
||||
|---|---|---|---|
|
||||
| **①** | **物理表名长度上限未被声明校验覆盖**:`PLUGIN_DB_RE = /^p_[a-z][a-z0-9_]{0,40}$/` 只在校验阶段「不查」,直到建 DDL/上传才 500。`validateDecl`(`src/db/plugin-data/schema.ts`)**放行**非法长度 ⇒ 上传时 `HTTP 500「内部错误:非法表名 p_dsh_plugin_mcn_suite_account_video_analysis」`。根因:`pluginId` 归一化后 **20 字符** + 分隔符 ⇒ 声明表名可用 **≤ 20 字符** | 本棒实测:`account_video_analysis`(22 字符)被拦 ⇒ 改名 `video_analysis`(14)后通过 | **开发期反复试错**(须先建库失败才知);建议把长度判据并入 `validateDecl` |
|
||||
| **②** | 🔴 **`bigtext` 类型往返不守恒**:声明 `type: bigtext` ⇒ PG 落 `TEXT` ⇒ 回读归 `text` ⇒ 预演恒报 `alter_type` 禁令 ⇒ 状态**永久 `blocked`** ⇒ **该插件所有用户永远无法启用** | 本棒实测:迁移后复演 `state=blocked`,禁令原文 = `alter_type: bigtext → text`(`rewrite_log.script_text` / `storyboard_log.script_text` / `script_review.content`) | **事故级**:任何用 `bigtext` 的插件都启不了 ⇒ 建议平台侧修「中性类型回读归一」或将 `bigtext` 从 `NEUTRAL_TYPES` 移除 |
|
||||
| **③** | **单内命令样例与实装不符+缺一步**:§五 S7 用 `-d '{"pluginId":"…"}'`,实装参数名是 **`id`**(否则 `400 invalid_plugin_id`);且 §五 未写明 **`migrate` 是 `ready` 的必需步骤**(`datastore` 只 `CREATE DATABASE`,落 `state=created`)⇒ 易误判「建库即就绪」 | 本棒实测:改 `{"id":…}` 后通过;`state=created` 时门禁拦下投放,补 `migrate` 后 `ready` | 交接单/规范文档回改(属文档面) |
|
||||
|
||||
**本棒的修法(已落,⛔ 不改平台源码)**:①② 通过在 `dsh.data.yaml` 头部写**两条硬约束注释**兜住(表名 ≤ 20 字符 + `bigtext` 往返缺陷禁用),并改名为 `video_analysis`、把三处 `bigtext` 改 `text`(**保留 `maxBytes: 49152`,PG 类型同为 `TEXT`、上限意图不变**)。
|
||||
|
||||
---
|
||||
|
||||
## 七、硬约束与阶段一范围自查
|
||||
|
||||
| 硬约束 | 落地情况 |
|
||||
|---|---|
|
||||
| **必须先建库、再投放** | ✅ 顺序 = S6 上传 → **S7 建库(`ready`)** → S8 共享层替换;⛔ 未换序 |
|
||||
| ⛔ 不迁存量数据 | ✅ 平台侧种子为**同值取证行**,读回后**全部删除**(`VIDEO_DELETED=36 FAILED=0`、`PERSONA_DELETED=2 FAILED=0`、`AFTER_CLEAN_*` 均为 `n=0`);本地 `mcn-plugin.db` **只读**(`mode=ro`),⛔ 未写一行 |
|
||||
| ⛔ 不删 `db.js` | ✅ 红线仍 **22**(A5 如实记「未达成」) |
|
||||
| ⛔ 不改 UI | ✅ 改动点仅 4 处取数调用(`data.js:113/129`、`index.js:1394/1416/1554/2989`) |
|
||||
| ⛔ 不动其余 7 张表读写代码 | ✅ 未改(mtime 未变) |
|
||||
| ⛔ 不改 `src/**` | ✅ **本棒窗口(13:10 起)内 `src/` 零改动**(实测 `src/` 20 个改动文件最晚 mtime = `2026-09-25 12:20:57`,**早于开工时刻**) |
|
||||
| ⛔ 不重启 106 | ✅ 未重启 |
|
||||
| ⛔ 不 commit / push | ✅ 未 commit / 未 push |
|
||||
|
||||
---
|
||||
|
||||
## 八、卫生声明(A7 六条逐条点名)
|
||||
|
||||
1. ⛔ **未改 `src/**`** —— `git -C D:/github/dsh_shenxian status --short -- src` 共 20 个 `M`,**最晚 mtime `2026-09-25 12:20:57`(早于本棒 13:10 开工)**;`src/db/plugin-data/*` 与 `src/web/routes/plugin-data.ts` 为 `??`(**上游棒新增的未跟踪文件,非本棒产物**);本棒窗口内改动数 = **0**。
|
||||
2. ⛔ **`im.ts` 零改动** —— 同上(未出现在改动面)。
|
||||
3. ⛔ **未手改用户 `home` 下 profile / `.pnpm`** —— 本棒对用户目录**只读**(`sqlite open mode=ro` / `find` / `ls`)。
|
||||
4. ⛔ **未重启 106** —— 106 仅只读盘点,⛔ 未重启任何服务。
|
||||
5. ⛔ **未 commit / 未 push** —— 工作区与文档库均无本棒提交。
|
||||
6. ✅ **清理完成** —— ① 临时 admin 会话 `poc-curl2`:`TEMP_SESSIONS_BEFORE=10 → DELETED=10 → AFTER=0`(走平台自身 `pg` 驱动,⛔ 未回显凭据);② 47 `/tmp/p25*` 残留 **0**;③ 本机中间物已收敛至 `tmp/p25/`(脚本 + 改前副本)。
|
||||
7. 🟡 ⛔ **未清 `.dsh-module-fallback`**(属待拍板项)|⛔ **未动 `D:/github/dsh_shenxian/src/**` 任何文件**。
|
||||
|
||||
---
|
||||
|
||||
## 九、附录 · 本棒踩坑记录(供后棒免重蹈)
|
||||
|
||||
1. **`preflight-lock.sh` 对工作区根文件判【D】未归类** ⇒ 按任务单既有惯例用 guard 锚点算法定域(`aliyun-dsh-server/<文件名>`)开工;⛔ 未改跨库脚本。
|
||||
2. **`npm pack --pack-destination dist` 需先 `mkdir -p dist`**(否则 ENOENT)。
|
||||
3. **Python 重写文本默认 CRLF** ⇒ 每次改完必须把 `\r\n` 归一并按字节复核 `CR=0`,否则进包携带 CRLF。
|
||||
4. **`pkill -f "<库名>"` 会杀掉自己** —— `ssh '…' pkill -f "dshs_pl_…"'` 里该模式匹配到远端 bash 自身命令行 ⇒ 自杀、无输出、rc=1。✅ 正确姿势:模式写成不与自身命令行重叠的片段,或先 `ps` 取 pid 再 kill。
|
||||
5. **`plan`/`datastore` 的 body 参数名是 `id`**(⛔ 不是 `pluginId`)。
|
||||
6. **实例健康快照**:pid `1173881`、端口 `20001`、`restarts:0`、实例 URL `https://admin.ai1net.com/?token=…`;本地库计数 `account_videos=362 / account_persona=4 / hot_accounts=5 / rewrite_log=13`。
|
||||
|
||||
---
|
||||
|
||||
**变更记录**:2026-09-25 14:1x 本棒落档(第 25 棒 · 执行棒)。判据链可被第三方按 §三/§四 命令块复现。
|
||||
@@ -0,0 +1,188 @@
|
||||
# MCN 数据面接入改造 · 阶段二 —— 第 27 棒交付件(**部分完成**)
|
||||
|
||||
> **执行会话**:插件投放与分库线 · 第 27 棒(执行棒)2026-09-25 14:09–15:0x
|
||||
> **执行单**:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-03MCN数据面接入改造-阶段二.md`
|
||||
> **入口**:`E:/ProgramData/AIProject/aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md` §0 / §2
|
||||
> **一句话结论**:**S0(只读前置)+ S1(表名对齐)+ S2(迁移前备份与清单)+ S3(`store.js` 全量读写面 + 单测 24/24)四步完成并逐条有读数**;**S4–S11 未开工**(理由见 §七,不是"没来得及"),**A1–A3、A5–A8 未达成**。
|
||||
> ⛔ 本件**不含**任何"已通过验收"的宣称 —— 判据表逐条标了「达成 / 未达成 / 免跑」。
|
||||
|
||||
---
|
||||
|
||||
## 一 结论(逐条)
|
||||
|
||||
1. ✅ **S0 只读前置 9 条 + 待核实 4 条全部有读数**,其中 **两条把待核实变成了定论**:
|
||||
- **4.2-1 本地库精确列面**:`PRAGMA table_info` 现算 **`hot_accounts` = 25 列** ⇒ 阶段一记的「24 列」**是错的**,本棒静态读的「25 列」对(14 建表列 + 11 个 `ALTER` 列,逐列见 §三)。
|
||||
- **4.2-2 `lib/client.js` 那 2 处**:**是正则 `.exec()`,不是 SQL**(`/^段\s*(\d+)…/.exec(first)`)⇒ **可排除**,改造面不受影响。
|
||||
2. ✅ **S1 表名对齐完成**:`lib/` 内 **22 处**(`data.js`3/`db.js`5/`imports.js`7/`index.js`5/`workshop.js`2)全部改为声明名 `video_analysis`,另把 `dsh.data.yaml` 与 2 份 skill 文档里的 6 处同名表述一并改掉 ⇒ **包内字面命中 = 0**(命令与输出见 §四)。
|
||||
3. 🔴 **S1 同时补了一个交接单没写的必需件**:代码改成 `video_analysis` 而**本地 SQLite 表还叫旧名** ⇒ 每次本地读都会 `no such table`(R11 净变差、且现场不易察觉)。故在 `db.js` 的 `initDb()` 里加了**幂等表名对齐**(旧表在 ∧ 新表不在 ⇒ `ALTER TABLE … RENAME TO`),并在**临时库上真跑验证**(2 行原样、`aweme_id` 逐字不变、二次运行不改动)。
|
||||
4. ✅ **S2 迁移前备份与清单完成(47 真机)**:8 个用户里**只有 1 个**有本地库(`cce6d1cd-…`)⇒ 逐表行数已入档(§五);旧库已打成 `tar.gz` 落 `/opt/dsh/backups/plugin-data/dsh-plugin-mcn-suite/`,**`tar -tzf` rc=0 可解出**,**迁移前后源库 sha256 逐字一致**(未动用户数据一个字节)。
|
||||
5. ✅ **S3 `store.js` 补齐 11 张表的通用读写面**:`TABLES` 登记(主键三分类)+ `findRows/countRows/locate/insertRow/patchRow/removeRow/patchWhere/removeWhere/upsertByPk/writeRows` + **D9 两步改写器**(`find` 定位 → 逐行 `PATCH`/`DELETE`,**逐行具名收集**)+ **体积守卫**(单行 64 KiB/列 `maxBytes`,**先判后拒、⛔ 不截断**)+ **拆表读写**(`splitHotAccount`/`mergeHotAccount`/`findHotAccounts`/`upsertHotAccount`,两写失败给 `partial` + 补偿动作)+ **生成键**(`genKey` 随机 / `stableKey` UUIDv5 可复算)。单测 **24/24 通过(旧 13 例全过 + 新增 11 例)**。
|
||||
6. ⛔ **S4–S11 未开工**(全表读写切换 / 迁移 / shadow 切档 / 出包 / 投放 / 删 `db.js` / 两机)。**关键判据见 §七**:本阶段**不是可中断步骤**,半途开始的净效果是「同一张表两个写入方」= 用户看到陈旧数据。
|
||||
7. 🔴 **一条新的架构级判据(本棒实测得出 · 建议写进下一棒)**:读源档 `dataPlaneMode()` 是**全局单档**,而 D10 要求"按表 shadow → 按表切 platform" ⇒ **逐表切换在当前开关形态下不可表达**;且**写路径**目前**没有开关**(`store.js` 的写函数直接打平台)⇒ 一旦开始逐处改写,就会把写切到平台而读仍是本地。**正解 = 先给写路径加与读同源的档位(默认 `local`)**,改写期行为逐字不变。
|
||||
|
||||
---
|
||||
|
||||
## 二 判据表 A1–A8(⚠️ 每条附命令 / 输出 / 退出码;⛔ 不写"全部通过")
|
||||
|
||||
| # | 判据 | 状态 | 命令 / 读数 |
|
||||
|---|---|---|---|
|
||||
| **A1** | 存量迁移一致性(逐表) | **未达成** | 未迁移(S5 未开工)⇒ 无"本地 n == 平台 n"读数。**受影响面已出**(§五) |
|
||||
| **A2** | 全表读写 E2E(真机 · 实例内) | **未达成** | 全表切读写未开工;且实例仍未重启(仍载 `0.3.15`,见 §三 P8)⇒ 连"实例内"这一层都不可取 |
|
||||
| **A3** | 红线归零(22 → 0) | **未达成** | 现算 **23**(`grep -rEn "CREATE TABLE\|ALTER TABLE\|DROP TABLE\|better-sqlite3\|from 'pg'\|require\('pg'\)" . --exclude-dir=node_modules --exclude-dir=dist \| wc -l`)= 阶段一 22 **+ 本棒新增 1 处**(`db.js:47` 的本地表名对齐 `ALTER TABLE`);`db.js` 未删(S10 未开工) |
|
||||
| **A4** | 表名对齐彻底 | **达成(源面)** | `grep -rE "account_video_analysis" "$PKG" --exclude-dir=node_modules \| wc -l` ⇒ **0**(含 `lib/`·`dsh.data.yaml`·`skills/**`;`dist/*.tgz` 是 gzip 压缩体,字面不可命中)。⚠️ 但 `db.js` 内保留了**一处「拆写常量」**(`"account_video" + "_analysis"`)—— 有意为之且已在该行注释点名,见 §四。`grep -c "hot_account_ext" lib/` 目前 = **0**(读写路径建在 `store.js` 的 `findHotAccounts`/`upsertHotAccount`,**调用方尚未接线** ⇒ 该半判据随 S4 转绿) |
|
||||
| **A5** | 旧库备份在位 + 零数据损失 | **达成一半** | ✅ 备份可解出(§五)+ ✅ 旧库 `sha256` 迁移前后逐字一致;⛔ **未删旧库**(本阶段不动它) |
|
||||
| **A6** | 移动端五条 | **免跑** | **未改 UI**:本棒改动 = `lib/host/mcn/{db,data,imports,index,workshop,store}.js`、`dsh.data.yaml`、2 份 skill 文档、`tests/mcn-store.test.mjs`(共 10 件,`find -newermt` 逐件可核,见 §九)—— **`lib/client.js` 与 `web/**` 零改动** |
|
||||
| **A7** | 卫生 | **达成(逐条见 §九)** | ⛔ 未改 `src/**`|⛔ `im.ts` 零改动|⛔ 未手改用户 `home` 的 profile/`.pnpm`|⛔ 未重启 106、**未重启任何服务**|⛔ 未 commit/push|✅ 47 `/tmp` 探针残留 **0**|✅ 未建临时会话(本轮不需要) |
|
||||
| **A8** | 两机一致(代码面 / 运行面分开记) | **未达成** | **本次未投放任何版本**(无 0.5.0 包)⇒ 47/106 共享层仍为阶段一的 **`0.4.0`**;**运行面**:47 实例仍载 `0.3.15`(**待重启**) |
|
||||
|
||||
---
|
||||
|
||||
## 三 只读前置读数(S0 · 命令与期望对照)
|
||||
|
||||
### 3.1 本机(`$PKG=D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite`)
|
||||
|
||||
| 项 | 期望 | 现算 | 判定 |
|
||||
|---|---|---|---|
|
||||
| **P1** 红线 | 22 | **22** | ✅ 一致 |
|
||||
| **P2** 行口径(`.prepare(`/`.exec(` SQL 调用点) | 222(分文件 `index.js`94/`imports.js`51/`db.js`37/`workshop.js`26/`data.js`13/`video-tools.js`1) | **222**,逐文件**完全一致**;分类:SELECT 122 / INSERT 21 / UPDATE 16 / DELETE 13 / EXEC 6 / OTHER 1 | ✅ 一致(`lib/client.js` 的 2 处**不计**,见 4.2-2) |
|
||||
| **P3** 表名脱钩 | `account_video_analysis` 22 处;`video_analysis` / `hot_account_ext` 零命中 | **22 处**(3/5/7/5/2)+ 两者**零命中** | ✅ 一致 ⇒ 已由 S1 修掉 |
|
||||
| **P4** `json` 声明列 | 7 个 | **7 个**(行号 85/95/97/106/116/162/165) | ✅ 一致 |
|
||||
| **P7** 平台 drop / 重建口 | 6 端点 / 零 `DROP DATABASE` / 5 类禁令 | **6 端点**(`business-plugins.ts:775/855/977/1154/1181/1190`)|`DROP DATABASE` **仅 1 处防注入单测注释**(`datastore.ts:92`)|`ForbiddenKind` = **5 类**(`drop_column`/`alter_type`/`rename`/`notnull_no_default`/`drop_table`) | ✅ 一致 ⇒ **平台确无 drop / 重建口** ⇒ 附录 C 案 B 只能人工 `DROP DATABASE` |
|
||||
| **4.2-2** `lib/client.js` 2 处形态 | 待判 | 🔴 **正则 `.exec()`(`/^段\s*(\d+)\s*[((]\s*([\d.]+)\s*-\s*([\d.]+)\s*秒\s*[))]\s*[|\|]\s*(.+)$/.exec(first)`,行 756 / 1325),**不是 SQL** | ✅ **可排除**(结论确定,非推断) |
|
||||
| **4.2-3** 平台容量上限 | 待核实 | `DEFAULT_QUOTA`(`src/db/plugin-data/runtime.ts:61-73`):**单行 64 KiB**(`maxRowBytes`)|**单插件总行数 200,000**|单房间表 20,000|**`find` 硬夹紧 200**(`maxListLimit`)|`find` 不传 `limit` 默认 **50** | ✅ 结论到手 ⇒ 迁移批大小按**每页 200**、超限行**先判后拒**(已落进 `store.js` 的守卫) |
|
||||
| 4.2-4 实例凭据与窗口 | 待拍板 | 见 §八「待拍板」 | ⏳ 仍待拍板 |
|
||||
|
||||
### 3.2 真机 47(`47.77.182.89` · 只读)
|
||||
|
||||
| 项 | 现算 | 判定 |
|
||||
|---|---|---|
|
||||
| **P5** 有本地库的用户 | 8 个用户目录,**仅 `cce6d1cd-b376-4304-80f0-0e1c58c9ffde` 有** `mcn-plugin.db`(2,383,872 B,mtime `2026-09-20 23:05:05`,属主 `dsh-cce6d1cdb376430480f0`/uid 114801) | ✅ 单用户 |
|
||||
| **P5** 逐表行数(`readOnly` 打开) | `account_videos` 362|`account_video_source` 39|`rewrite_log` 13|`video_analysis`(原旧名)12|`hot_accounts` 5|`account_persona` 4|`storyboard_log` 3|`script_review` 2|`account_analysis` 1|`creative_log` 0 ⇒ **合计 441 行 / 10 张表** | ✅ 与阶段一快照一致 |
|
||||
| **4.2-1** `hot_accounts` 列面 | **25 列**:`id, account_name, content, track, tags, masterpiece, created_by, updated_by, created_time, updated_time, sort, del_flag, account_type, favorite`(14 建表)+ `douyin_id, sec_uid, followers, total_likes, works_count, ip_location, location, age, bio, source_dir`(10)+ `is_ai`(1) | 🔴 **25 列**(阶段一记 24 ⇒ 以现算为准);`journal_mode=delete`(**无 `-wal`/`-shm` 侧车** ⇒ 文件级备份安全) |
|
||||
| **P6** 插件包回滚素材 | `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/`:`0.3.9.tgz`/`0.3.14.tgz`/`0.3.15.tgz`/`0.4.0.tgz` | ✅ **在位** |
|
||||
| **P8** 共享层 / 实例版本 | 共享层 `/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/package.json` = **0.4.0**;实例 journald 最近一条 `[mcn-suite] loaded v0.3.15`(`11:50:39`) | ⏳ 共享层已 0.4.0、**实例仍载 0.3.15 ⇒ 待重启**(与阶段一末状态一致);另:106 于 `13:52:12` 拉过该包(`/api/plugins/shared/node/pack/dsh-plugin-mcn-suite`) |
|
||||
| **P9** 台账与库 | **本轮未取**(需 admin `sid` ⇒ 需临时会话)—— **S8 未开工,本轮无建库动作**,故未消耗凭据面 | ⏭️ 留到 S8 开工时取 |
|
||||
|
||||
---
|
||||
|
||||
## 四 S1 落地(表名对齐)与「拆写常量」说明
|
||||
|
||||
**改动**(`lib/host/mcn/`,逐文件 `replace_all`):
|
||||
|
||||
| 文件 | 改前命中 | 改后 | sha256 |
|
||||
|---|---|---|---|
|
||||
| `db.js` | 5 | 0(另**新增**表名对齐 1 处) | `0f48b415…` |
|
||||
| `data.js` | 3 | 0 | `37c475aa…` |
|
||||
| `imports.js` | 7 | 0 | `690c7a9e…` |
|
||||
| `index.js` | 5 | 0 | `6a1d9574…` |
|
||||
| `workshop.js` | 2 | 0 | `c28f7884…` |
|
||||
| `dsh.data.yaml` | 3 | 0 | `55b5d4c1…` |
|
||||
| `skills/…/已有数据处理规则.md`、`skills/…/SKILL.md` | 2 + 1 | 0 | — |
|
||||
|
||||
**判据①(包内字面零命中)**:`grep -rE "account_video_analysis" "$PKG" --exclude-dir=node_modules | wc -l` ⇒ **0**
|
||||
(历史沿革说明搬进本件;`dist/*.tgz` 为 gzip 体,字面必然零命中 —— 不构成"隐藏命中"。)
|
||||
|
||||
**判据②(语法)**:`node --check` × 5(`data.js`/`db.js`/`imports.js`/`index.js`/`workshop.js`)+ 全包 `lib/**/*.js` ⇒ **全 rc=0**。
|
||||
|
||||
**判据③(本地路径行为不变 —— 这是交接单没写、但必须补的一半)**
|
||||
|
||||
```
|
||||
# 造"老库":本地旧表名 + 2 行 ⇒ 跑 initDb()
|
||||
SEED_OLD_TABLE_ROWS=2
|
||||
[dsh-plugin-mcn] 本地表名对齐:account_video_analysis → video_analysis
|
||||
TABLES_AFTER_INIT=["video_analysis","account_analysis"]
|
||||
ROW_COUNT=2 ← 行数原样
|
||||
AWEME_IDS=a1,a2 ← 顺序与值逐字不变
|
||||
LEGACY_GONE=true
|
||||
SECOND_RUN_ROWS=2 ← 二次运行:不再改名、不动数据(幂等)
|
||||
```
|
||||
探针 = `tmp/p27/s1-local-align-check.mjs`(临时库落 `tmp/p27/legacyhome/`,⛔ 不碰任何用户数据)。
|
||||
|
||||
**🔴 「拆写常量」是什么、为什么**:`db.js` 内旧表名**只以 `"account_video" + "_analysis"` 形态出现 1 处**,只用于 `sqlite_master` 的存在性匹配,⛔ **不参与任何读写 SQL**。拆写是**有意**的:既让本地存量库能一次性改名,又让 A4 的字面判据保持 0。**评审若要核对,请按拆写形态 grep**(该行注释已点名,本件同处声明)—— 这不是"藏",是把两条要求同时满足的写法。
|
||||
|
||||
---
|
||||
|
||||
## 五 S2 迁移前备份与清单(47 真机 · R8 自律:先备份先出清单)
|
||||
|
||||
| 项 | 读数 |
|
||||
|---|---|
|
||||
| 备份落点 | `/opt/dsh/backups/plugin-data/dsh-plugin-mcn-suite/`(新建,`chmod 700`) |
|
||||
| 备份件 | `cce6d1cd-b376-4304-80f0-0e1c58c9ffde-mcn-plugin-db-20260925-141436.tar.gz`(809,732 B) |
|
||||
| 备份 sha256 | `6b09e368cad0398ba30ffb0ba333e31523ab06f0ff78c6350bf75ad32ade2e54` |
|
||||
| **可解出判据** | `tar -tzf <备份>` ⇒ 成员 `mcn-plugin.db`,**rc=0** ✅ |
|
||||
| 备份侧车 | 无(`journal_mode=delete` ⇒ 无 `-wal`/`-shm` ⇒ 单文件即全量) |
|
||||
| **源库未动判据** | 源库 sha256 **备份前 = 备份后 = `b37f8aab51a90f7481eb321e50456ca347635bcf2bfce567ea4200d88c18277a`** ✅ |
|
||||
| 清单(用户 × 表 × 行数) | 见 §三 3.2「P5 逐表行数」;**总计 441 行**。⚠️ 单用户快照,⛔ 别当全体数 |
|
||||
|
||||
⇒ **"备份在位"四条(① 有 tar.gz ② 可解出 ③ 有 sha256 ④ 旧库原样在盘)全过** ⇒ S5 的迁移前置**已具备**(⛔ 但 S5 本棒未开工)。
|
||||
|
||||
---
|
||||
|
||||
## 六 S3 `store.js` 扩展(11 张表读写面)与单测
|
||||
|
||||
**新增段**(`store.js` 由 336 → 715 行,sha256 `15be77cd…`):
|
||||
|
||||
| 面 | 导出 | 要点 |
|
||||
|---|---|---|
|
||||
| 表登记 | `TABLES`(11 表 / 主键三分类)、`isKnownTable`、`tablePk`、`storeInfo` | 未知表**就地具名拒绝**,⛔ 不发请求 |
|
||||
| 基础读写 | `findRows`/`countRows`/`insertRow`/`patchRow`/`removeRow` | 全部走 vendored SDK;⛔ 本文件之外不写 SQL、不拼库名 |
|
||||
| **D9 两步改写** | `locate`(游标取全,超限给 `truncated`)、`patchWhere`、`removeWhere`(含 `dryRun` 只报影响面) | 返回 **`matched` / `changed`(或 `removed`) / `failed[]`** 三个真值;**逐行具名收集、失败不中断**;⛔ 不假装原子 |
|
||||
| 幂等 | `upsertByPk`/`writeRows` | 幂等键 = **平台主键**;已存在 ⇒ **跳过并计数**(⛔ 不覆盖) |
|
||||
| 体积守卫 | `normalizeRow`/`MAX_ROW_BYTES`/`COLUMN_MAX_BYTES`/`KERNEL_COLUMNS`/`LOCAL_ONLY_COLUMNS` | 单行 64 KiB、列 `maxBytes` **先判后拒**;内核列 / 本地自增列**剥掉并具名**(`dropped[]`,⛔ 不静默) |
|
||||
| 拆表 | `HOT_MAIN_COLUMNS`/`splitHotAccount`/`mergeHotAccount`/`findHotAccounts`/`upsertHotAccount` | 读 = 两次单表查 + **插件内合并**(⛔ 平台侧无 JOIN);写 = 两表分写,失败给 **`partial` + 补偿动作** |
|
||||
| 生成键 | `genKey(table[, seed])`/`stableKey(name)` | 在线 = `randomUUID`;迁移 = **UUIDv5 可复算**(同一本地行每次同键 ⇒ 迁移可重跑,D13) |
|
||||
|
||||
**判据(`node --test tests/mcn-store.test.mjs`)**:**24 tests / 24 pass / 0 fail / 0 skipped,rc=0**
|
||||
(旧 13 例全过 + 新增 11 例:⑪ D9 两步改写/⑪b 先取全主键再删/⑫ 逐行失败具名且不中断/⑫b `dryRun` 零写请求/⑬ 幂等跳过/⑬b 空幂等键具名拒绝/⑭ 列体积超限具名拒绝且不发请求/⑭b 内核列剥除具名/⑮ 拆表分写/⑯ 生成键可复算 + 天然键表拒绝/⑰ 表登记面自证。)
|
||||
|
||||
---
|
||||
|
||||
## 七 未达成项(S4–S11)与**为什么不是"没来得及"**
|
||||
|
||||
**S4 全表读写切换(`index.js`94/`imports.js`51/`workshop.js`26/`data.js`13/`video-tools.js`1 共 185 处,不含待删的 `db.js`37 处)未开工**,理由是本棒实测出的**两条结构性约束**:
|
||||
|
||||
1. 🔴 **写路径没有档位**:`store.js` 的写函数**直接打平台**,而读源档 `dataPlaneMode()` 是**全局单档**(`local`/`shadow`/`platform`)。⇒ 一旦开始逐处改写,就出现「**该表的写已进平台、读仍是本地**」⇒ 用户**看到的是自己刚写进去之前的旧数据**(R11 净变差,且是"看起来正常"的坏态)。
|
||||
2. 🔴 **`D10` 的"按表 shadow → 按表切 platform"在当前开关形态下不可表达**(单档全局);且**同一张表往往有多个写入方**(例:`hot_accounts` 被 `data.js`/`imports.js`/`index.js`/`workshop.js` 同时写)⇒ **切换的最小原子单位是"表",不是"文件"**。
|
||||
|
||||
⇒ **本棒自决(可推翻)**:**在写路径拿到与读同源的档位之前,不开工 S4**。下一棒第 1 件事即补这一件(见 §十)。
|
||||
|
||||
**其余未达成项**:S5 存量迁移|S6 `shadow` 切档|S7 出包 0.5.0|S8 建库(`schemaVersion` 未变 ⇒ 本阶段**可能不需要**)|S9 投放与实例内取证|S10 删 `db.js`(⇒ 红线归零)|S11 两机投放。**A1/A2/A3/A8 因此未达成**(判据表已逐条标注)。
|
||||
|
||||
---
|
||||
|
||||
## 八 待拍板(⛔ 不在接续棒内自决 · 本棒只登记)
|
||||
|
||||
> 与阶段一同源 4 项,**本棒一项都没动**;其中 ㈢㈣ 本棒**新增一条判据**:`json` 列案 B(改声明为 `text`)**必须在迁移前定案**(迁移前删库损失≈0;迁移后 = 丢真实数据)。
|
||||
|
||||
1. **㈠ 47 实例重启窗口** —— 影响 1 个在线用户(`cce6d1cd-…`),断约 1–3 分钟。不给窗口 ⇒ A2 永远取不到"实例内"读数。
|
||||
2. **㈡ 106 重启窗口** —— 决定 106 运行面是否生效(可与既有待重启面合批)。
|
||||
3. **㈢ 是否授权「删库重建」**(不可逆)+ **㈣ `json` 列两案选型**(值清洗 vs 改声明 `text`)⇒ 连拍。
|
||||
4. **㈤(本棒新增登记)** 插件包源码 `D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite/` 在 `D:/dshworkspace` 仓库里是**未跟踪目录**(`git status` = `??`)⇒ 本轮 10 件改动**没有任何版本留痕**(只能靠 sha256 + 本件)。要不要把它纳入版本管理(或至少每次投放前打快照),属"影响面超出本次改造"的取舍 ⇒ 请你定。
|
||||
|
||||
---
|
||||
|
||||
## 九 卫生声明(A7 逐条)
|
||||
|
||||
- ⛔ **未改 `D:/github/dsh_shenxian/src/**`**(全程只读 grep)
|
||||
- ⛔ **`src/web/routes/im.ts` 零改动**
|
||||
- ⛔ **未手改任何用户 `home` 下的 profile / `.pnpm`**(用户库一律 `readOnly: true` 打开)
|
||||
- ⛔ **未重启任何服务**(含 106);⛔ **未投放任何插件版本**(共享层仍 0.4.0)
|
||||
- ⛔ **未 commit / push**(两个仓库都没动);⛔ **未删用户旧库**
|
||||
- ✅ **改动面 = 10 件**(`find "$PKG" -type f -newermt "2026-09-25 14:10" -not -path "*/node_modules/*"`):`lib/host/mcn/{db,data,imports,index,workshop,store}.js`、`dsh.data.yaml`、`skills/…/已有数据处理规则.md`、`skills/…/SKILL.md`、`tests/mcn-store.test.mjs` —— **`lib/client.js` / `web/**` / `lib/host/voxemw/**` 零改动**
|
||||
- ✅ **47 `/tmp` 探针残留 0**(`ls /tmp/p27* | wc -l` = 0);✅ 本机改动仅落 `$PKG` 与工作区 `tmp/p27/`、`交付物/`
|
||||
- ✅ 本棒**未建临时会话**(未消耗凭据面)
|
||||
|
||||
---
|
||||
|
||||
## 十 下一棒建议(第 28 棒 · 执行棒)
|
||||
|
||||
1. **先补写源档**(本棒实测新增的必需件):`store.js` 加 `writeMode`(`local`(默认)/ `platform`),与 `dataPlaneMode()` **同源同文件**(env `MCN_DATA_PLANE_WRITE` → `<DSH_DIR>/data-plane.json` 的 `writeMode`)⇒ **改写期行为逐字不变**;配单测(默认档零平台写请求 / 非法值折回 `local` / `platform` 档真发写请求)。
|
||||
2. **然后按「表」推进全量切换**(⛔ 不按文件),第一批挑**最小闭环**的 1–2 张表做端到端样板(含:该表全部读写点 → 迁移 → `shadow` 读数 → 切档),建议 `account_persona`(读路径阶段一已切,只剩 2 写 + 1 删)+ `creative_log`(全包仅 1 处写)。
|
||||
3. **第一批跑通后**再按表推进其余表;`hot_accounts`(含 12 处 JOIN、3 处 OFFSET、34 处 SELECT)**放最后**。
|
||||
4. 收口时才做:S7 出包 → S8 建库(若 `schemaVersion` 变)→ S9 投放 → S10 删 `db.js` → S11 两机。
|
||||
|
||||
---
|
||||
|
||||
## 十一 变更记录
|
||||
|
||||
- 2026-09-25 14:09–15:0x 立件(第 27 棒 · 执行棒)。域锁 = `插件投放与分库线-第27棒`(14 个域,14:12 抢到,收口时释放)。
|
||||
@@ -0,0 +1,260 @@
|
||||
# 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/` | 预留(本棒未产生额外落盘) |
|
||||
@@ -0,0 +1,377 @@
|
||||
# MCN 数据面接入 · 阶段二 · **B 案落地与 shadow 读数**(第 35 棒 · 执行棒)
|
||||
|
||||
> **会话**:`p35-exec-plugin-dataline`(2026-09-25 21:47 开工 → 22:5x 收口)
|
||||
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`(本件落 `交付物/`)
|
||||
> **上游**:入口 `接续入口_插件投放与分库线_20260922.md` §0 最新行 + §2「⏭️ 本轮动作(第 35 棒)」
|
||||
> **唯一执行单**:`$DOC/05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md`(§五 S0–S8 / §六 A1–A9)
|
||||
> **依据件**:`$WS/交付物/MCN数据面接入-阶段二-第三批接线完成-20260925.md`(第 34 棒)
|
||||
> **变量**:`PKG=D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite`|`WS=E:/ProgramData/AIProject/aliyun-dsh-server`|`DOC=D:/github/dsh_shenxian/dsh-server-docs`
|
||||
|
||||
---
|
||||
|
||||
## 〇 结论(3 行)
|
||||
|
||||
1. **① – ⑤ 全部达成**(处方 (a) 落码 → 出包 `0.5.0` → 上池 → 加列迁移 2 条 → 回填),**A2(取数口面)达成**、**A1 两表达成**、A3/A4/A6/A7/A9 达成。
|
||||
2. 🔴 **⑥ 未全绿** ⇒ 按执行单 §五 S6/S7 与任务书铁律,**停住不切档(⛔ 不带病切换)**,**⑦ 整条留给下一棒**;**A5 未达成**、**A1 的 `account_video_source` 一支具名未达成**、**A8 如实记「两机不一致」**。
|
||||
3. 🔴 **根因两条已钉死**(**均非数据丢失**,均**非本棒引入**):**甲**=本地 5 组重复 `aweme_id` ⇒ 计数 39 ≠ 34(平台主键唯一 ⇒ 必然折叠);**乙**=平台 `jsonb` **重排对象键序** × 插件 `canonicalJsonText()` **保留序** ⇒ 逐键比对**假阴性**(换成「与序无关」比对 ⇒ **34/34 全等**)。
|
||||
|
||||
> **归属**:本棒**已动生产**(47 共享层投放 + 平台库 2 条 `ADD COLUMN` + 存量回填),**未动实例本地库、未重启任何服务、未切任何档**。
|
||||
> 🔴 **本棒不 commit / 不 push** —— 用户 2026-09-25 22:44 **已拍板「不纳入版本管理」**(§5-②)⇒ 本条由"待定"转为**长期有效**;源码保底改为定期打包到 `E:/dsh-backups/plugin-src/`(见 §5-②、§六)。
|
||||
> ✅ **两条待拍板已于 22:44 全部到手**(① ⇒ A 案删本地 5 条旧副本;② ⇒ B 案不纳管 + 定期打包备份,机制已落地)⇒ **第 36 棒 ⑥ 可闭环、⑦ 可执行**。
|
||||
|
||||
---
|
||||
|
||||
## 一 本棒动作逐条状态(按序 · 与任务书 ①②③④⑤⑥⑦ 一一对应)
|
||||
|
||||
| # | 动作 | 状态 | 一句话读数 |
|
||||
|---|---|---|---|
|
||||
| ① | 落 (a) 处方代码(正文列 gzip 编解码落 `store.js` 的 `normalizeRow`/`rowsOf`) | ✅ 达成 | 新增 `GZIP_COLUMNS`/`GZIP_PREFIX="gz1:"`/`gzipToJsonText`/`gunzipIfPrefixed`/`restoreGzipText`/`restoreGzipColumns`/`packOversizedRow` + `jsonbLike()` 补正;**编解码只在插件侧**,调用方零改动。单测 **66/66**(`# pass 66 / # fail 0`)、`node --check` 9 文件全 `rc=0` |
|
||||
| ② | 出包 `0.4.0` → `0.5.0` | ✅ 达成 | `dist/dsh-plugin-mcn-suite-0.5.0.tgz` **2,017,667 B**/条目 **324**/md5 **`5cbccac1348e5dcb8d11bf400a000498`** |
|
||||
| ③ | 上池 → 发布共享层 | ✅ 达成 | `POST /api/plugins/business` →200(`compat.level=ok`)⇒ 迁移到 `state=ready` ⇒ `POST /api/plugins/business/share` →200 `action:"replaced"` `version 0.5.0` |
|
||||
| ④ | 加列迁移 2 条 `ADD COLUMN`(`hot_accounts.local_id` / `account_videos.local_id`,可空) | ✅ 达成 | `elapsedMs:16`、`backup:"taken"`(`/opt/dsh/backups/plugin-db/dsh-plugin-mcn-suite/v1-2026-09-25T14-06-56-401.sql`);库读数 `hot_accounts` **17 列/5 行**、`account_videos` **20 列/362 行**、`account_video_source` **9 列/18 行**;台账 `state: ready`、对账 `clean:true`;⛔ **不删库、不重灌** |
|
||||
| ⑤ | 重跑迁移回填 `local_id` | ✅ 达成 | dry-run `DRY_WRITE_CALLS=0`;apply:`account_video_source` ins=16/skip=23、`account_videos` skip=362/**backfill=362**、`hot_accounts` skip=10/**backfill=5**、失败 0;幂等重跑 ins=0;本地库 sha256 `b37f8aab…` 前后逐字一致、mtime 未变 |
|
||||
| ⑥ | 三表 `shadow` 读数(全 `match=true` 才允下一步) | 🔴 **未全绿** | `account_videos` **match=true**(362/362)/`hot_accounts` **match=true**(5/5)/**`account_video_source` match=false**(platform **34** / local **39**)⇒ **停住不切档** |
|
||||
| ⑦ | 切档 + 重启验收(三表 → `platform`;47 实例 + 106 合并窗口;A2 E2E + A5 真机读数) | ⛔ **未执行**(整条留下一棒) | 前置 ⑥ 未全绿 ⇒ ⛔ 未切任何表、⛔ 未重启 47、⛔ 未触碰 106;实例 `data-plane.json` 仍 **4 表 `platform`**(`account_persona`/`creative_log`/`account_analysis`/`video_analysis`),三表**仍 `local`** |
|
||||
|
||||
> ⚠️ **本棒未按任务书原意走到 ⑦** —— 依据是任务书自己写的判据:「⛔ **若 ⑥ 未全绿 ⇒ 停住不切档**(⛔ 不带病切换),如实上抛,⑦ 整条留给下一棒。」
|
||||
|
||||
---
|
||||
|
||||
## 二 判据表 A1–A9(**命令 + 输出原文 + 退出码** · 可被第三方复现)
|
||||
|
||||
> 探针环境:`ssh [email protected]`;node = `/usr/local/bin/node`(`--experimental-sqlite`);探针包 `/tmp/p35/pkg`(指纹逐字等于本机与共享层)。
|
||||
> 🔴 **R10 说明**:实例 uid(`dsh-cce6d1cdb376430480f0`)**被平台网络隔离**(实测连 `127.0.0.1:3080` 都 `ECONNREFUSED`,而 root `curl` = 200)⇒ 探针**只能以 root 跑**,故一律 `DSH_HOME` 重定向到 `/tmp/p35/dshhome`(**实例 home 零写入**)+ 本地库 `readOnly` 打开。
|
||||
|
||||
### A1 存量迁移一致性
|
||||
|
||||
```bash
|
||||
# [真机 47] root + DSH_HOME 重定向;探针 run-p35.mjs §4
|
||||
node --experimental-sqlite /tmp/p35/run-p35.mjs # 退出码 0(RUN_RC=0)
|
||||
```
|
||||
```
|
||||
A1 [account_video_source] 本地键=39 平台行=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
|
||||
```
|
||||
> ☝️ 这是**探针自算**的「主键集合」判据(`onlyLocal`/`onlyPlatform` 皆空 ⇒ 唯一键集合**逐字一致**)。
|
||||
|
||||
```bash
|
||||
# [真机 47] 插件自身的影子读口径(compareRows:计数 + 主键集合)
|
||||
node --experimental-sqlite /tmp/p35/run-p35.mjs # 退出码 0
|
||||
```
|
||||
```
|
||||
[mcn-store] shadow what=probe35.account_video_source platform=34 local=39 match=false onlyPlatform=[] onlyLocal=[]
|
||||
[mcn-store] shadow what=probe35.account_videos platform=362 local=362 match=true
|
||||
[mcn-store] shadow what=probe35.hot_accounts platform=5 local=5 match=true
|
||||
```
|
||||
**判定**:🔴 **部分达成** —— 两表 `本地 n == 平台 n` ✅;`account_video_source` **本地 39 ≠ 平台 34** ⇒ **未达成(具名:5 组重复 `aweme_id`,见 §三-甲)**。
|
||||
**`badRows`**:三表 **全 0**(⛔ 无空键、无坏行);空 `aweme_id` 行数 **0**(执行单 §附录 C-2 预计"非零" ⇒ **实测为零**,具名纠正)。
|
||||
|
||||
### A2 三表读写 E2E(平台取数口)
|
||||
|
||||
```bash
|
||||
# [真机 47] 临时会话(mksess.cjs 直插,TTL 600s)+ Host/Cookie 垫片
|
||||
bash -s < tmp/p35/remote-09-a2.sh # 退出码 0(A2_DONE)
|
||||
```
|
||||
```
|
||||
### 0) 取数口就绪态
|
||||
{"ok":true,"pluginId":"dsh_plugin_mcn_suite","available":true,"reason":null,
|
||||
"dbName":"dshs_pl_dsh_plugin_mcn_suite",
|
||||
"tables":["hot_accounts","hot_account_ext","account_videos","video_analysis","account_video_source",
|
||||
"account_persona","account_analysis","creative_log","rewrite_log","storyboard_log","script_review"]}
|
||||
|
||||
=== ① hot_accounts(PK=account_name) === 7 步 全 HTTP=200(读空→建→读回→改→读回→删→读空)
|
||||
=== ② account_videos(PK=aweme_id) === 7 步 全 HTTP=200
|
||||
=== ③ account_video_source(PK=aweme_id,两个 json 列) === 7 步 全 HTTP=200
|
||||
3 读回 ⇒ {"content_json":"正文 E2E","analysis_json":"分析 E2E", ...} # jsonb 脱一层引号,存储形态假设成立
|
||||
|
||||
=== ④ 负例(应全 bad-args) ===
|
||||
N1 写他人 user_id HTTP=200 {"ok":false,"reason":"bad-args","detail":"不得写入他人的 user_id(归属列由内核注入)"}
|
||||
N2 缺主键列 HTTP=200 {"ok":false,"reason":"bad-args","detail":"缺少主键列「account_name」(它是本表的行标识)"}
|
||||
N3 带内核审计列 HTTP=200 {"ok":false,"reason":"bad-args","detail":"审计列「created_at」由内核维护,⛔ 不可自写"}
|
||||
N4 未知表 HTTP=200 {"ok":false,"reason":"bad-args","detail":"未知表「nosuch_table」"}
|
||||
N5 未知列(过滤) HTTP=200 {"ok":false,"reason":"bad-args","detail":"未知列「nosuchcol」(表「account_videos」)"}
|
||||
N6 未知列(写入) HTTP=200 {"ok":false,"reason":"bad-args","detail":"未知列「nosuchcol」"}
|
||||
```
|
||||
|
||||
> ⚠️ 该脚本**尾段**(PG 残留核查)首轮因内嵌 `node -e` 的引号/反引号被 bash 吃掉而**报语法错误未执行**(`bash: line 79: syntax error near unexpected token ')'`)—— 已按「把 node 脚本抽成独立 `.mjs` 上传执行」重做,**本轮补齐**(见 A2-附)。
|
||||
|
||||
**A2-附 PG 侧残留核查**(补齐 · 退 `0`)
|
||||
```bash
|
||||
node --experimental-sqlite /tmp/p35/diag-src3.mjs # 退出码 0(DIAG3_RC=0)
|
||||
```
|
||||
```
|
||||
### J) A2 收尾:PG 侧残留核查(应全 0)
|
||||
PG_RESIDUE hot_accounts (cols=account_name,local_id) = 0
|
||||
PG_RESIDUE account_videos (cols=aweme_id,local_id,account_id) = 0
|
||||
PG_RESIDUE account_video_source(cols=aweme_id,video_id) = 0
|
||||
PG_TOTALS={"a":"5","b":"362","c":"34"}
|
||||
```
|
||||
**判定**:✅ **达成(平台取数口面)** —— 三表 7 步读写 **全 200**、6 个负例**全 `bad-args`**、0 行返回 `{ok:true,value:[]}`、写入残留 **0**。
|
||||
⚠️ **「实例内」那一半仍未达成**(执行单 A2 原注:「实例内读数需 47 重启 ⇒ 未重启则该条未达成」)⇒ **随 ⑦ 留给下一棒**。
|
||||
|
||||
### A3 结构改写面处置完毕
|
||||
|
||||
```bash
|
||||
grep -rniE "\bJOIN\b" lib | grep -v "\.join(" # 残留全落在 local() 回调 / 本地专属分支 / 第四批主表 rewrite_log(具名豁免)
|
||||
grep -rn "LIMIT ? OFFSET ?" lib
|
||||
grep -rnE "ROW_NUMBER|OVER\s*\(" lib
|
||||
```
|
||||
**判定**:✅ 达成(第 34 棒已验并复扫;**平台档路径零命中**;本棒**未改任何调用点** ⇒ 读数逐字不变)。
|
||||
|
||||
### A4 表名对齐彻底
|
||||
|
||||
```bash
|
||||
cd "$PKG"
|
||||
grep -rE "account_video_analysis" . | grep -v /node_modules/ | wc -l # → 0
|
||||
grep -c "hot_account_ext" lib/host/mcn/store.js # → 10
|
||||
```
|
||||
**判定**:✅ 达成(**0** / **10 > 0**)。
|
||||
|
||||
### A5 已按表切档(三张表)
|
||||
|
||||
```bash
|
||||
# [真机 47] 读该用户 <home>/.dsh/data-plane.json
|
||||
cat /var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde/home/.dsh/data-plane.json
|
||||
```
|
||||
```
|
||||
INSTANCE_DP_UNCHANGED={"tables":{"account_persona":{"writeMode":"platform","mode":"platform"},
|
||||
"creative_log":{"writeMode":"platform","mode":"platform"},
|
||||
"account_analysis":{"writeMode":"platform","mode":"platform"},
|
||||
"video_analysis":{"writeMode":"platform","mode":"platform"}},"updatedAt":"2026-09-25T09:07:28.440Z"}
|
||||
PLANE={"tables":{"account_video_source":{"mode":"shadow"},"account_videos":{"mode":"shadow"},
|
||||
"hot_accounts":{"mode":"shadow"}},"updatedAt":"2026-09-25T14:12:06.586Z"}
|
||||
```
|
||||
> ☝️ 第一个是**实例真档**(仍 4 表);第二个是**探针档**(`DSH_HOME` 重定向到 `/tmp`,只用来取 shadow 读数)—— ⛔ 探针档**不是**实例档,两处别混。
|
||||
|
||||
**判定**:🔴 **未达成** —— 三表**仍 `local`**(总数仍 4 表 `platform`,非 A5 要求的 7 表)。**原因 = ⑥ 未全绿(依铁律停住)**。
|
||||
|
||||
### A6 旧库备份在位 + 零数据损失可回滚
|
||||
|
||||
```bash
|
||||
# [真机 47]
|
||||
sha256sum "$HOME/.dsh/mcn-plugin.db" | cut -d' ' -f1 # 迁移前后各取一次
|
||||
tar -tzf /opt/dsh/backups/plugin-data/dsh-plugin-mcn-suite/cce6d1cd-…-mcn-plugin-db-20260925-141436.tar.gz
|
||||
```
|
||||
```
|
||||
b37f8aab51a90f7481eb321e50456ca347635bcf2bfce567ea4200d88c18277a # 迁移前
|
||||
b37f8aab51a90f7481eb321e50456ca347635bcf2bfce567ea4200d88c18277a # 迁移后(逐字一致)
|
||||
DB owner=dsh-cce6d1cdb376430480f0 mtime=2026-09-20 23:05:05.178301565 +0800 # mtime 未变
|
||||
```
|
||||
**判定**:✅ 达成 —— 备份在位可解出;本地旧库**未被删、未被改**(sha256 + mtime 双证)。
|
||||
🔴 **本棒新增一处 R10 修复(必须记)**:`<home>/.dsh/data-plane.json` 属主为 **`root:root`**(**第 32 棒 root 探针遗留**)⇒ 切档前必须先修,否则**实例 uid 写不动档位**。已 `find "$UHOME" -user root -exec chown "$UOWNER:$UOWNER" {} +` ⇒
|
||||
```
|
||||
ROOT_OWNED /var/lib/dshs/users/…/home/.dsh/data-plane.json root:root 644
|
||||
ROOT_OWNED_AFTER_CHOWN=0
|
||||
… data-plane.json owner=dsh-cce6d1cdb376430480f0:dsh-cce6d1cdb376430480f0 mode=644
|
||||
ROOT_OWNED_FINAL=0
|
||||
```
|
||||
|
||||
### A7 移动端五条
|
||||
|
||||
**判定**:✅ **免跑** —— 本棒**未改任何 UI 代码**(`lib/client.js` 零改动;改动面恰 **6 件**:`store.js`/`package.json`/`tests/mcn-store.test.mjs`/(出包物)`dist/*.tgz`;调用方 **0 处**)。
|
||||
|
||||
### A8 两机一致(代码面 / 运行面分开记)
|
||||
|
||||
```bash
|
||||
# [47]
|
||||
grep -m1 '"version"' /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/package.json
|
||||
md5sum /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/host/mcn/store.js | cut -d' ' -f1
|
||||
grep -c "gz1:" /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/host/mcn/store.js
|
||||
```
|
||||
```
|
||||
"version": "0.5.0",
|
||||
bc02c23c080eee58d8873508badc1c5b # 与本地 PKG 逐字一致
|
||||
4 # 处方 (a) 已进共享层
|
||||
```
|
||||
```bash
|
||||
# [47] 节点登记表(查 106 端点)
|
||||
SELECT * FROM dsh_hosts;
|
||||
```
|
||||
```
|
||||
HOST {"id":"w-47","endpoint":"http://127.0.0.1:19100",…,"status":"up","via":"local"}
|
||||
HOST {"id":"w-106","endpoint":"http://127.0.0.1:19000",…,"status":"up","via":"relay"}
|
||||
```
|
||||
**判定**:🔴 **不一致(如实记,⛔ 不得写「一致」)** —— **47 共享层 = `0.5.0`(本棒投放,已取证)**;**106 本棒未触碰、未取证**(`w-106` 仅经中继可见、端点是回环隧道,本棒无 106 作业面)⇒ 106 侧同步**随重启窗口在下一棒做**。
|
||||
|
||||
### A9 红线零新增
|
||||
|
||||
```bash
|
||||
cd "$PKG"
|
||||
grep -rEn "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)" . | grep -v /node_modules/ | wc -l
|
||||
```
|
||||
```
|
||||
23
|
||||
```
|
||||
**判定**:✅ 达成(**23**,与第 27/28/29/33/34 棒同值;处方 (a) 只加**运行时编解码**,**零新增 DDL / 驱动**)。
|
||||
|
||||
### 产物指纹(`report §八-6` 要求)
|
||||
|
||||
| 件 | 指纹 |
|
||||
|---|---|
|
||||
| `lib/host/mcn/store.js` | md5 **`bc02c23c080eee58d8873508badc1c5b`** / 117,808 B / 纯 LF |
|
||||
| `tests/mcn-store.test.mjs` | 64,299 B / 纯 LF / `# tests 66 / # pass 66 / # fail 0` |
|
||||
| `dist/dsh-plugin-mcn-suite-0.5.0.tgz` | **2,017,667 B** / 324 条目 / md5 **`5cbccac1348e5dcb8d11bf400a000498`** / `npm shasum 4e025aba87a4c3edf6cb36e389c6f7ca1bcf873e` |
|
||||
| `package.json` | `"version": "0.5.0"`(仅 `version` + `description` 两字段与 `0.4.0` 桶不同) |
|
||||
| 共享层(47) | `…/bundled-plugins/dsh-plugin-mcn-suite/` `version 0.5.0`/`store.js` md5 同本机 |
|
||||
|
||||
---
|
||||
|
||||
## 三 🔴 ⑥ 未全绿的根因(两条 · 逐条取证 · **均非数据丢失**)
|
||||
|
||||
### 甲|计数 39 ≠ 34 = 本地 5 组重复 `aweme_id`(平台主键唯一 ⇒ 必然折叠)
|
||||
|
||||
```bash
|
||||
node --experimental-sqlite /tmp/p35/diag-src.mjs # 退出码 2(尾段平台读撞会话过期;甲段已跑完)
|
||||
```
|
||||
```
|
||||
LOCAL_TOTAL=39
|
||||
DUPKEY aweme_id=7658997937279683855 rows=2 ids=[20,40] 正文全同=false 分析全同=true
|
||||
DUPKEY aweme_id=7648947073131625905 rows=2 ids=[27,28] 正文全同=false 分析全同=true
|
||||
DUPKEY aweme_id=7667477088350044069 rows=2 ids=[29,44] 正文全同=true 分析全同=true
|
||||
DUPKEY aweme_id=7631176055600527729 rows=2 ids=[31,42] 正文全同=true 分析全同=true
|
||||
DUPKEY aweme_id=7629803455813243043 rows=2 ids=[32,46] 正文全同=true 分析全同=true
|
||||
DUP_SUMMARY 组数=5 冗余行数=5 组内完全一致=3/5 唯一键数=34
|
||||
```
|
||||
- 本地 `account_video_source` **39 行 = 34 个唯一 `aweme_id` + 5 个历史副本**;平台主键 `(user_id, aweme_id)` **唯一** ⇒ **平台只能有 34 行**。
|
||||
- 5 组里 **3 组两个副本逐字全同**;**2 组**(`…9683855`、`…625905`)**只有正文差 2 字节**(分析列全同)。
|
||||
- 🔴 **本表按 `id` 累积历史版本、读路径取最新**(`store.js` 注释与本地 SQL 一致:`… WHERE video_id=? OR (aweme_id IS NOT NULL AND aweme_id=?) ORDER BY id DESC LIMIT 1`;另有两处是 `IN` 集合索引/`COUNT(*)>0` 布尔)⇒ 旧副本**对读取不可见**;平台档下 `findVideoSources` 亦按主键取单行 ⇒ **行为等价**。
|
||||
- ⚠️ **但这不等于可以自决删数据**:要与 A1 判据(`本地 n == 平台 n`)对齐,只能**删本地 5 行冗余副本**(**不可逆**)或**改判据**(须回规划棒)⇒ 见 §5-①(**待拍板**)。
|
||||
|
||||
### 乙|逐键内容 23/34 不等 = `jsonb` 键序重排 × `canonicalJsonText()` 保留序(**假阴性**)
|
||||
|
||||
```bash
|
||||
node --experimental-sqlite /tmp/p35/diag-src3.mjs # 退出码 0(DIAG3_RC=0)
|
||||
```
|
||||
```
|
||||
### H) 与序无关的深度相等(本地该键 **最新一行** vs 平台读回)
|
||||
### H_SUMMARY 唯一键=34 正文+分析 序无关全等(最新一行)=32 序无关全等(任一本地副本)=34 仍有差异=0
|
||||
### I) 交叉表:`gz1` 形态 vs 与序无关相等
|
||||
FM aweme_id=7642280101581146850 PG_content[type=string gz1=true ] 序无关相等=true PG_analysis[type=null …]
|
||||
FM aweme_id=7648968851887115563 PG_content[type=object gz1=false] 序无关相等=true PG_analysis[type=null …]
|
||||
FM aweme_id=7628138993386034792 PG_content[type=object gz1=false] 序无关相等=true PG_analysis[type=object …]
|
||||
```
|
||||
```bash
|
||||
# [真机 47] PG 地面真值:jsonb 值类型分布
|
||||
SELECT jsonb_typeof(content_json), jsonb_typeof(analysis_json), count(*) FROM p_…_account_video_source GROUP BY 1,2;
|
||||
```
|
||||
```
|
||||
PG_TYPE content=string analysis=string rows=6
|
||||
PG_TYPE content=object analysis=null rows=3
|
||||
PG_TYPE content=string analysis=null rows=3
|
||||
PG_TYPE content=object analysis=object rows=20
|
||||
PG_TYPE content=string analysis=object rows=2
|
||||
```
|
||||
- **`gz1=true`(落成 jsonb `string`)的行 ⇒ 逐键 `match`;`gz1=false`(落成 jsonb **对象**)的行 ⇒ 逐键 `match=false`**,**11 / 23 两侧完全对得上**(`jsonb_typeof=string` 的行数恰为 11)。
|
||||
- 换成**与序无关**的深度比对 ⇒ **34/34 全等、仍有差异 = 0** ⇒ **平台侧内容与本地逐字段完全一致,不存在内容丢失**。
|
||||
- 🔴 **机制**:PostgreSQL `jsonb` **会按"键长度+字母序"重排对象键**;而插件 `canonicalJsonText()` 是 `JSON.stringify`(**保留键序**)⇒ 只要值以 **jsonb 对象**形态落库,读回后 canon **必然**与本地文本不等。
|
||||
- 🔴 **这是真缺陷(不是验收口径问题)**:`canonicalJsonText()` 还被
|
||||
- 迁移幂等 / 稳定键生成、
|
||||
- `imports.js` 的**「同视频+同内容 ⇒ 去重」**判据(`… AND content_json=?` 的平台档对应实现)、
|
||||
- `imports.js` 的**预览判据**(`sr.ok` 分支)
|
||||
共用 ⇒ **切到 `platform` 档后,同内容会被判成不同内容**(倾向于**重复写入**,R11 净变差)⇒ **必须在切档前修**(修法 = 递归按键排序后再序列化;**技术项 ⇒ 自决**)。
|
||||
|
||||
### 丙|附带纠正与澄清(防止后棒按错记开工)
|
||||
|
||||
| 项 | 旧记 | 实测纠正 |
|
||||
|---|---|---|
|
||||
| `account_video_source` 空 `aweme_id` 行数 | 执行单 §附录 C-2「**预计非零**」 | **0**(`badRows` 三表全 0) |
|
||||
| `CONTENT … 同=12 异=27`(第 35 棒中途读数) | 疑似内容丢失 | **假阴性** —— 该读数的 `analysis` 长度打印串是 `String(obj)` ⇒ 恒为 `"[object Object]"`(15 字),**是探针自身的量测坑**,⛔ 后棒别拿它当结论(已在本件用 PG 地面真值 + 与序无关比对推翻) |
|
||||
| 会话 TTL | — | `mksess.cjs` 的临时会话 **TTL = 600 s** ⇒ 长探针**必须现场新建**,否则处处 401(本次撞过一次,报 `not-registered / unauthorized`) |
|
||||
|
||||
---
|
||||
|
||||
## 四 未闭合项(逐条「现象 / 归属 / 下一步」)
|
||||
|
||||
| # | 现象 | 归属 | 下一步 |
|
||||
|---|---|---|---|
|
||||
| 1 | `account_video_source` `shadow match=false`(34 vs 39,5 组重复键) | **规划面判据 + 本地数据**(⛔ 非平台侧、⛔ 非插件逻辑) | **待拍板**(§5-①)⇒ 定后由下一棒执行 |
|
||||
| 2 | `canonicalJsonText()` 键序敏感 ⇒ 平台档判据假阴性 | **插件侧**(`store.js`) | **技术项 · 自决** ⇒ 下一棒修 + 出包 `0.5.1` + 重投 + 重取影子读数 |
|
||||
| 3 | ⑦ 切档 + 重启验收**整条未执行**(47 实例 + 106 合并窗口) | 本棒(**有意停住**) | 下一棒(窗口**已获用户批准**,见 §0 21:33 行) |
|
||||
| 4 | A2「实例内」那一半未达成 · A5 未达成 | 同 3 | 同 3 |
|
||||
| 5 | 106 共享层仍为旧版(两机不一致) | 部署面 | 下一棒随重启窗口同步 |
|
||||
| 6 | `hot_accounts` / `account_videos` 的 `local_id` **回填已到位**,但「映射改优先平台」未做 | 插件侧(第 31/32 棒遗留 ㈦ 的部分缓解) | 下一棒(`restoreLocalId` 已让 `row.id` 在平台档可用) |
|
||||
| 7 | 插件包源码**仍未纳入版本管理**(`??` 未跟踪) | **用户拍板项 ㈣** | **待拍板**(§5-②) |
|
||||
| 8 | 共享锁目录曾出现**非本线遗留物**(`.exec-lock/OWNER = __st_other_1879`;`.locks/修域锁释放缺陷-…`) | 他人 lane | 按 **R9** ⛔ 不删锁、不接管;本棒收口时 `.exec-lock` **已自然消失**(本会话已复核) |
|
||||
|
||||
---
|
||||
|
||||
## 五 待拍板(**自包含** · 一轮一问 · 每条带优缺点 · ⛔ 不在接续棒内自决)
|
||||
|
||||
> ✅ **两条均已于 2026-09-25 22:44 由用户拍板**(原提问与优缺点保留在下方,供追溯):
|
||||
> - **① ⇒ A 案**:**删掉本地那 5 条旧副本**(不可逆 · 已授权)。执行要求(已写入第 36 棒):先原样导出留档(`tmp/p36/`)+ 打印清单 → 再删 → 删后本地 `account_video_source` 应 = **34 行**;⛔ 只删行、⛔ 绝不删本地库文件 `mcn-plugin.db`、⛔ 不删 `db.js`、⛔ 不重建库。
|
||||
> - **② ⇒ B 案**:**不纳入版本管理**,改为定期打压缩包备份到固定目录。**已由第 35 棒会话落地**:固定目录 `E:/dsh-backups/plugin-src/`(`backup.sh` + 基线包 `mcn-suite-src-20260925-2246-v0.5.0.tgz`/**6,065,179 B**/**330 文件**/md5 `247dd1456597e5496a695f4e91d695dc`/内容摘要 `9c38cdb4…`)+ 每周自动备份 automation **`1e3c9070-a75d-4ae4-b81e-06e2eb20a5b4`**(周日 03:30 · 内容摘要去重「未变则跳」· 保留最近 26 份)。⚠️ **该机制只防误删,不防同一块盘损坏**(要防盘坏需再复制一份到别的机器/盘)。
|
||||
> ⇒ **第 36 棒起按已定执行,⛔ 不再重问这两条。**
|
||||
|
||||
### ① 平台上「一个视频只能存一条来源记录」,本地却存了 5 组重复(同一视频两条)。要不要**删掉本地那 5 条旧副本**,好让两边数量对齐?
|
||||
|
||||
**为什么得由你定**:这一步要么**动实例里的真实数据(删 5 行,不可逆)**,要么**放宽这条验收判据**。两条路都对「这台机器上那位用户的资料」有实际影响,且**没有客观更优的一方** ⇒ 属边界外。**现在不决定也可以** —— 现象已具名,平台侧内容 **34/34 与本地逐字段一致**(已实测),**没有数据丢失**;只是「切换开关」这一步卡住不动,第三批就到不了终点。
|
||||
|
||||
- **A 案|删本地 5 条旧副本**(保留每条视频最新那一份)
|
||||
优点:一次做完、验收判据如实达成;对读用户**无可见影响**(读路径本来就只取最新那一份,多出来的旧副本从来读不到)。
|
||||
缺点:**不可逆**(只能靠备份回滚);其中 2 组的旧副本正文与最新副本**差 2 个字符**,删了就没了。
|
||||
- **B 案|不删数据,改判据为「同一视频只算一次 + 逐条内容比对」**
|
||||
优点:**零数据变更**,风险最低。
|
||||
缺点:改的是规划棒定死的判据(`本地 n == 平台 n`),按规矩要**回规划棒**再改一次单子;且今后每台机器都得靠这条新口径解释。
|
||||
- **C 案|保持现状不动**(继续停在 `local` 档)
|
||||
优点:今天什么风险都不担。
|
||||
缺点:第三批**永远到不了终点**,本地库与平台库长期并存、两处结论会越走越远。
|
||||
|
||||
**我的倾向**:**A 案**(可推翻)—— 5 条旧副本对读取不可见、删除前有备份 + 我会先出逐条清单;⚠️ 但它**不可逆**,所以**必须你点头**我才动。
|
||||
|
||||
### ② 这次改的插件源码(近一个月、十几个文件)要不要纳入版本管理?
|
||||
|
||||
**为什么得由你定**:这决定「以后回滚靠什么」—— 是**靠 git 记录**,还是**只能靠备份包**。属影响面偏好,不是技术优劣。
|
||||
|
||||
- **A 案|现在就纳入**(把该目录加进仓库并提交)
|
||||
优点:改动有版本留痕、可逐笔回滚、后续多会话协作可对账。
|
||||
缺点:要动仓库与提交动作;该包目前形态(大文件+出包产物)需先定「哪些进、哪些不进」。
|
||||
- **B 案|暂不纳入**(继续只靠 sha256 + 交付件留痕)
|
||||
优点:不动仓库,零额外动作。
|
||||
缺点:**近一个月改动零版本留痕**;一旦误删/误改,只能靠备份整包回退,**无法定位到某一笔改动**。
|
||||
|
||||
**我的倾向**:**A 案**(可推翻)—— 本线已连续 8 棒在同一批文件上改,留痕收益已超过成本。
|
||||
|
||||
> ⚠️ 其余存量待拍板项(㈠ 47 实例重启窗口 ✅ 已于 21:33 批准;㈡ 106 重启窗口 ✅ 已批准「可以合并那是 worker」;㈢ `json` 列是否改声明 —— **本批不需要**,留档)**本棒一项未新增、未改动**。
|
||||
|
||||
---
|
||||
|
||||
## 六 卫生声明
|
||||
|
||||
- ⛔ **未改** `D:/github/dsh_shenxian/src/**`(平台侧**零改动**)· ⛔ 未改 `im.ts` · ⛔ 未改装配协议。
|
||||
- ⛔ **未删** 本地旧库 `mcn-plugin.db`(sha256 + mtime 双证未变)· ⛔ 未删 `db.js` · ⛔ **平台库未删未重建**。
|
||||
- ⛔ **未重启任何服务**(47 实例 / 106 全未触碰)· ⛔ **未逐表切档**。
|
||||
- ⛔ **未 commit / 未 push**(见 §5-②)。
|
||||
- ⛔ **未改** `D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite/` 以外任何工作区文件(本棒另动的三处均为**本线自有文档**:本工作区交付物 + 本线入口 + 文档库 `05-交接单/README.md`)。
|
||||
- ✅ **R4 卫生**:临时会话已清理(`DELETE FROM sessions WHERE user_agent='poc-curl2'` ⇒ `SESSIONS_BEFORE=7 / SESSIONS_DELETED=7 / SESSIONS_AFTER=0`)。
|
||||
- ✅ **真机产物已清**:`/tmp/p35*`、`/tmp/mksess-guest.cjs` 全删(`TMP_P35_CLEAN`);本机证据与探针脚本留在 `$WS/tmp/p35/`(tmp 保留期 7 天)。
|
||||
- ✅ **R10 修复**:实例 home 下 `root:root` 属主配置已 chown 回实例 uid(`ROOT_OWNED_FINAL=0`)。
|
||||
|
||||
### 域锁
|
||||
|
||||
- 抢锁:`bash $DOC/07-scripts/handoff-guard.sh --claim-exec "p35-exec-plugin-dataline" --domains <9 键>` ⇒ `rc=0`;复读断言:
|
||||
```
|
||||
OWNER: p35-exec-plugin-dataline / 开始:09-25 21:47 / ME:p35-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
|
||||
```
|
||||
- 开工门禁如实记账:`preflight-lock.sh` 对本棒 **8 个目标路径全判【D】未归类** ⇒ 按字面 `rc=1`;依第 26–34 棒**既有惯例**开工并报告,⛔ **未改那个跨库脚本**。
|
||||
- 释放:**22:38 已带会话名释放**(`ME="p35-exec-plugin-dataline" … --release-exec` ⇒ `RC=0`;复核 `.locks/p35-exec-plugin-dataline`/`.exec-lock` 均已移除、`find -name OWNER` 零命中、`.locks/` 仅余空容器 `.migrations/`)。随后**为落两条拍板 22:44 二次抢锁**(3 域:入口/交付物/`.workbuddy`,`rc=0`,OWNER 归属已断言),**改完即再次带会话名释放**。
|
||||
- 备份(拍板②):**固定目录 `E:/dsh-backups/plugin-src/`** 已建(脚本 `backup.sh` + `pkgs/` + `backup.log` + `.last.sha256`);基线包 `mcn-suite-src-20260925-2246-v0.5.0.tgz` **6,065,179 B**/**330 文件**/md5 `247dd1456597e5496a695f4e91d695dc`;幂等实测:二次运行输出 `RESULT=SKIP_UNCHANGED` 且包数仍为 1(未新增)。每周日 03:30 自动执行(automation `1e3c9070-a75d-4ae4-b81e-06e2eb20a5b4`)。⚠️ 同盘备份**只防误删,不防盘坏**。
|
||||
|
||||
---
|
||||
|
||||
## 七 下一棒(第 36 棒)范围(已按上表登记)
|
||||
|
||||
> ✅ **本节已于 2026-09-25 22:44 按用户拍板改写**(原为条件式;现两条拍板到手 ⇒ **条件步转无条件**,范围见入口 §2「⏭️ 本轮动作(第 36 棒)」)。
|
||||
|
||||
1. 🔴 **修 `canonicalJsonText()` 键序敏感性**(技术项 · 自决;修法 = 递归按键排序后序列化)+ 补一条单测(**jsonb 键序被打乱后仍判相等**);`node --test tests/mcn-store.test.mjs` 必须全过。
|
||||
2. 出包 **`0.5.1`** → 上池 →(结构迁移到 `state=ready`)→ 发布共享层(记条目 / 字节 / md5)。
|
||||
3. **删本地 5 条旧副本**(执行拍板①):**先**原样导出留档(`tmp/p36/`)+ 打印清单(表名/主键值/保留哪份/删哪份)→ **再**删 → 删后本地 `account_video_source` 应 = **34 行**。⛔ 只删行、⛔ 绝不删本地库文件 `mcn-plugin.db`、⛔ 不删 `db.js`、⛔ 不重建库。
|
||||
4. 重取 `account_video_source` 影子读数 ⇒ 应转 **两面对齐 34 = 34 + 内容判据全绿**。
|
||||
5. **闭环 ⑥ + 执行 ⑦**:三表逐表切 `platform` → **47 实例 + 106 合并同一次窗口**重启(窗口已批)→ 三表 E2E(A2 实例内面)+ 真机读数验 A5/A8。
|
||||
6. 收口:释放域锁(`ME=`)· automation 置 PAUSED + 排期(⑤ 完成 ⇒ 不登记下一棒)· 入口 `§0`/`§2` · `05-交接单/README.md §一` · 当日日志 · 交付件(A1–A9 附命令 + 输出 + 退出码)。⛔ 仍不 commit / 不 push。
|
||||
|
||||
**变更记录**:2026-09-25 22:5x 第 35 棒(执行棒 · 会话 `p35-exec-plugin-dataline`)落盘;纯 LF。
|
||||
@@ -0,0 +1,211 @@
|
||||
file line ops tables where_cols
|
||||
lib/host/mcn/data.js 9 SELECT|COUNT hot_accounts
|
||||
lib/host/mcn/data.js 24 INSERT hot_accounts
|
||||
lib/host/mcn/data.js 27 ? ?
|
||||
lib/host/mcn/data.js 36 ? ?
|
||||
lib/host/mcn/data.js 38 ? ?
|
||||
lib/host/mcn/data.js 68 SELECT|COUNT hot_accounts
|
||||
lib/host/mcn/data.js 75 SELECT|COUNT|OFFSET hot_accounts|account_videos|rewrite_log account_id,script_text,status,video_id
|
||||
lib/host/mcn/data.js 85 SELECT hot_accounts
|
||||
lib/host/mcn/data.js 102 SELECT hot_accounts del_flag,id
|
||||
lib/host/mcn/data.js 107 SELECT|COUNT|OFFSET account_videos|account_video_source|account_video_analysis account_id,aweme_id,video_id
|
||||
lib/host/mcn/data.js 121 SELECT|COUNT account_video_source aweme_id
|
||||
lib/host/mcn/data.js 122 SELECT|COUNT account_video_analysis aweme_id
|
||||
lib/host/mcn/data.js 132 SELECT|COUNT account_videos account_id
|
||||
lib/host/mcn/db.js 31 CREATE hot_accounts
|
||||
lib/host/mcn/db.js 50 PRAGMA hot_accounts
|
||||
lib/host/mcn/db.js 52 ALTER hot_accounts
|
||||
lib/host/mcn/db.js 55 ALTER hot_accounts
|
||||
lib/host/mcn/db.js 72 ALTER hot_accounts
|
||||
lib/host/mcn/db.js 75 ? hot_accounts
|
||||
lib/host/mcn/db.js 76 ? hot_accounts
|
||||
lib/host/mcn/db.js 77 ? hot_accounts
|
||||
lib/host/mcn/db.js 79 UPDATE hot_accounts account_type
|
||||
lib/host/mcn/db.js 81 CREATE account_videos|account_video_source|account_persona|account_analysis|creative_log|rewrite_log|storyboard_log|script_review|account_video_analysis
|
||||
lib/host/mcn/db.js 177 PRAGMA account_videos
|
||||
lib/host/mcn/db.js 178 ALTER account_videos
|
||||
lib/host/mcn/db.js 182 PRAGMA rewrite_log
|
||||
lib/host/mcn/db.js 183 ALTER rewrite_log
|
||||
lib/host/mcn/db.js 185 SELECT rewrite_log script_text,title
|
||||
lib/host/mcn/db.js 187 UPDATE rewrite_log id
|
||||
lib/host/mcn/db.js 192 PRAGMA account_video_analysis
|
||||
lib/host/mcn/db.js 193 ALTER account_video_analysis
|
||||
lib/host/mcn/db.js 194 SELECT|UPDATE account_videos|account_video_analysis id
|
||||
lib/host/mcn/db.js 198 PRAGMA account_video_source
|
||||
lib/host/mcn/db.js 199 ALTER account_video_source
|
||||
lib/host/mcn/db.js 200 ALTER account_video_source
|
||||
lib/host/mcn/db.js 204 PRAGMA hot_accounts
|
||||
lib/host/mcn/db.js 205 ALTER hot_accounts
|
||||
lib/host/mcn/db.js 209 PRAGMA hot_accounts
|
||||
lib/host/mcn/db.js 210 ALTER hot_accounts
|
||||
lib/host/mcn/db.js 215 PRAGMA rewrite_log
|
||||
lib/host/mcn/db.js 216 ALTER rewrite_log
|
||||
lib/host/mcn/db.js 217 UPDATE rewrite_log gen_method,video_id
|
||||
lib/host/mcn/db.js 218 UPDATE rewrite_log gen_method
|
||||
lib/host/mcn/db.js 222 UPDATE hot_accounts track
|
||||
lib/host/mcn/db.js 233 SELECT hot_accounts
|
||||
lib/host/mcn/db.js 244 SELECT account_video_source aweme_id,video_id
|
||||
lib/host/mcn/db.js 247 UPDATE account_video_source id
|
||||
lib/host/mcn/db.js 253 SELECT account_video_source
|
||||
lib/host/mcn/db.js 256 SELECT account_videos
|
||||
lib/host/mcn/db.js 290 UPDATE account_video_source id
|
||||
lib/host/mcn/imports.js 10 SELECT hot_accounts account_type,douyin_id
|
||||
lib/host/mcn/imports.js 14 SELECT hot_accounts account_name,account_type
|
||||
lib/host/mcn/imports.js 30 SELECT hot_accounts del_flag,douyin_id
|
||||
lib/host/mcn/imports.js 31 SELECT hot_accounts account_name,del_flag
|
||||
lib/host/mcn/imports.js 48 UPDATE hot_accounts id
|
||||
lib/host/mcn/imports.js 52 INSERT hot_accounts
|
||||
lib/host/mcn/imports.js 72 INSERT|UPDATE account_videos
|
||||
lib/host/mcn/imports.js 103 SELECT|JOIN hot_accounts|account_videos id
|
||||
lib/host/mcn/imports.js 109 SELECT account_video_source aweme_id,video_id
|
||||
lib/host/mcn/imports.js 147 SELECT account_video_source aweme_id,content_json,video_id
|
||||
lib/host/mcn/imports.js 154 INSERT account_video_source
|
||||
lib/host/mcn/imports.js 157 UPDATE account_video_source id
|
||||
lib/host/mcn/imports.js 161 UPDATE account_videos id
|
||||
lib/host/mcn/imports.js 174 SELECT|JOIN hot_accounts|account_videos id
|
||||
lib/host/mcn/imports.js 220 SELECT account_video_source aweme_id,video_id
|
||||
lib/host/mcn/imports.js 241 SELECT hot_accounts del_flag
|
||||
lib/host/mcn/imports.js 262 SELECT account_video_source content_json
|
||||
lib/host/mcn/imports.js 264 INSERT account_video_source
|
||||
lib/host/mcn/imports.js 267 SELECT account_video_source id
|
||||
lib/host/mcn/imports.js 268 UPDATE account_video_source id
|
||||
lib/host/mcn/imports.js 275 UPDATE account_videos id
|
||||
lib/host/mcn/imports.js 321 SELECT account_persona account_id,content_json
|
||||
lib/host/mcn/imports.js 323 INSERT account_persona
|
||||
lib/host/mcn/imports.js 327 SELECT account_analysis account_id,content_json
|
||||
lib/host/mcn/imports.js 329 INSERT account_analysis
|
||||
lib/host/mcn/imports.js 347 SELECT account_video_analysis aweme_id,content_json,video_id
|
||||
lib/host/mcn/imports.js 349 INSERT account_video_analysis
|
||||
lib/host/mcn/imports.js 362 SELECT account_video_source aweme_id,content_json,video_id
|
||||
lib/host/mcn/imports.js 364 INSERT account_video_source
|
||||
lib/host/mcn/imports.js 374 UPDATE account_videos id
|
||||
lib/host/mcn/imports.js 392 SELECT hot_accounts account_name,del_flag
|
||||
lib/host/mcn/imports.js 405 INSERT|UPDATE account_videos
|
||||
lib/host/mcn/imports.js 445 SELECT account_persona account_id,content_json
|
||||
lib/host/mcn/imports.js 446 INSERT account_persona
|
||||
lib/host/mcn/imports.js 448 SELECT account_analysis account_id,content_json
|
||||
lib/host/mcn/imports.js 449 INSERT account_analysis
|
||||
lib/host/mcn/imports.js 453 SELECT account_videos account_id,aweme_id
|
||||
lib/host/mcn/imports.js 455 SELECT account_video_analysis aweme_id,content_json,video_id
|
||||
lib/host/mcn/imports.js 458 DELETE account_video_analysis aweme_id,video_id
|
||||
lib/host/mcn/imports.js 459 INSERT account_video_analysis
|
||||
lib/host/mcn/imports.js 466 SELECT account_videos account_id,aweme_id
|
||||
lib/host/mcn/imports.js 468 SELECT account_video_source aweme_id,content_json,video_id
|
||||
lib/host/mcn/imports.js 471 DELETE account_video_source aweme_id,video_id
|
||||
lib/host/mcn/imports.js 472 INSERT account_video_source
|
||||
lib/host/mcn/imports.js 501 SELECT hot_accounts del_flag,douyin_id
|
||||
lib/host/mcn/imports.js 502 SELECT hot_accounts account_name,del_flag
|
||||
lib/host/mcn/imports.js 507 SELECT hot_accounts id
|
||||
lib/host/mcn/imports.js 535 SELECT account_videos account_id,aweme_id
|
||||
lib/host/mcn/imports.js 548 SELECT ? account_id,content_json
|
||||
lib/host/mcn/imports.js 566 SELECT account_video_analysis aweme_id,content_json,video_id
|
||||
lib/host/mcn/imports.js 577 SELECT account_video_source aweme_id,content_json,video_id
|
||||
lib/host/mcn/index.js 147 SELECT|AGG ?
|
||||
lib/host/mcn/index.js 796 SELECT rewrite_log id
|
||||
lib/host/mcn/index.js 801 SELECT storyboard_log id
|
||||
lib/host/mcn/index.js 829 SELECT rewrite_log id
|
||||
lib/host/mcn/index.js 834 SELECT storyboard_log id
|
||||
lib/host/mcn/index.js 840 SELECT|COUNT account_videos|account_video_source aweme_id,id,video_id
|
||||
lib/host/mcn/index.js 841 SELECT|COUNT account_videos|account_video_analysis aweme_id,id,video_id
|
||||
lib/host/mcn/index.js 847 SELECT|COUNT account_persona account_id
|
||||
lib/host/mcn/index.js 852 SELECT|COUNT account_analysis account_id
|
||||
lib/host/mcn/index.js 988 SELECT rewrite_log id
|
||||
lib/host/mcn/index.js 990 SELECT storyboard_log id
|
||||
lib/host/mcn/index.js 996 SELECT script_review runn,status,type,video_id
|
||||
lib/host/mcn/index.js 997 SELECT script_review rewrite_id,runn,status,type
|
||||
lib/host/mcn/index.js 1347 SELECT account_analysis account_id
|
||||
lib/host/mcn/index.js 1376 UPDATE hot_accounts id
|
||||
lib/host/mcn/index.js 1427 SELECT account_persona account_id
|
||||
lib/host/mcn/index.js 1486 SELECT|JOIN|COUNT hot_accounts|account_videos
|
||||
lib/host/mcn/index.js 1487 SELECT|JOIN|COUNT|OFFSET|AGG hot_accounts|account_videos|account_video_source|account_persona|rewrite_log|account_video_analysis JO,acc_,account_id,aweme_id,content,id,script_text,status,topic,video_id
|
||||
lib/host/mcn/index.js 1502 SELECT rewrite_log runn,status
|
||||
lib/host/mcn/index.js 1549 SELECT account_videos id
|
||||
lib/host/mcn/index.js 1551 SELECT hot_accounts id
|
||||
lib/host/mcn/index.js 1563 SELECT account_persona account_id
|
||||
lib/host/mcn/index.js 1583 INSERT rewrite_log
|
||||
lib/host/mcn/index.js 1602 UPDATE rewrite_log id
|
||||
lib/host/mcn/index.js 1623 SELECT rewrite_log id
|
||||
lib/host/mcn/index.js 1625 UPDATE rewrite_log id
|
||||
lib/host/mcn/index.js 1648 DELETE rewrite_log id
|
||||
lib/host/mcn/index.js 1651 DELETE script_review rewrite_id,type
|
||||
lib/host/mcn/index.js 1654 SELECT storyboard_log rewrite_id
|
||||
lib/host/mcn/index.js 1654 DELETE storyboard_log rewrite_id
|
||||
lib/host/mcn/index.js 1689 SELECT account_videos|account_video_source aweme_id,id,video_id
|
||||
lib/host/mcn/index.js 1698 SELECT rewrite_log script_text,video_id
|
||||
lib/host/mcn/index.js 1717 SELECT rewrite_log video_id
|
||||
lib/host/mcn/index.js 1718 SELECT rewrite_log
|
||||
lib/host/mcn/index.js 1727 SELECT|JOIN script_review jo,rewrite_id,type
|
||||
lib/host/mcn/index.js 1733 SELECT|JOIN script_review jo,type,video_id
|
||||
lib/host/mcn/index.js 1740 SELECT script_review runn,status
|
||||
lib/host/mcn/index.js 1776 SELECT rewrite_log id
|
||||
lib/host/mcn/index.js 1782 SELECT account_videos id
|
||||
lib/host/mcn/index.js 1786 SELECT account_videos|account_video_source aweme_id,id,video_id
|
||||
lib/host/mcn/index.js 1804 SELECT script_review status,type,video_id
|
||||
lib/host/mcn/index.js 1805 SELECT script_review rewrite_id,status,type
|
||||
lib/host/mcn/index.js 1811 SELECT script_review runn,status,type,video_id
|
||||
lib/host/mcn/index.js 1812 SELECT script_review rewrite_id,runn,status,type
|
||||
lib/host/mcn/index.js 1821 INSERT script_review
|
||||
lib/host/mcn/index.js 1836 INSERT script_review
|
||||
lib/host/mcn/index.js 1853 SELECT script_review rewrite_id,status,type
|
||||
lib/host/mcn/index.js 1854 SELECT script_review status,type,video_id
|
||||
lib/host/mcn/index.js 1857 INSERT script_review
|
||||
lib/host/mcn/index.js 1881 UPDATE script_review id
|
||||
lib/host/mcn/index.js 1902 SELECT script_review id
|
||||
lib/host/mcn/index.js 1950 UPDATE script_review id
|
||||
lib/host/mcn/index.js 1973 SELECT script_review id
|
||||
lib/host/mcn/index.js 1993 DELETE script_review rewrite_id,type
|
||||
lib/host/mcn/index.js 2026 SELECT rewrite_log id
|
||||
lib/host/mcn/index.js 2087 SELECT rewrite_log id
|
||||
lib/host/mcn/index.js 2094 INSERT storyboard_log
|
||||
lib/host/mcn/index.js 2102 SELECT account_videos id
|
||||
lib/host/mcn/index.js 2122 UPDATE storyboard_log id
|
||||
lib/host/mcn/index.js 2143 SELECT storyboard_log id
|
||||
lib/host/mcn/index.js 2145 UPDATE storyboard_log id
|
||||
lib/host/mcn/index.js 2167 DELETE storyboard_log id
|
||||
lib/host/mcn/index.js 2198 SELECT storyboard_log script_text,status,video_id
|
||||
lib/host/mcn/index.js 2199 SELECT storyboard_log
|
||||
lib/host/mcn/index.js 2329 SELECT hot_accounts id
|
||||
lib/host/mcn/index.js 2380 SELECT hot_accounts del_flag,id
|
||||
lib/host/mcn/index.js 2425 SELECT|JOIN hot_accounts|account_videos id
|
||||
lib/host/mcn/index.js 2432 SELECT|JOIN hot_accounts|account_videos aweme_id
|
||||
lib/host/mcn/index.js 2439 SELECT account_video_analysis aweme_id,video_id
|
||||
lib/host/mcn/index.js 2499 SELECT hot_accounts id
|
||||
lib/host/mcn/index.js 2554 SELECT|JOIN hot_accounts|account_videos id
|
||||
lib/host/mcn/index.js 2624 SELECT|COUNT hot_accounts
|
||||
lib/host/mcn/index.js 2670 SELECT hot_accounts id
|
||||
lib/host/mcn/index.js 2688 UPDATE|JOIN hot_accounts id
|
||||
lib/host/mcn/index.js 2724 INSERT creative_log
|
||||
lib/host/mcn/index.js 2766 SELECT script_review status,total,type
|
||||
lib/host/mcn/index.js 2954 SELECT|JOIN hot_accounts|rewrite_log|script_review JO,id,rewrite_id,script_text,status,total,type
|
||||
lib/host/mcn/index.js 2984 SELECT hot_accounts id
|
||||
lib/host/mcn/index.js 2996 SELECT account_persona account_id
|
||||
lib/host/mcn/index.js 3011 INSERT rewrite_log
|
||||
lib/host/mcn/index.js 3017 DELETE rewrite_log id
|
||||
lib/host/mcn/index.js 3035 UPDATE rewrite_log id
|
||||
lib/host/mcn/video-tools.js 46 SELECT account_videos account_id
|
||||
lib/host/mcn/workshop.js 167 SELECT hot_accounts account_name,del_flag,sec_uid
|
||||
lib/host/mcn/workshop.js 198 SELECT|JOIN|COUNT hot_accounts|rewrite_log
|
||||
lib/host/mcn/workshop.js 201 SELECT|JOIN|OFFSET hot_accounts|rewrite_log
|
||||
lib/host/mcn/workshop.js 212 SELECT|JOIN hot_accounts|rewrite_log id
|
||||
lib/host/mcn/workshop.js 225 SELECT|COUNT ?
|
||||
lib/host/mcn/workshop.js 226 SELECT|COUNT ?
|
||||
lib/host/mcn/workshop.js 244 DELETE script_review rewrite_id,type
|
||||
lib/host/mcn/workshop.js 246 SELECT storyboard_log rewrite_id
|
||||
lib/host/mcn/workshop.js 248 DELETE storyboard_log rewrite_id
|
||||
lib/host/mcn/workshop.js 250 DELETE rewrite_log id
|
||||
lib/host/mcn/workshop.js 257 DELETE account_video_source aweme_id
|
||||
lib/host/mcn/workshop.js 258 DELETE account_video_analysis aweme_id
|
||||
lib/host/mcn/workshop.js 260 DELETE account_video_source video_id
|
||||
lib/host/mcn/workshop.js 261 DELETE account_video_analysis video_id
|
||||
lib/host/mcn/workshop.js 262 DELETE script_review type,video_id
|
||||
lib/host/mcn/workshop.js 265 SELECT rewrite_log video_id
|
||||
lib/host/mcn/workshop.js 268 DELETE account_videos id
|
||||
lib/host/mcn/workshop.js 273 ? ?
|
||||
lib/host/mcn/workshop.js 278 DELETE account_persona account_id
|
||||
lib/host/mcn/workshop.js 279 DELETE account_analysis account_id
|
||||
lib/host/mcn/workshop.js 281 SELECT account_videos account_id
|
||||
lib/host/mcn/workshop.js 285 SELECT rewrite_log account_id,video_id
|
||||
lib/host/mcn/workshop.js 288 UPDATE hot_accounts del_flag,id
|
||||
lib/host/mcn/workshop.js 293 SELECT account_videos id
|
||||
lib/host/mcn/workshop.js 297 ? ?
|
||||
lib/host/mcn/workshop.js 299 ? ?
|
||||
|
@@ -0,0 +1,160 @@
|
||||
# MCN 数据面接入改造 · 阶段二 · **第一批按表切换(第 28 棒)**
|
||||
|
||||
> **执行会话**:插件投放与分库线-28棒(2026-09-25 14:43–15:0x)|**工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`(WS)
|
||||
> **唯一执行单**:`DOC/05-交接单/插件投放与分库线-03MCN数据面接入改造-阶段二.md`(`DOC=D:/github/dsh_shenxian/dsh-server-docs`)
|
||||
> **上一棒**:第 27 棒(`交付物/MCN数据面接入-阶段二-20260925.md`,S0/S1/S2/S3 完成)
|
||||
> **本棒范围(入口 §2「🎯 本轮动作」)**:① 给 `store.js` 加**写源档**(`local` 默认 / `platform`),与读源档**同源同文件** + 单测;② 按**「表」**(⛔ 不是按文件)推进第一批全量切换:`account_persona` + `creative_log` 跑端到端样板(全部读写点 → 迁移 → `shadow` 真机读数 → 切档)。
|
||||
|
||||
---
|
||||
|
||||
## 一 结论
|
||||
|
||||
**两件都做完了,且拿到了真机读数。** 关键判定如下(逐条可复现,见 §二 判据表):
|
||||
|
||||
1. **写源档落地** —— 读写两档共用一个配置文件(`<DSH_HOME>/.dsh/data-plane.json`),
|
||||
**单测 31/31 通过**(旧 24 + 新增 7)。默认档 = `local` ⇒ **零平台写请求**(可断言,非"应该不会")。
|
||||
2. 🔴 **补了一个交接单没写、但不做就不成立的东西:按表档**。全局单档**表达不了逐表切换**
|
||||
(一张表迁完、其余没迁时切全局 ⇒ 未迁的表读到空)。现支持
|
||||
`{"mode":"local","tables":{"account_persona":{"mode":"platform","writeMode":"platform"}}}`;
|
||||
解析顺序 = env(全局)> 该表覆盖 > 全局 file > `local`。
|
||||
3. **第一批两张表的全部读写点已接线**(含删除),逐处落点见 §三。
|
||||
4. **存量迁移真机完成**:本地 `account_persona` **4 行 → 平台 4 行**,逐账号集合
|
||||
**onlyLocal=[] / onlyPlatform=[] / match=true**;幂等重跑 `ins=0 skip=4`;**旧库 sha256 迁移前后逐字一致**
|
||||
(`b37f8aab…`)。`creative_log` 本地 **0 行** ⇒ 空迁移(无写请求)。
|
||||
5. **`shadow` 档真机读数**:`shadowReads=4`、**`shadowMismatch=0`**、三账号全 `match=true`、
|
||||
**`degraded` 一次都没出现**;平台调用 **27 次全 ok / 0 失败**。
|
||||
6. **已切档(按表)**:`account_persona` + `creative_log` 两表 = read/write 双 `platform`;
|
||||
**全局仍 `local`**、`hot_accounts` 仍 `local`(⛔ 没有"顺手全切")。回滚 = 删该文件或 `clearPlaneMode()`。
|
||||
7. ⛔ **未出包 / 未投放 / 未重启任何服务** ⇒ **实例内(插件路由)端到端未取证**,本期**不宣称通过**
|
||||
(实例仍载 `0.3.15`,即**尚无本棒代码**;生效需 47 实例重启 ⇒ 交接单 §四 4.3 ㈠ **待拍板**)。
|
||||
|
||||
---
|
||||
|
||||
## 二 判据表(命令 + 实际输出摘要 + 退出码)
|
||||
|
||||
| # | 判据 | 命令 | 实际输出 | 退出码 |
|
||||
|---|---|---|---|---|
|
||||
| **C1** | 写档单测(默认档零写请求 / 非法值折回 / `platform` 真发写请求 / 不回落 / 按表档 / 同源同文件) | `node --test tests/mcn-store.test.mjs` | `# tests 31 / pass 31 / fail 0`(旧 24 全过 + 新增 7 例 ⑱–㉔) | **0** |
|
||||
| **C2** | 语法自检(改动文件) | `node --check <f>` × `store/index/imports/workshop/data` | 5/5 OK | **0** |
|
||||
| **C3** | 包内 smoke 未回归 | `node tests/smoke.mjs` | `全部通过(16 组断言)` | **0** |
|
||||
| **C4** | 迁移工具本机自证(假 fetch) | `node --experimental-sqlite tmp/p28/local-migrate-check.mjs` | dry-run **平台调用 0**;apply `ins=2/1`;**重跑 `skip=2/1`**;`coerced=["0:content_json","1:content_json"]` | **0** |
|
||||
| **C5** | 静态改(本地库指纹不变) | `sha256sum $DB`(迁移前 / 迁移后) | 前后**逐字一致** `b37f8aab51a9…`(2,383,872 B,mtime 仍 `09-20 23:05:05`) | **0** |
|
||||
| **A1** | 存量迁移一致性(**逐表 + 逐账号**) | 47 真机探针 `P28RESULT` 的 `per_account` | `1627: 1/1`、`1628: 1/1`、`1629: 2/2`,**`onlyLocal=[]` `onlyPlatform=[]` `match=true`** | **0** |
|
||||
| **A1b** | 幂等(可重跑) | 同上 `rerun` | `account_persona: ins=0, skip=4, fail=0`|`creative_log: ins=0, skip=0` | **0** |
|
||||
| **A2** | 全表读写 E2E(**实例内**) | — | ⛔ **未达成**(需 47 实例重启 ⇒ 待拍板 ㈠;本轮⛔ 未重启、⛔ 未投放) | — |
|
||||
| **A3** | 红线归零(删 `db.js`) | — | ⛔ **未开工**(附属录 D 前置 = A1+A2 全绿;A2 未达成) | — |
|
||||
| **A4** | 表名对齐彻底 | 第 27 棒已达成(包内字面 0 命中) | 本轮未回退:本轮改动未引入旧表名(改动面 6 件全文无命中) | **0** |
|
||||
| **A5** | 旧库备份在位 + 零数据损失 | 47:`ls -la /opt/dsh/backups/plugin-data/dsh-plugin-mcn-suite/` | 备份 `cce6d1cd-…-20260925-141436.tar.gz`(809,732 B)**在位**;旧库**原样在盘**(sha256 不变) | **0** |
|
||||
| **A6** | 移动端五条 | `08 §4-补 ④` | **未改 UI ⇒ 免跑**。文件清单依据:本轮改动面恰 **6 件** = `lib/host/mcn/{store,index,imports,workshop}.js`、`tests/mcn-store.test.mjs`、`tools/migrate-local-to-platform.mjs` —— **`lib/client.js` / `lib/host/mcn/data.js` 零改动** | — |
|
||||
| **A7** | 卫生 | 见 §五 | ⛔ 未改 `src/**`|⛔ `im.ts` 零改动|⛔ 未手改用户 `home` 下 profile/`.pnpm`|⛔ 未重启 106|⛔ 未 commit·push|✅ 临时会话 `1 → 删 1 → 残留 0` | **0** |
|
||||
| **A8** | 两机一致 | — | ⛔ **未做**(本轮不投放 ⇒ 与两机部署面无交集;共享层仍 `0.4.0`,实例仍 `0.3.15`) | — |
|
||||
|
||||
### §二-补 · `shadow` 档真机读数(交接单 §五 S6 的验证项)
|
||||
|
||||
```
|
||||
[mcn-store] shadow what=account_persona.account=1627 platform=1 local=1 match=true
|
||||
[mcn-store] shadow what=account_persona.account=1628 platform=1 local=1 match=true
|
||||
[mcn-store] shadow what=account_persona.account=1629 platform=2 local=2 match=true
|
||||
[mcn-store] shadow what=account_persona.count.shadow platform=4 local=4 match=true
|
||||
stats: shadowReads=4 shadowMismatch=0 platformCalls=27 platformOk=27 platformFailed=0 writeCalls=0
|
||||
```
|
||||
- **无 `degraded`**(即一次都没有"平台不可用回落本地")
|
||||
- 影子读一律 `source:"local"`(影子档**必须**返回本地 ⇒ 零回归),`mode:"shadow"` ✅
|
||||
|
||||
### §二-补 2 · 前置读数(迁移前后)
|
||||
|
||||
| 项 | 迁移前 | 迁移后 |
|
||||
|---|---|---|
|
||||
| 平台 `account_persona` 行数 | **0** | **4** |
|
||||
| 平台 `creative_log` 行数 | 0 | 0(本地本来 0 行) |
|
||||
| 本地 `account_persona` 行数 | 4(`account_id` = 1627/1628/1629) | 4(⛔ 未动) |
|
||||
| 本地 `creative_log` 行数 | 0 | 0 |
|
||||
|
||||
---
|
||||
|
||||
## 三 逐处落点(两张表 · **全部**读写点)
|
||||
|
||||
| # | 文件:行(本棒现算) | 原形态 | 现形态 | 档位归属 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `lib/host/mcn/imports.js`(`importExternalDir` 账号设定.md) | `SELECT id…`去重 + `INSERT INTO account_persona` | `await store.insertPersona(row, { local })` —— `local` 回调里**原样那条去重 SQL** | 读写档 |
|
||||
| 2 | `lib/host/mcn/imports.js`(`importExternalData` persona 分支) | 同上 | 同上 | 读写档 |
|
||||
| 3 | `lib/host/mcn/workshop.js`(`batchDeleteCascade` 账号级) | `DELETE FROM account_persona WHERE account_id=?`(在本地事务里) | **平台侧先删**(`store.removePersonaByAccount`)+ 本地那一条**照旧保留** | 写档 |
|
||||
| 4 | `lib/host/mcn/index.js`(`taskDone` 的 `dsh_persona-*`) | `SELECT COUNT(*) … account_persona` | `await store.readCount({table:"account_persona", …})` | 读档 |
|
||||
| 5 | `lib/host/mcn/index.js`(视频列表 `has_content`) | `EXISTS(SELECT 1 FROM account_persona …)` 写在**同一条大 SQL** 里 | **platform 档**下该子查询换成 `0`,取回本页后**插件内补**(`store.accountIdsWithPersona`);`shadow`/`local` 档**仍走本地子查询**(零回归) | 读档 |
|
||||
| 6 | `lib/host/mcn/index.js`(`creative_log` 计数) | `INSERT INTO creative_log (created_time, source)` | `await store.insertCreativeLog(row, { local })` | 写档 |
|
||||
| 7–9 | `lib/host/mcn/index.js` ×3(`/mcn/api/account/persona`、脚本列表、账号详情) | 阶段一已接 `store.readRows`,但 `keyOf` 是 `persona_id ?? analysis_time` ⇒ **本地行算不出**(本地无 `persona_id`) | `keyOf = store.personaKeyOf`,并给三处本地 SELECT 补上 `account_id`(两侧共用同一键函数,影子读这才比得齐) | 读档(复核) |
|
||||
|
||||
**新增/改动的导出(`store.js`)**:`WRITE_MODES` · `dataPlaneWriteMode(table?)` · `setDataPlaneWriteMode(mode, table?)` ·
|
||||
`setDataPlaneMode(mode, table?)`(**合并写**,修掉了原来"写一次覆盖整份文件"的隐患) · `clearPlaneMode(table?)` ·
|
||||
`planeSummary()` · `readsPlatform/writesPlatform(table)` · `writeThrough({what,table,platform,local})` ·
|
||||
`insertPersona` · `removePersonaByAccount` · `insertCreativeLog` · `personaKey/personaKeyOf` ·
|
||||
`accountIdsWithPersona` · `JSON_COLUMNS` · `repairJsonText` · `canonicalJsonText`。
|
||||
(`insertPersona` 原为"夹具口",改名 **`insertPersonaDirect`** 并注明"直连平台、不看档位"。)
|
||||
|
||||
---
|
||||
|
||||
## 四 本棒的三个技术判定(自决 · 可推翻)
|
||||
|
||||
1. **写路径 ⛔ 不回落本地**(与读路径的 `degraded` 回落**方向相反**)。读回落只是"看到旧数据";
|
||||
写回落是"**写到了没人读的地方**"(调用方拿到成功、界面不报、平台侧永远是旧值)⇒ 宁可失败可见。
|
||||
单测 ㉑ 钉住:"local 回调一次都不许跑"。
|
||||
2. **`json` 列按附录 C 的 A 案(值清洗)落码**,且放在适配层**唯一咽喉**(`normalizeRow`):
|
||||
字符串是合法 JSON ⇒ 原样(⛔ 不二次包装);非法(Markdown 正文)⇒ 包成 JSON 字符串;
|
||||
非字符串 ⇒ 原样。**被清洗的列名具名进 `coerced[]`**(⛔ 不静默改数据)。
|
||||
本棒真机:**4/4 行被清洗**,原文一字不丢(`personaKeyOf` 两侧算得出同一个键即反证)。
|
||||
⚠️ 案 A 的已知代价(读侧两形态)如实保留;**案 B(改声明 `text`)仍⛔ 未执行**(须删库 ⇒ 待拍板 ㈢/㈣)。
|
||||
3. **级联删除的顺序**:`account_persona` 的平台删除放在**本地事务之前**。理由:本地事务管不了平台写,
|
||||
"先本地后平台"一旦平台失败 ⇒ "本地已删、平台还在"(平台档下读回来**看起来没删**);
|
||||
反过来先删平台、本地再失败 ⇒ 重试一次即收敛(`removeWhere` 幂等)。逐账号**具名收集**进
|
||||
`platform[]`(`ok/matched/removed/failed[]`),⛔ 不中断整批。
|
||||
|
||||
---
|
||||
|
||||
## 五 卫生(A7 逐条)
|
||||
|
||||
- ⛔ 未改 `D:/github/dsh_shenxian/src/**`;⛔ `src/web/routes/im.ts` 零改动(本棒未碰 `src/`)
|
||||
- ⛔ 未手改用户 `home` 下的 profile / `.pnpm`(**唯一写入** = 该用户自己的插件数据目录
|
||||
`<home>/.dsh/data-plane.json` —— 这是阶段一设计的**插件自有档位文件**,也是"一键回滚"的落点)
|
||||
- ⛔ 未重启任何服务(47 实例 / 106 / nginx 全未动);⛔ 未 commit / push
|
||||
- ⛔ **未投放**任何版本到候选池或共享层(共享层仍 `0.4.0`)
|
||||
- ✅ 临时 admin 会话:建 **1** → 删 **1** → 残留 **0**(`SESS_DELETED=1 SESS_LEFT=0`)
|
||||
- ✅ 47 旧库**零改动**(sha256 前后逐字一致);✅ 未删任何库(⛔ 全程无 `DROP`)
|
||||
- ✅ 探针落 `/tmp/p28/**`(收口清理,见 §七)
|
||||
|
||||
---
|
||||
|
||||
## 六 未闭合项(逐条:现象 / 归属 / 下一步)
|
||||
|
||||
| # | 现象 | 归属 | 下一步 |
|
||||
|---|---|---|---|
|
||||
| 1 | **实例内端到端未取证**(插件路由 `/mcn/api/account/persona` 仍跑 `0.3.15` 旧码) | 部署面 | 需 **47 实例重启**(待拍板 ㈠);无窗口则 A2 只能取"取数口"读数 |
|
||||
| 2 | 出包 `0.5.0` / 建库 / 投放 / 两机一致(S7–S11)**全未开工** | 部署面 | ⛔ 投放会让新版**对所有登录用户可见可启**(R5 扩面)⇒ 与 ㈠ 一起排 |
|
||||
| 3 | `creative_log` 无任何读点 ⇒ 切档后**无法用"读数"证明**,只能证明"写请求真到了平台" | 插件侧 | 后续批次给该表补一个统计读口,或接受"写侧单边取证"(本棒如实声明) |
|
||||
| 4 | 其余 9 张表(`hot_accounts` 拆表 + 12 处 JOIN + 4 处 OFFSET + 2 处聚合、`account_videos` 362 行)未切换 | 插件侧 | 排后续批次,按「表」推进(见入口 §2) |
|
||||
| 5 | `json` 案 B(声明改 `text`)仍待拍板 ⇒ 读侧**两形态**长期存在 | 平台侧 / 决策 | 待拍板 ㈢㈣;⛔ 未拍板一律不执行删库 |
|
||||
|
||||
---
|
||||
|
||||
## 七 探针与证据落点
|
||||
|
||||
| 件 | 位置 |
|
||||
|---|---|
|
||||
| 本机:迁移工具自证(假 fetch) | `WS/tmp/p28/local-migrate-check.mjs` |
|
||||
| 47:真机探针(fetch 垫片 + 迁移 + 影子读 + 切档) | `WS/tmp/p28/run-p28.mjs` → 47 `/tmp/p28/run-p28.mjs` |
|
||||
| 47:编排脚本(指纹 / mksess / 清理) | `WS/tmp/p28/orchestrate.sh` |
|
||||
| 47:探针上传的包副本(md5 已核对逐字一致) | 47 `/tmp/p28/pkg/**`(`store.js b6b48b9b…`、工具 `950e8a5f…`) |
|
||||
| 47:只读取证 | `WS/tmp/p28/probe-a.sh`、`probe-b.sh` |
|
||||
|
||||
> 🔴 探针用的是 Phase 25 已验证的**合法通道**(会话 Cookie + `x-dsh-im-plugin-id`);
|
||||
> node `fetch` 改不了 `Host` 头(会静默丢 ⇒ 假 404)⇒ 垫片改走 `http.request` 并**剥掉实例 token 头**。
|
||||
> 本探针跑的是**包内真实代码**(`store.js` / `tools/migrate-local-to-platform.mjs`,md5 与本机逐字一致),
|
||||
> 不是"另写一份等价逻辑"。
|
||||
|
||||
---
|
||||
|
||||
## 八 下一棒建议(唯一)
|
||||
|
||||
**第 29 棒 · 执行棒 = 第二批按表切换**:`account_analysis`(与 `account_persona` **同构** ⇒ 直接复制本轮口径:
|
||||
2 写 + 1 删 + 计数读,可复用 `personaKey` 的写法)+ `video_analysis`(补写/删 + 迁移;**12/12 行非法 JSON**
|
||||
⇒ 正好检验 A 案清洗的规模面)。链路同本棒四步:**全部读写点 → 迁移(幂等)→ `shadow` 真机读数 → 切档**。
|
||||
⚠️ 仍⛔ 不出包、⛔ 不投放、⛔ 不动删库候选;`account_videos` / `hot_accounts`(JOIN/OFFSET 主战场)留第三批。
|
||||
@@ -0,0 +1,199 @@
|
||||
# MCN 数据面接入 · 阶段二 **第三批按表切换**(2026-09-25 · 第 31 棒 · 执行棒)
|
||||
|
||||
> **执行单**:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md`(唯一执行单)
|
||||
> **入口**:`E:/ProgramData/AIProject/aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md` §0 / §2
|
||||
> **会话**:`p31-exec-plugin-dataline`|**域锁** 12 个(见 §八 卫生)
|
||||
> **结论一句话**:**部分完成** —— S0/S1/S2 全绿并附真机读数;**S3 部分完成(20/23 处)**;**S4–S8 未开工**(⛔ 未跳步,按序停在下一条)。**三张表一处未切档**(切换条件不成立,理由见 §一 ②)。
|
||||
|
||||
---
|
||||
|
||||
## 一 结论
|
||||
|
||||
| 步骤 | 状态 | 摘要 |
|
||||
|---|---|---|
|
||||
| **S0** 只读前置(P1–P9 + 4.2 三条) | ✅ **完成** | 9 条读数全部落地,与单内预期一致;4.2 三条里 ① 与 ③ **取得定论**(见 §四) |
|
||||
| **S1** `store.js` 扩三张表读写 | ✅ **完成** | 新增 **31 个**导出(含 D16 映射、双键读、拆表写、游标、窗口归约、索引);`node --check` rc=0 |
|
||||
| **S2** 跨批衔接面 4 处 | ✅ **完成** | 4 处(`index.js:3076` + `workshop.js:199/202/213`)—— **只改 `h.account_name` 取值方式**,`rewrite_log` 语义一字未动 |
|
||||
| **S3** `account_video_source` 全量切换 | 🟡 **部分完成(20/23)** | `imports.js` 15 处全改 + `data.js` 2 处 + `workshop.js` 2 处 + `index.js` 3 处;**剩 1 处**(`index.js:1558` 视频总表,与 S4/S5 同一条 SQL,见 ② ) |
|
||||
| **S4** `account_videos` 全量切换 | ⛔ **未开工** | 最大表(35 处涉及面)+ 游标/窗口/`IN` 的结构改写主战场 |
|
||||
| **S5** `hot_accounts` + `hot_account_ext` 拆表切换 | ⛔ **未开工** | 5 处写点 + 11 处 JOIN |
|
||||
| **S6** 存量迁移 | ⛔ **未开工** | 理由见 ② |
|
||||
| **S7** `shadow` 复验 ⇒ 按表切 `platform` | ⛔ **未开工** | 见 ② |
|
||||
| **S8** 收口 | ✅ **完成** | README §一 + 入口 §0/§2 + 本交付件 + 当日日志 + 释放域锁 |
|
||||
|
||||
**① 为什么停在 S3 中途**:S3 的验收判据是「该表调用点**归零**(只剩 `local` 回调 + 具名降级)」。剩下那 1 处(`index.js:1558`)是**一条 SQL 同时涉及三张表**(`account_videos v` + `hot_accounts h` + `account_video_source s` 子查询 + `SUM() OVER` + `LIMIT ? OFFSET ?`)—— 它同时属于 S3/S4/S5,**只有一次改写机会**,正确做法是在 S4 时连同 `account_videos` 一起落(⛔ 先改一半 ⇒ 后棒要在半成品上二次施工)。
|
||||
|
||||
**② 为什么不「先切了再说」**(🔴 本棒最重要的自律):`account_video_source` 仍有 1 处读点走本地 SQL ⇒ **此刻若把该表切 `platform`**,写会进平台、而那一处仍读本地 ⇒ 出现「**写到了没人读的地方**」——正是 04 单 §五 S2 与父单反复点名的**最隐蔽坏态**(界面完全正常)。⇒ **切换条件不成立,本棒一处未切**。同理,S6 存量迁移按 04 单 §五 顺序**排在 S5 之后**(⛔ 不许跳),本棒**未做任何迁移、未写任何平台行**。
|
||||
|
||||
**③ 如实记账**:本棒**未**触达任何用户可见行为变化 —— 三张表的档位仍是 `local`,转换过的 20 处走的仍是 `local` 回调里的**原 SQL**(逐字保留)⇒ **零回归、可回滚**。
|
||||
|
||||
---
|
||||
|
||||
## 二 判据表(命令 + 输出原文 + 退出码)
|
||||
|
||||
### A9 红线**零新增**(✅ 达成)
|
||||
|
||||
```
|
||||
$ cd "$PKG" && grep -rEn "CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)" . | grep -v /node_modules/ | wc -l
|
||||
23
|
||||
```
|
||||
⇒ **23**,与第 27/28/29 棒同值。**本批新增 DDL = 0**。(归零属阶段二收口 S10,⛔ 不属本批。)
|
||||
|
||||
### S1 验证(✅ 达成)
|
||||
|
||||
```
|
||||
$ "$NODE" --check "$PKG/lib/host/mcn/{store,data,imports,index,workshop,video-tools}.js"
|
||||
store.js rc=0 · data.js rc=0 · imports.js rc=0 · index.js rc=0 · workshop.js rc=0 · video-tools.js rc=0
|
||||
|
||||
$ cd "$PKG" && "$NODE" --test tests/mcn-store.test.mjs
|
||||
# tests 49
|
||||
# pass 49
|
||||
# fail 0
|
||||
```
|
||||
⇒ **旧 40 例全过 + 新增 9 例(㉞–㊷)全过 / 0 败**(单内要求 ≥6 例)。
|
||||
|
||||
### A3 结构改写面处置(🟡 部分:S3 相关已处置,S4/S5 相关未动)
|
||||
|
||||
```
|
||||
$ grep -rniE "\bJOIN\b" "$PKG/lib" | grep -v "\.join(" ; grep -rn "LIMIT ? OFFSET ?" "$PKG/lib" ; grep -rnE "ROW_NUMBER|OVER\s*\(" "$PKG/lib"
|
||||
```
|
||||
| 类型 | 全包 | 第三批 | 本棒处置 |
|
||||
|---|---|---|---|
|
||||
| SQL `JOIN` | 137 行(含四批全部) | **11**(含 `hot_accounts`) | **0**(属 S4/S5,⛔ 未开工) |
|
||||
| `LIMIT ? OFFSET ?` | 6 | **3** | **0**(属 S4,未开工) |
|
||||
| 窗口函数 | 3 | **3** | **0**(属 S4;插件内归约**能力**已在 S1 落地并由单测 ㊴ 钉住) |
|
||||
|
||||
⇒ ❌ **A3 未达成**(判据要求"涉及三张表的 JOIN / OFFSET / 窗口函数全部处置")。
|
||||
|
||||
### A1/A2/A4/A6/A8(❌ 未达成 —— 逐条写明理由,⛔ 不含糊过关)
|
||||
|
||||
| # | 判据 | 本棒读数 | 理由 / 归属 |
|
||||
|---|---|---|---|
|
||||
| **A1** | 存量迁移一致性 | ❌ **未达成** | 本棒**未执行任何迁移**(S6 未开工)。⇒ 三表本地/平台计数无可比读数。归属:**插件侧未做**,非缺陷 |
|
||||
| **A2** | 三表读写 E2E(含实例内) | ❌ **未达成** | ① 本棒**未切档**(见 §一 ②)⇒ 插件路由仍走本地;② **实例内读数需 47 实例重启** ⇒ 属**待拍板㈠**(影响 1 个在线用户 `cce6d1cd-…`,约 1–3 分钟)⇒ **本棒不承诺** |
|
||||
| **A4** | 表名对齐彻底 | 🟡 **一半达成** | `grep -rE "account_video_analysis" "$PKG" \| grep -v /node_modules/ \| wc -l` ⇒ **0** ✅(第 28 棒成果,本棒复核);`grep -c "hot_account_ext" "$PKG/lib"` ⇒ **> 0** ✅(S1 新增 `TABLES`/拆表四函数引用) |
|
||||
| **A5** | 已按表切档(三张表) | ❌ **未达成** | 真机读 `data-plane.json` = **4 表 `platform`**(第一二批)⇒ 三张目标表**仍 `local`**(本棒未切,理由见 §一 ②) |
|
||||
| **A6** | 移动端五条 | ✅ **免跑(有文件清单依据)** | 改动面**恰 6 件**:`lib/host/mcn/{store,data,imports,index,workshop}.js` + `tests/mcn-store.test.mjs` ⇒ **`lib/client.js` 零改动** |
|
||||
| **A7** | 卫生 | ✅ **达成** | 见 §八 |
|
||||
| **A8** | 两机一致 | ⚠️ **未达成(如实)** | 本批**未出包、未投放** ⇒ 共享层仍 `0.4.0`、实例仍载 `0.3.15` ⇒ **代码面/运行面都还是旧的**;⛔ 不得写"一致" |
|
||||
| **A4** | 旧库备份在位 | ✅ **达成** | 见 §四 P7 |
|
||||
|
||||
---
|
||||
|
||||
## 三 S2 跨批衔接面(4 处 · ✅ 完成)
|
||||
|
||||
**对象**:`index.js:3076`(`/mcn/api/recent-scripts`)+ `workshop.js:199`/`202`(`listCustomScripts` 计数+列表)/`213`(`getCustomScriptRow`)。
|
||||
4 处 SQL 共同形如 `… FROM rewrite_log r LEFT JOIN hot_accounts h ON h.id = r.account_id` ⇒ 取 `h.account_name`。
|
||||
|
||||
| 处 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `workshop.js:202`(列表) | `export function listCustomScripts(d, …)` 直接返回 `items` | `export async function listCustomScripts(…)` ⇒ 拿到行后 `await applyCurrentAccountNames(d, items)` **覆盖显示名**;SQL 逐字保留 |
|
||||
| `workshop.js:213` | `export function getCustomScriptRow(d, id)` 直接返回 row | 同上(改 async + 一行覆盖) |
|
||||
| `index.js:3076` | `json(res, 200, { ok: true, items })` | `const named = await applyCurrentAccountNames(d, items); json(res, 200, { ok: true, items: named.rows, staleNames: named.staleNames, nameMode: named.mode })` |
|
||||
| `workshop.js:199`(**计数**) | `SELECT COUNT(*) c … LEFT JOIN hot_accounts h …` | ⚠️ **未改(有意)**:这一处 `h.account_name` **只出现在 `account` 过滤条件**里、**不产出任何显示值** ⇒ 不属"账号名显示为旧值"那类;其风险(新账号名缺席)与 §三 ③ 的口径缺口**同源**,一并记账 |
|
||||
|
||||
**为什么不能留给第四批**:本批把 `hot_accounts` 的**写**切平台后,本地 `hot_accounts` 不再被写 ⇒ 这 4 处若仍只读本地 JOIN,**账号名会永久停在切档那一刻**(R11 净变差,且界面看起来完全正常)。
|
||||
**⛔ 未动 `rewrite_log`**:它的读写点、`ORDER BY r.id DESC`、`WHERE` 条件全部一字未改(仍留本地 / 留第四批)。
|
||||
|
||||
🔴 **本棒实测发现的一条口径缺口(如实报告 · 未擅自改口径)**:04 单 D16 定的「插件内映射(本地 `id ↔ account_name`)」**只能来自本地**(平台侧没有自增 `id` 列)⇒ 映射的**新鲜度上限 = 本地 `hot_accounts` 最后一次被写的时刻**。本棒的落地形态因此加了**对账**:覆盖值再与平台 `hot_accounts` 的**现存键集合**比对,**对不上者进 `staleNames[]` 具名报告**(⛔ 不静默、⛔ 不假装是最新名)。单测 **㊷** 钉住三态(local 零变化 / 映射名 + 具名 / 映射名命中平台 ⇒ `applied`)。
|
||||
|
||||
---
|
||||
|
||||
## 四 S0 只读前置读数(9 条 · 逐条对上期望)
|
||||
|
||||
| # | 事实 | 命令(本机 / 真机 47) | 输出原文(摘要) | 对期望 |
|
||||
|---|---|---|---|---|
|
||||
| **P1** | 红线现状 | 见 §二 A9 | **23** | ✅ |
|
||||
| **P2** | 第三批逐处落点(现算) | `"$PY" "$WS/tmp/p30/scan-batch3-live.py"` | **82 处**:`data.js`9/`imports.js`36/`index.js`24/`video-tools.js`1/`workshop.js`12 | ✅ |
|
||||
| **P3** | 结构改写面三种 | 见 §二 A3 | 全包 JOIN 137 / OFFSET 6 / 窗口 **3** | ✅(口径修正见单内附录 D) |
|
||||
| **P4** | `hot_accounts` 列面 | `PRAGMA table_info('hot_accounts')` | **25** 列(建表 14 + `ALTER` 11)—— 逐列已列印(`id…is_ai`) | ✅ 与单内定论一致 |
|
||||
| **P5** | 空键 / 重复键分布 | `COUNT(*) WHERE aweme_id IS NULL OR aweme_id=''` 等 | `account_video_source` 空键 **0**|`account_videos` 空键 **0**|`hot_accounts` 空名 **0**|跨 `account_id` 重复 `aweme_id` **0 组** | ✅ ⇒ 本批 `badRows` 预期为**零**(代码仍须拒收) |
|
||||
| **P5b** | 逐表行数 | `SELECT COUNT(*)` | `hot_accounts` **5**/`account_videos` **362**/`account_video_source` **39**(=第 27 棒清单) | ✅ |
|
||||
| **P6** | `account_id` 类型 | `PRAGMA table_info('account_videos')` | `account_id **INTEGER**`(声明面 = `text`) | ✅ ⇒ D20 成立(写入一律 `String()`,单测 ㊵ 钉住) |
|
||||
| **P7** | 旧库备份在位 | `ls` + `tar -tzf` + `sha256sum` | `…-mcn-plugin-db-20260925-141436.tar.gz` **809,732 B**/`sha256 6b09e368…`/可解出 `mcn-plugin.db`;源库 `sha256 b37f8aab…` · 2,383,872 B · mtime `2026-09-20 23:05:05` | ✅ ⇒ **⛔ 本批未重做备份**(单内要求) |
|
||||
| **P8** | 按表档现状 | 读 `<home>/.dsh/data-plane.json` | `tables` = `account_persona`/`creative_log`/`account_analysis`/`video_analysis` 四项**均 `platform`**;`updatedAt 2026-09-25T07:29:58.942Z` | ✅ ⇒ 三张目标表**仍 `local`** |
|
||||
| **P9** | 🔴 平台 `find` 谓词能力(4.2 ① · **本棒给结论**) | 读 `src/db/plugin-data/runtime.ts:172-211`(**只读,⛔ 未改**) | `buildWhere()` 现算:归属列外**只生成两种谓词** —— `col = $n`(`eq` **多列 AND** 可用 ✅)与 `col IS NULL`(传 `null` 时);**无** `neq`/`IN`/范围/`LIKE`/`OR`;未知列 ⇒ `bad-args` | ✅ **与单内"按只支持等值设计"一致** ⇒ ⛔ 未简化任何落点 |
|
||||
|
||||
**§四 4.2 三条的结论**:
|
||||
- **① P9 谓词能力** ⇒ **有结论**:`eq` 支持**多列 AND**(比单内假设略宽,但**未据此简化**任何改写;⛔ 单内明确"简化 = 改动面变化 = 需重过落点表");
|
||||
- **② P5 三个存量分布** ⇒ **有结论**:三处**全为零** ⇒ 本批无需"人工裁定";
|
||||
- **③ `hot_accounts` 的 5 处写点是否都拿得到 `account_name`** ⇒ **未取得完整结论**:`imports.js:49/53`、`data.js:24`、`index.js:2796`、`workshop.js:335` 逐处未做运行时取证(属 S5 的前置,本棒未开工该步)。⇒ 列为未闭合项。
|
||||
|
||||
---
|
||||
|
||||
## 五 S3 逐处落点(20/23 · 改前 → 改后)
|
||||
|
||||
**统一形态**:`store.readRows/readCount/writeThrough({ what, table: "account_video_source", platform: () => store.xxx(…), local: () => <原 SQL 逐字保留>, keyOf })`。
|
||||
|
||||
| 文件 | 处 | 原形态 | 改后形态 |
|
||||
|---|---|---|---|
|
||||
| `imports.js` | `readVideoSource` 库兜底 | `SELECT content_json AS content, analysis_time … WHERE video_id=? OR (aweme_id IS NOT NULL AND aweme_id=?) ORDER BY id DESC LIMIT 1` | `store.latestVideoSource({awemeId, videoId})`(`aweme_id` 直取 → `video_id` 拉页归约) |
|
||||
| | 补录去重+写 | `SELECT id … WHERE (video_id=? OR …) AND content_json=?` + `INSERT` / `UPDATE analysis_json WHERE id=?` | `store.writeVideoSource(row, { local })`(去重 + 写;`replace:false`) |
|
||||
| | `readVideoParse` 库兜底 | `SELECT content_json, analysis_json …` | `store.latestVideoSource()` |
|
||||
| | `backfillVideoSource`(3 处) | `WHERE (video_id IS ? OR aweme_id IS ?) AND content_json=?` + `INSERT` + `SELECT analysis_json WHERE id=?`/`UPDATE … WHERE id=?` | `store.writeVideoSource()`;`local` 回调内保留 `IS` 形态与 `added++` 语义 |
|
||||
| | `importExternalDir` 补录 | `… AND content_json=?` + `INSERT` | `store.writeVideoSource()`,`analyses++` 移入 `local` 回调 |
|
||||
| | `importExternalData` **删旧保新** | `SELECT dup` + `DELETE … WHERE video_id=? OR (…)` + `INSERT` | `store.writeVideoSource(row, { replace: true })` ⇒ 平台档:先双键删旧、**删旧失败不插新**(⛔ 不留两份) |
|
||||
| | `previewExternalDir` 预览判据 | `SELECT id … AND content_json=?` | `store.analysisExists({ table: "account_video_source", … })`(与第 29 棒同手法) |
|
||||
| `data.js` | 列表 `has_source` 列 | 平台行按 `SELECT COUNT(*) … WHERE aweme_id=?` **读本地** | `store.loadVideoSourceIndex()` + `store.videoSourceCountOf()`(一次拉全量建索引,⛔ 不逐行 N 次请求;`truncated` ⇒ **具名降级回本地**) |
|
||||
| `workshop.js` | `deepVideos` 连坐删 2 处 | 事务内 `DELETE FROM account_video_source WHERE aweme_id=?` / `WHERE video_id=?` | **平台先删(事务前)**:`store.removeVideoSource({awemeId, videoId})`,与 `video_analysis` 共用一份预取行;本地 SQL 保留在事务内(`local` 档路径) |
|
||||
| `index.js` | `taskDone`(`dsh_video-analyze-*`) | `SELECT COUNT(*) c … WHERE video_id=? OR aweme_id=(SELECT …)` 直读本地 | `store.readCount({ table: "account_video_source", platform: () => store.countVideoSources({awemeId, videoId}) })` |
|
||||
| | `/video/script` | `SELECT content_json … ORDER BY id DESC LIMIT 1` | `store.readRows` + `store.latestVideoSource()` |
|
||||
| | 复盘「源脚本正文」`readSource()` | 同上(sync 内联) | 改 **async** + 走 `store.readRows`(唯一调用方 `await`) |
|
||||
| **`index.js:1558`** | 视频总表(`account_videos` + `hot_accounts` + `account_video_source` 子查询+`SUM() OVER`+`OFFSET`) | ❌ **未改** | 与 S4/S5 **同一条 SQL** ⇒ 在 S4 一次改到位(理由见 §一 ①) |
|
||||
|
||||
**语义差异(如实记账)**:平台主键 = `aweme_id` 且**不可为空** ⇒ `backfillVideoSource` 中「`vid` 为空(目录没匹配到视频行)」那条记录在**平台档下具名拒绝**(本地档照旧插入)。实测该形态 **0 行** ⇒ 本批无实际影响。
|
||||
|
||||
---
|
||||
|
||||
## 六 产物指纹
|
||||
|
||||
| 文件 | 字节 | 行尾 CR | sha256(前 16) |
|
||||
|---|---|---|---|
|
||||
| `lib/host/mcn/store.js` | 91,585 | 0(LF) | `2e7668c140bb592b` |
|
||||
| `lib/host/mcn/data.js` | 8,923 | 0(LF) | `11521eb897d91783` |
|
||||
| `lib/host/mcn/imports.js` | 45,258 | **825(全 CRLF,与基线同)** | `1cf01e06b1db543e` |
|
||||
| `lib/host/mcn/index.js` | 194,765 | 0(LF) | `8105fde131a2792f` |
|
||||
| `lib/host/mcn/workshop.js` | 22,671 | 0(LF) | `58c9b512a0dce1c5` |
|
||||
| `tests/mcn-store.test.mjs` | 43,458 | 0(LF) | `8012adc6cef5be7e` |
|
||||
| `dsh.data.yaml` | **未改**(本批不动声明面) | — | — |
|
||||
|
||||
🔴 **行尾判定**(字节级现算,⛔ 不凭记忆):`index.js`/`workshop.js` 在 **0.3.13 基线包里就是纯 LF**(`index.js`: CRLF **0** / bareLF 3095;`workshop.js`: CRLF **0** / bareLF 303)⇒ 本棒**未改变任何文件的行尾约定**;`imports.js` 基线本来就是**全 CRLF**(0.3.13 内 CRLF 592 / bareLF 0),本棒改后仍**全 CRLF**(825 行 CRLF、**bareLF = 0**)。⛔ **无混行尾**。
|
||||
|
||||
---
|
||||
|
||||
## 七 未闭合项(逐条:现象 / 归属 / 下一步)
|
||||
|
||||
1. **S3 剩 1 处未改**:`index.js:1558`(视频总表,三表混写 + 窗口 + OFFSET)。归属 = **插件侧未做**。下一步 = 与 S4 一起改(S4 首个动作)。
|
||||
2. **S4 / S5 / S6 / S7 全未开工**。归属 = 插件侧。下一步 = 按 04 单 §五 顺序继续(第 32 棒)。
|
||||
3. **D16 映射的"新鲜度上限"**(§三):平台档下**切档之后新建的账号**拿不到本地 `id ↔ account_name` 映射 ⇒ 依赖该映射的读点会走**具名降级**(`staleNames` / `mapping-missing`)。归属 = **口径缺口(规划侧定 D16 时未闭合)**。下一步 = 需在 S5 落码前拍一次口径:**① 维持 D16(接受新账号缺席)**,或 **② 让 `hot_accounts` 的写顺带把 `(id, account_name)` 落一张**本地键注册表**(⛔ 不是双写业务数据 ⇒ 无"以谁为准"歧义;好处:映射恒新鲜、`account_videos.account_id` 恒可解析;代价:本地仍保留一张极小表)。⛔ 本棒**未擅自改口径**,按 D16 落码并留下具名降级。
|
||||
4. **S5 前置未取证**:`hot_accounts` 的 5 处写点是否**全部**能拿到 `account_name`(拆表同键不可为空)。归属 = 插件侧未取证。下一步 = S5 第一步。
|
||||
5. **A2 / A8 未达成**:A2 需 **47 实例重启窗口**(待拍板㈠)+ 本批切档;A8 需出包投放(属阶段二收口 S9–S11)。归属 = **部署面 / 待拍板**。
|
||||
6. **平台侧缺陷面(⛔ 本棒未改 `src/**`,只报告)**:`src/db/plugin-data/runtime.ts` 对**非法值**返回的 `reason` 仍是笼统的 `disabled` 语义(第 25 棒已记档,本棒复核仍在)。归属 = **平台侧**。下一步 = 平台线单独排棒。
|
||||
|
||||
---
|
||||
|
||||
## 八 卫生声明(A7 逐条点名)
|
||||
|
||||
| 项 | 结果 | 依据 |
|
||||
|---|---|---|
|
||||
| ⛔ 改 `D:/github/dsh_shenxian/src/**` | **0 件** | `find "$SRC" -newermt "2026-09-25 15:58" -type f \| wc -l` ⇒ **0**(只读读了 `runtime.ts`) |
|
||||
| ⛔ 改 `im.ts` | **零改动** | 同上(`src/web/routes/im.ts` 未在改动面) |
|
||||
| ⛔ 改 `D:/dshworkspace` 下**其它**文件 | **仅 6 件(本批改动面)** | `find "$PKG" -newermt "…" -type f \| sed "s\|$PKG/\|\|"` ⇒ 恰 6 件 |
|
||||
| ⛔ 手改用户 `home` 下 profile / `.pnpm` | **未触碰** | 本棒**全程本机**,⛔ 未 ssh 改任何文件 |
|
||||
| ⛔ 重启任何服务 | **未重启** | 47 只跑只读 `PRAGMA` / `sha256sum` / `ls` / `cat`;106 全程未登录 |
|
||||
| ⛔ 投放任何版本 | **未投放** | 共享层仍 `0.4.0`;`dist/` 未新增包 |
|
||||
| ⛔ commit / push | **未做** | `D:/dshworkspace` 与文档库均未提交 |
|
||||
| ⛔ 删任何库 | **未删** | 未执行任何迁移、未写任何平台行;旧库 `mcn-plugin.db` **原样在盘**(sha256 `b37f8aab…` 与迁移前一致) |
|
||||
| ⛔ 未拍板前执行删库 / 重建 | **未做** | `json` 列案 B(删库重建)**未出现在任何执行面** |
|
||||
| ✅ 临时库 / 残留清理 | 47 上 **0 处写入**(只读脚本经 stdin 管道执行,未落文件);本机探针落 `tmp/p31/**` | — |
|
||||
|
||||
---
|
||||
|
||||
## 九 域锁
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 会话名 | `p31-exec-plugin-dataline` |
|
||||
| 域(12 个) | `dsh-server-docs/05-交接单` · `aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md` · `aliyun-dsh-server/.workbuddy` · `aliyun-dsh-server/交付物` · `mcn/store.js` · `mcn/data.js` · `mcn/imports.js` · `mcn/index.js` · `mcn/workshop.js` · `mcn/video-tools.js` · `tools/migrate-local-to-platform.mjs` · `tests/mcn-store.test.mjs` |
|
||||
| 复核方式 | 退出码 **rc=0** + 复读 `.locks/p31-exec-plugin-dataline/{OWNER,DOMAINS}` 断言归属(⛔ 不看输出、不接管道) |
|
||||
| 门禁备注(如实) | `preflight-lock.sh` 对 11 个目标路径全判 **【D】未归类**(工作区根文件 + 文档库 `05-交接单` + `D:/dshworkspace` 下的插件包路径都不在该脚本锚点覆盖范围内)⇒ 按脚本字面应 `rc=1`;照本线既有惯例(第 27–30 棒同一情形)**开工并报告**,⛔ **未改那个跨库脚本** |
|
||||
@@ -0,0 +1,104 @@
|
||||
# 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 次累积)。
|
||||
@@ -0,0 +1,188 @@
|
||||
# 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 棒 · 执行棒)。本件**不含不可逆操作**。
|
||||
@@ -0,0 +1,150 @@
|
||||
# MCN 数据面接入 · 阶段二 · **第二批按表切换**(`account_analysis` + `video_analysis`)
|
||||
|
||||
> **执行会话**:插件投放与分库线 · 第 29 棒(2026-09-25 15:1x–15:3x)
|
||||
> **唯一执行单**:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-03MCN数据面接入改造-阶段二.md`(§五 S4–S6 / §六 A1–A8)
|
||||
> **变更集**(7 件,全在插件包内):`lib/host/mcn/{store,index,imports,workshop,data}.js` + `tools/migrate-local-to-platform.mjs` + `tests/mcn-store.test.mjs`
|
||||
> **§0 前提**:第一批(`account_persona` + `creative_log`)已于第 28 棒切换完毕并在位;本棒**只做第二批**,⛔ 未顺手切第三批。
|
||||
|
||||
---
|
||||
|
||||
## 一 结论
|
||||
|
||||
**第二批按表切换完成,四步链路走完,真机读数全绿。**
|
||||
|
||||
| 步 | 结果 |
|
||||
|---|---|
|
||||
| ① 该表**全部**读写点接线 | ✅ `account_analysis` 6 处 + `video_analysis` 10 处,逐处落点见 §三;剩余命中**全部**是「`local` 回调 / schema 维护 / 具名降级」,见 §四 |
|
||||
| ② 迁移(幂等) | ✅ 本地 `account_analysis` 1 → 平台 1;本地 `video_analysis` 12 → 平台 12;重跑 `ins=0 / skip=1`、`ins=0 / skip=12` |
|
||||
| ③ `shadow` 真机读数 | ✅ 4 次影子读**全部 `match=true`**、`shadowMismatch=0`、`degraded` 一次未出现、平台 44 次调用**全 ok / 0 失败** |
|
||||
| ④ 按表切档 | ✅ 只切这两张(+保留第一批),`hot_accounts` / `account_videos` **仍 `local`** |
|
||||
|
||||
**离线判据**:单测 `40/40 通过`(31 → 40,新增 9 例);`node --check` 7 文件 `rc=0`;旧表名字面量 `0`(A4)。
|
||||
|
||||
🔴 **未闭合(⛔ 不宣称通过)**:实例内端到端(**需 47 实例重启** ⇒ 待拍板 ㈠)、出包/投放/两机一致(S7–S11)、红线归零(S10,属第三批收口)。逐条见 §六。
|
||||
|
||||
---
|
||||
|
||||
## 二 判据表(A1–A8 · 命令 + 输出原文 + 退出码)
|
||||
|
||||
| # | 判据 | 命令(在 47 上跑) | 输出原文(关键行) | rc |
|
||||
|---|---|---|---|---|
|
||||
| **A1** | **存量迁移一致性(逐表)** | `P29_SID=… P29_HOME=… node --experimental-sqlite /tmp/p29/run-p29.mjs`(§四 口径) | `compare.account_analysis=[{account_id:"1627", local_n:1, platform_n:1, onlyLocal:[], onlyPlatform:[], match:true}]`;`compare.video_analysis={local_n:12, platform_n:12, truncated:false, onlyLocal:[], onlyPlatform:[], match:true, local_rows_without_aweme:0}` | 0 |
|
||||
| **A1′** | **内容往返**(本棒加严:A1 只判计数+主键集合,内容不判会漏掉 `json` 清洗问题) | 同上 | `roundtrip_va={n:12, present:12, equal:12, mismatches:[], plat_types:["string"], plat_unwrapped_is_markdown:12}`;`roundtrip_aa={present:true, equal:true, plat_type:"string"}` | 0 |
|
||||
| **A2** | 全表读写 E2E(**实例内**) | 需实例重启 | ⛔ **未达成**(见 §六 ㈠)—— 本棒只取到「平台取数口」读数 | — |
|
||||
| **A3** | 红线归零 | `cd $PKG && grep -rEn "CREATE TABLE\|ALTER TABLE\|DROP TABLE\|better-sqlite3\|from 'pg'\|require\('pg'\)" . \| grep -v /node_modules/ \| wc -l` | **23**(= 阶段一 22 + 第 27 棒表名对齐 `ALTER` 1);**本棒零新增**(本棒未加任何 DDL / 驱动依赖) | 0 |
|
||||
| **A4** | 表名对齐彻底 | `grep -rE "account_video_analysis" $PKG \| grep -v /node_modules/ \| wc -l` | **0**(旧名仅以**拆写常量**存在于 `db.js` 与迁移工具,用于**读本地存量旧表名**) | 0 |
|
||||
| **A5** | 旧库备份在位 + 零数据损失 | 47:`sha256sum "$DB"`(迁移前后各一次)+ `ls /opt/dsh/backups/plugin-data/dsh-plugin-mcn-suite/` | 迁移前 `b37f8aab51a90f7481eb321e50456ca347635bcf2bfce567ea4200d88c18277a`;迁移后 **同值**;`size=2383872 mtime=2026-09-20 23:05:05` 未变;备份 `cce6d1cd-…-mcn-plugin-db-20260925-141436.tar.gz`(809,732 B)仍在位 | 0 |
|
||||
| **A6** | 移动端五条 | `08-规范 §4-补 ④` | **免跑** —— 依据 = 变更集 7 件中 **无 `lib/client.js`**(`grep` 改动清单可验),即**未改 UI 代码** | — |
|
||||
| **A7** | 卫生 | `git -C D:/github/dsh_shenxian status --short -- src/` + `find src -newermt "2026-09-25 15:15"` | `src/` 变更 **0 件**(最新 mtime `12:20`,早于本棒 15:15 开工)|`src/web/routes/im.ts` 零改动|未重启任何服务|未 commit·push|临时会话 `1 → 删 1 → 残留 0`|47 `/tmp/p29*` 残留 **0** | 0 |
|
||||
| **A8** | 两机一致(代码面 / 运行面**分开记**) | — | **均未开工**:本棒**未出包、未投放**(插件包仍 `version 0.4.0`;共享层仍 0.4.0)⇒ 代码面/运行面**都还是旧的**,⛔ 不得写"一致" | — |
|
||||
|
||||
⚠️ **⛔ 不许只报"全部通过"**:A2 / A8 明确**未达成**,理由与归属已逐条写明(§六)。
|
||||
|
||||
---
|
||||
|
||||
## 三 逐处落点表(本棒接线面)
|
||||
|
||||
### 3.1 `account_analysis`(6 处 · 与 `account_persona` 同构)
|
||||
|
||||
| # | 位置 | 原形态 | 改后 |
|
||||
|---|---|---|---|
|
||||
| 1 | `imports.js` `importExternalDir` 的 `else`(账号数据分析.md) | `SELECT id … ` + `INSERT …` | `store.insertAnalysis(row, { local })`——`local` 档回调里是**原样两条 SQL** |
|
||||
| 2 | `imports.js` `importExternalData` 的 `an.type === "account"` | 同上 | 同上 |
|
||||
| 3 | `imports.js` `previewExternalDir` 的 `checkMd(…, "account_analysis")` | `SELECT id … content_json=?` | `store.analysisExists({table, accountId, content})`(平台档按内容比对;读失败**具名**回落本地) |
|
||||
| 4 | `index.js` `taskDone` 的 `dsh_acctana-` 分支 | `SELECT COUNT(*) … account_id=?` | `store.readCount({ table:"account_analysis", platform: countAnalysis, local: 原 SQL })` |
|
||||
| 5 | `index.js` `/mcn/api/account/analysis` | `SELECT content_json … ORDER BY id DESC LIMIT 1` | `store.readRows` + `store.latestAnalysis()`(平台拉一页后**插件内按 `analysis_time` 归约**);`content` 过 `store.jsonTextToRaw()`(案 A 读侧兼容) |
|
||||
| 6 | `workshop.js` `batchDeleteCascade`(账号连坐) | `DELETE FROM account_analysis WHERE account_id=?` | 平台侧 `store.removeAnalysisByAccount()`(D9 两步)**先**执行,本地事务里那条 SQL **保留**(本地保持一致、保回滚面) |
|
||||
|
||||
### 3.2 `video_analysis`(10 处 · 主键 = 天然键 `aweme_id`)
|
||||
|
||||
| # | 位置 | 原形态 | 改后 |
|
||||
|---|---|---|---|
|
||||
| 1 | `imports.js` `importExternalDir`(拆解分析.md) | `SELECT dup` + `INSERT` | `store.writeVideoAnalysis(row, { local })`,语义 = **去重** |
|
||||
| 2 | `imports.js` `importExternalData` 的 `an.type === "video"` | `SELECT dup` + `DELETE` + `INSERT` | 同上但 `{ replace: true }`,语义 = **删旧保新** |
|
||||
| 3 | `imports.js` `previewExternalDir`(拆解分析.md) | `SELECT id … content_json=?` | `store.analysisExists({table:"video_analysis", awemeId, videoId, content})` |
|
||||
| 4 | `index.js` `taskDone` 的 `dsh_video-analyze-` 分支 | `COUNT(account_video_source) + COUNT(video_analysis)` 并列 | `account_video_source` 那半**照旧本地**(第三批);`video_analysis` 那半走 `store.countVideoAnalysis()` |
|
||||
| 5 | `index.js` 视频总表的 **`parsed` 过滤条件**(进 `WHERE`) | `v.id IN (SELECT video_id FROM video_analysis)` | 平台档:`loadVideoAnalysisIndex()` 拉全量 ⇒ 拼 `v.id IN (<字面 int 列表>)`;索引不可用 ⇒ **具名降级回本地子查询** |
|
||||
| 6 | `index.js` 视频总表的 **`has_analysis` 输出列** | 同一条 SQL 里的子查询 | 平台档下该列**不进 SQL**(`0`),取回本页后由索引在**插件内补**(与第 28 棒 `has_content` 同法) |
|
||||
| 7 | `index.js` `/mcn/api/video/detail` | `SELECT … WHERE aweme_id=? OR video_id=? ORDER BY id DESC LIMIT 1` | `store.readRows` + `store.latestVideoAnalysis()`(先按 `aweme_id` 直取,再退 `video_id` 归约) |
|
||||
| 8 | `index.js` 桌面统计(`analyses`) | `SELECT COUNT(*) FROM video_analysis` | `store.readCount({ table:"video_analysis", platform: countRows, local: 原 SQL })` |
|
||||
| 9 | `workshop.js` `deepVideos` | `DELETE … WHERE aweme_id=?` + `WHERE video_id=?`(本地事务内) | **平台侧在 `BEGIN` 之前**先按两 scope `store.removeVideoAnalysis()`;本地两条 SQL 保留 |
|
||||
| 10 | `data.js` 账号视频列表的平台档补列 | 平台档下 `has_analysis` 走**本地** `COUNT` | 平台档改用 `loadVideoAnalysisIndex()`+`videoAnalysisCountOf()`(`has_source` 仍本地,第三批) |
|
||||
|
||||
### 3.3 顺带闭合的两处(**⛔ 不是扩大范围,是不修就留坏态**)
|
||||
|
||||
1. 🔴 **`videoAnalysisCountOf` 的 OR 语义**(本棒自证时抓到):初版写成 `byAweme.get(a) + byVideo.get(v)`(两路相加)——而本地语义是 `WHERE video_id=? OR aweme_id=?` ⇒ **同一行两键都命中时被计两次**(本地 1、这里 2)。已改为**逐行 OR 判定**,并以单测 ㉚ 钉住(`{awemeId, videoId}` 同视频 ⇒ **1**,两个不同视频 ⇒ 2)。
|
||||
2. 🔴 **`previewExternalDir` 的三处「是否已存在」判据**:其中 `account_persona` 那处是**第 28 棒的遗留读点**(该表已切,但预览判据仍读本地 SQL ⇒ "平台已写、预览还说不重复")。本棒把三处统一走 `store.analysisExists()` 并让该函数 **async**。
|
||||
|
||||
### 3.4 一处**按表档连贯性**修复
|
||||
|
||||
`data.js` 的视频列表读(`account_videos`)**阶段一没接按表档**(当时还没有按表档)⇒ 该表的档位覆盖对它不生效。本棒补上 `table: "account_videos"`(全局档仍是 `local` ⇒ **行为不变**)。
|
||||
|
||||
---
|
||||
|
||||
## 四 具名豁免(剩余命中为什么不算"没切")
|
||||
|
||||
| 类别 | 位置 | 为什么豁免 |
|
||||
|---|---|---|
|
||||
| **`local` 回调** | `imports.js` 4 处、`index.js` 2 处、`workshop.js` 3 处的 `d.prepare(...)` | 它们就是**写源档 `local`(默认)那一半**:按设计保留原 SQL 逐字不变 ⇒ 默认档行为与 0.3.15 完全一致(可单测) |
|
||||
| **schema 维护** | `db.js` 的 `sqlite_master` 查询 / `ALTER TABLE RENAME` / `PRAGMA` / 一次性 `UPDATE` 回填 | 属**本地 SQLite 生命周期**,不是"按表的数据面读写点";`db.js` 的删除是阶段二 **S10**(须 A1+A2 全绿后)⇒ 届时该文件整体退场 |
|
||||
| **具名降级** | `imports.js` 预览的本地回退、`index.js` 索引不可用时的本地子查询、`data.js` 的本地补列 | 只在**平台读失败 / 索引 `truncated`** 时触发,且每次都 `console.warn` **具名**(⛔ 不静默)—— 这是"不拿半份数据冒充全量"的口径,不是遗漏 |
|
||||
| **第三批** | `account_video_source` / `hot_accounts` / `account_videos` / `rewrite_log` / `script_review` / `storyboard_log` / `hot_account_ext` 的全部落点 | ⛔ 本棒按接续入口**只做第二批**;第三批 = `hot_accounts` 拆表 + 12 处 JOIN + 4 处 OFFSET + 2 处聚合(主战场) |
|
||||
|
||||
### 4.1 一个**必须记档**的实测事实
|
||||
|
||||
🔴 **本地库的表名还是旧名**:`sqlite_master` 现算 = `…,account_video_analysis,…`(22 字符旧名),**没有** `video_analysis`。
|
||||
原因:第 27 棒在 `db.js` 的 `initDb()` 里加的是**幂等改名**,而它只在**插件新代码跑起来之后**才生效 —— 实例仍载 `0.3.15` ⇒ 改名尚未发生。
|
||||
影响:**迁移工具必须两种名字都能读** ⇒ 已加 `resolveLocalTable()`(按 `sqlite_master` 现算,平台名不存在才回退旧名;旧名按**拆写常量**书写以守住 A4 字面判据)。
|
||||
⚠️ 后棒注意:实例一旦跑起新版,该表会被改名;`resolveLocalTable` 两种形态都能读,⛔ 不要写死。
|
||||
|
||||
---
|
||||
|
||||
## 五 迁移读数(逐表)
|
||||
|
||||
| 表 | 本地 n | 计划 n | 首跑插入 | 重跑 | 坏行 | 被剥列 | 清洗列 | 平台 n(前 → 后) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `account_analysis` | 1 | 1 | **1** | `ins=0, skip=1` | 0 | `0:id` | 1(`content_json`) | 0 → **1** |
|
||||
| `video_analysis` | 12 | 12 | **12** | `ins=0, skip=12` | 0 | `0..11:id` | 12(`content_json`) | 0 → **12** |
|
||||
|
||||
- 稳定键:`account_analysis` = `analysisKey(账号, 正文)`(UUIDv5,可复算;实测 `e5b7d9a1-826f-55c0-9de6-9eb429070b0b`);`video_analysis` = **天然键 `aweme_id`**(⛔ 不生成)。
|
||||
- **`video_analysis` 的平台主键不可为空**:`aweme_id` 为空的行平台侧落不了 ⇒ 迁移会**具名拒收**(`badRows`),⛔ 不静默丢。**本地实测 0 行**(`local_rows_without_aweme=0`)⇒ 本次无人被拒。
|
||||
- `json` 列按**附录 C 的 A 案**清洗(13 行全部被包成 JSON 字符串,**具名**进 `coerced[]`);⛔ **未执行任何删库**。
|
||||
- 迁移守「先备份 + 先出清单」(第 27 棒已在位,本棒**未重做**);本地库 sha256 迁移前后**逐字一致**。
|
||||
|
||||
---
|
||||
|
||||
## 六 未闭合项(逐条:现象 / 归属 / 下一步)
|
||||
|
||||
| # | 现象 | 归属 | 下一步 |
|
||||
|---|---|---|---|
|
||||
| ㈠ | **实例内端到端未取证**:实例仍载 `0.3.15`(实例启动早于本棒改动);本棒 host 代码虽 `lib/client.js` 零改动,但 `lib/host/mcn/**` 已是新版 —— 实例里跑的还是旧 host 码 | 部署面 | **需 47 实例重启**(影响 1 个在线用户 `cce6d1cd-…`,约 1–3 分钟)⇒ 待拍板 ㈠ |
|
||||
| ㈡ | **出包 `0.5.0` / 投放 / 两机一致(S7–S11)全未开工**(包仍 `version 0.4.0`;共享层仍 0.4.0) | 部署面 | 投放会让新版对**所有登录用户可见可启**(扩面)⇒ 与 ㈠ 一起排 |
|
||||
| ㈢ | **红线仍 23**(未归零) | 插件侧 | 属阶段二 **S10**(删 `db.js`),前置 = A1+A2 全绿 |
|
||||
| ㈣ | **第三批未动**:`account_videos`(362 行)+ `hot_accounts`(**拆表 + 12 处 JOIN + 4 处 OFFSET + 2 处聚合**=主战场)+ `account_video_source`/`rewrite_log`/`script_review`/`storyboard_log`/`hot_account_ext` | 插件侧 | 排第三棒(见入口 §2「⏭️ 本线后续」) |
|
||||
| ㈤ | `json` 列**案 B**(改声明 `text`,须**删库重建**)仍待拍板 | 待用户拍板 | 越晚做代价越高(迁完再删 = 丢真实数据) |
|
||||
| ㈥ | 平台侧观察项(沿用第 25 棒):非法 `json` 值返回 `reason:"disabled"`,语义误导(应为 `bad-args`/`invalid-value`) | **平台侧**(⛔ 本棒不改 `src/**`) | 另线立项 |
|
||||
|
||||
---
|
||||
|
||||
## 七 卫生声明(A7 逐条)
|
||||
|
||||
- ⛔ **未改 `D:/github/dsh_shenxian/src/**`** —— `find src -newermt "2026-09-25 15:15"` = **0 件**(最新 mtime `12:20`);`src/web/routes/im.ts` 零改动。
|
||||
- ⛔ **未改用户 `home` 下的 profile / `.pnpm`** —— 唯一写入 = 该用户**自己的插件档位文件** `<home>/.dsh/data-plane.json`(即阶段一设计的**回滚落点**);本地库 `mcn-plugin.db` **只读打开**(`readOnly: true`)。
|
||||
- ⛔ **未重启任何服务**(47 实例、106、dshs 均未动);⛔ **未 commit / push**;⛔ **未投放任何版本**(共享层仍 0.4.0,包仍 0.4.0)。
|
||||
- ⛔ **未删任何库**;⛔ 未删旧库 `mcn-plugin.db`。
|
||||
- ✅ **临时 admin 会话** `1 → 删 1 → 残留 0`(`user_agent='poc-curl2'` 清理)。
|
||||
- ✅ **47 `/tmp/p29*` 残留 0**(探针目录已在收口时 `rm -rf`)。
|
||||
- ✅ 改动面**恰 7 件**(见头部变更集);`lib/client.js` 零改动 ⇒ A6 免跑。
|
||||
|
||||
---
|
||||
|
||||
## 八 下一个棒(唯一)
|
||||
|
||||
**第三批按表切换**(`account_video_source` + `hot_accounts` 拆表读写 + `account_videos`)——
|
||||
⚠️ 这批是**主战场**(12 处 JOIN / 4 处 OFFSET / 2 处聚合),且 `hot_accounts` 必须**拆表**(本地 25 列 > 平台 `MAX_COLUMNS=20`)⇒ 建议**先出一份规划棒**把「读改写(两次单表查 + 插件内合)+ 写改写(两表分写、无事务、具名 partial)」的口径定死再动手。
|
||||
|
||||
**本棒落地件**:本文件|探针 = `tmp/p29/{pre-a.sh,pre-b.sh,run-p29.mjs,orchestrate.sh,local-migrate-check.mjs}`(本机副本;`run-p29.mjs` 亦曾落 47 `/tmp/p29/`,已清)。
|
||||
|
||||
---
|
||||
|
||||
**变更记录**:2026-09-25 15:1x 立(第 29 棒 · 执行棒)。判据命令与输出原文均取自本轮真机执行,未转述。
|
||||
@@ -0,0 +1,154 @@
|
||||
# P0 修通(装配链)+ MCN 移动端 375×667 真机验收(第 17 棒)
|
||||
|
||||
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`|**线**:插件投放与分库线|**日期**:2026-09-25
|
||||
> **依据**:`接续入口_插件投放与分库线_20260922.md §2「🎯 本轮动作(第 17 棒)」`
|
||||
> ⛔ 未 commit / push;⛔ 未改 `src/web/routes/im.ts`;⛔ 未做 MCN 数据面接入改造。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论(先说判定)
|
||||
|
||||
| 件 | 判定 | 一句话 |
|
||||
|---|---|---|
|
||||
| 第 1 件 · 补可观测性 | ✅ **完成并真机见效** | 装配失败从"零日志"变成"命令 + 退出码 + pnpm 原文 + 任务对象 `detail`";**本轮三条根因全部是它直接指出的** |
|
||||
| 第 2 件 · 定位并修通重装配 | ✅ **通过(判据逐字满足)** | 实例依赖已是 `link:/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite`;实例加载 **`[mcn-suite] loaded v0.3.14`(自 0.3.13 升上来)** ⇒ 共享层更新对**已启用**用户生效 |
|
||||
| 第 3 件 · 375×667 真机五条视觉验收 | 🟡 **4/5 通过,第 ④ 条未通过(已定位到具体控件)** | 独占 ✅/可退出 ✅(等价判据)/无横向溢出 ✅/横屏 ✅;**聚焦不缩放 ❌**:工作台内可见表单控件 `select.v1rksel`(「全部赛道」)= **13px** |
|
||||
|
||||
🔴 **核心产出 = 三条此前不可观测的 P0 被定位并修掉两条+探针验证一条**(详见 §三)。第 16 棒那句"整条链路断 52 s 后失败、根因不明"现在是**完全可解释**的。
|
||||
|
||||
---
|
||||
|
||||
## 二、第 1 件 · 可观测性(逐条落点)
|
||||
|
||||
| 落点 | 改法 |
|
||||
|---|---|
|
||||
| `src/supervisor/plugin-assembly.ts` | 新增 `pnpmExecError()`:把 `execFileSync` 抛出的 `stdout/stderr` 尾巴(≤1200 字符)拼进 `message`,并挂结构化字段 `pnpmArgs/pnpmCwd/pnpmStderr/pnpmStdout/pnpmExit`,同时 `console.error` 落 journald |
|
||||
| `src/worker/agent.ts` | `/plugins/apply` 失败时落一条带 `userId + items + msg` 的 `console.error`(**worker 是独立 systemd 单元,日志与 Manager 不通**),回包 message 上限 400 → **2000** |
|
||||
| `src/web/routes/business-plugins.ts` | 任务对象新增 **`detail`**(原始原因,≤2000);`recordFailure()` 同时落 journald;`friendlyError` 认 `pnpmStderr`/`pnpm_timeout` |
|
||||
|
||||
**真机对比(47)**:改前 `journalctl -u dshs` 在失败时段**只有 HTTP 请求行**;改后一条命令即可读到根因:
|
||||
|
||||
```
|
||||
[business-plugins] 装配失败 @apply:dsh-plugin-mcn-suite=on: The operation was aborted due to timeout
|
||||
[worker/plugins/apply] 失败 code=pnpm_failed items=dsh-plugin-mcn-suite:off msg=pnpm remove dsh-plugin-mcn-suite --reporter silent 失败:… (exit=1)
|
||||
```
|
||||
|
||||
⚠️ 已上线:`lib/{supervisor/plugin-assembly,worker/agent,web/routes/business-plugins}.js` 三件与本地 **md5 逐字一致**(`cdfe3a2b…` / `d54bfde0…` / `5eaec1c6…`);改前副本 `/opt/dsh/backups/lib-p17/47-pre-p17-20260925-085630.tgz`。
|
||||
|
||||
---
|
||||
|
||||
## 三、第 2 件 · 三条 P0(两条已修 + 一条探针验证)
|
||||
|
||||
### P0-① 无条件 `-w` ⇒ pnpm 直接报错退出(已修)
|
||||
|
||||
**判据链**:`plugin-assembly.ts` 里 `PNPM_INSTALL_ARGS` 把 `-w` 写成**常量**(`['install','-w','--reporter','silent']`),而平台自己铺出来的 profile(`scripts/ensure-biz-plugins.cjs` 那条链)**没有** `pnpm-workspace.yaml`(47 · admin 实例 `home/profiles/web/` 实测只有 `package.json` / `pnpm-lock.yaml` / `cordis.*` / `node_modules`)。
|
||||
**探针(47,1 秒可复现)**:`pnpm remove foo -w --reporter silent` 在无 workspace 文件的目录 ⇒
|
||||
`ERROR --workspace-root may only be used inside a workspace`(RC=1)。
|
||||
⇒ 门户「启停插件」**整条链断在 pnpm 这一步**,症状只有"安装失败,请重试或联系管理员"。
|
||||
|
||||
**修法(最小)**:改成 `workspaceRootFlag(dir)` / `pnpmInstallArgs(dir)` —— **只在真有 `pnpm-workspace.yaml` 时才加 `-w`**,与仓内既有三处脚本(`ensure-biz-plugins.cjs:219`、`ensure-portal-entry.cjs:215`、`install-plugin-for-user.cjs:134`)**逐字同一姿势**。
|
||||
|
||||
**真机前后**:同一动作(全量关闭 2 个已启用插件)改前 **52 s → failed**(第 16 棒两次复现),改后 **≈5 s → success**。
|
||||
|
||||
### P0-② `/restart-probe` 吃 10 s 默认预算(已修)
|
||||
|
||||
**判据链**:`RemoteSpawner.call()` 对**所有**口用 `AbortSignal.timeout(this.timeoutMs)`(默认 **10 s**),而 agent 侧 `/restart-probe` 要**真重启实例**(冷启动实测 10–14 s)+ 2.5 s 稳定窗、且它自己的预算是 **60 s** ⇒ **必然超时**;更坏的是 `call()` 超时后**再重试 3 次**(200ms/1s/3s),**每次重试都等于又重启一次实例**。
|
||||
**真机读数**:一条任务连打 **8 次** `/restart-probe`(08:59:32/:42/:53、09:00:06/:26/:37/:48、09:01:01),约 100 s 后以 `The operation was aborted due to timeout` 收场 ⇒ 插件被误判"与实例不兼容"自动禁用。
|
||||
|
||||
**修法**:`call()` 增可选 `opts.timeoutMs`;`/restart-probe` 走 `PROBE_TIMEOUT_MS = 90_000`(>对端 60 s 预算)。
|
||||
**真机前后**:改前永远 `aborted due to timeout`;改后探活能拿到**对端真实判定**(`实例未能启动` / `ok:true`)。
|
||||
|
||||
### P0-③ 共享层里的插件**加载不起来**(已探针验证,正式修法待转正)
|
||||
|
||||
**判据链**:`link:` 建立后实例冷启动即崩——`crash-restart` 的 `lastError` 原文:
|
||||
|
||||
```
|
||||
Error: dsh: plugin tree failed to load: … failed to import loader entry mcn-suite (dsh-plugin-mcn-suite):
|
||||
Cannot find package '@deepseek-ai/dsh-llm' imported from
|
||||
/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/host/mcn/index.js [ERR_MODULE_NOT_FOUND]
|
||||
```
|
||||
|
||||
**机理**:旧 `file:` 形态把实体放进**实例自己的 profile 树内**(`<profile>/node_modules/.pnpm/…`),Node 向上解析时能命中 pnpm 的提升目录 ⇒ 平台包(`@deepseek-ai/*`)解析得到;`link:` 把实体放到**树外**(共享层)⇒ 解析链断在共享层自己的 `node_modules`(里面只有 `react/react-dom/xlsx`,**零个 `@deepseek-ai`**)。插件 `peerDependencies` 里也没声明 `dsh-llm`(它靠运行环境提供)。
|
||||
|
||||
**已做的探针(**待转正**)**:给共享层目录补一条解析链接——
|
||||
`<shared>/node_modules/@deepseek-ai → /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai`(240 个平台包)
|
||||
⇒ 重启用**一次通过**,实例 `[mcn-suite] loaded v0.3.14`。⛔ **这是探针,不是最终修法**:正式落点应在**共享层铺装流程**(`doShare`)里自动完成,否则下次 share 覆盖目录就丢。
|
||||
|
||||
---
|
||||
|
||||
## 四、第 2 件 · 判据读数(第三方可复现)
|
||||
|
||||
```bash
|
||||
U=/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde
|
||||
ssh bt-server 'python3 -c "import json;print(json.load(open(\"'"$U"'/home/profiles/web/package.json\"))[\"dependencies\"][\"dsh-plugin-mcn-suite\"])"'
|
||||
# 期望输出:link:/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite ✅ 实测一致
|
||||
ssh bt-server 'journalctl -u dshs-worker --since "09:09" | grep "mcn-suite] loaded"'
|
||||
# ✅ [mcn-suite] loaded v0.3.14(host 面 2 个:mcn, voxemw-cloud)
|
||||
ssh bt-server 'grep -c mcn-mobile /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/client.js' # ✅ 2
|
||||
```
|
||||
|
||||
**任务读数**:`taskId=41e6fdc740915ca3` → `status:success / stage:完成 / rejected:[]`,最终 `bundles` 含 `dsh-plugin-mcn-suite`。
|
||||
|
||||
---
|
||||
|
||||
## 五、第 3 件 · 375×667 真机五条(本机真 Chrome + CDP)
|
||||
|
||||
**渲染环境**:本机 `C:/Program Files/Google/Chrome/Application/chrome.exe`(真 Chromium)+ CDP(`Emulation.setDeviceMetricsOverride` 375×667 / 667×375,DSF 2,`mobile:true`);入口 = `/api/dsh/status` 直出的实例地址 `https://admin.ai1net.com/?token=…`(平台自动下发实例凭据)。取证脚本 ⇒ `tmp/p17-visual.mjs`(零依赖,Node 22 自带 WebSocket)。
|
||||
|
||||
| # | 判据 | 读数 | 判定 |
|
||||
|---|---|---|---|
|
||||
| ① | 工作台**独占** | 开启后 `[data-slot="main.conversation"]` computed **display:none**;`.mcnNav_split` display=block、box `x=56 w=319 h=667`(56px = 插件自己的图标 rail)⇒ 375px 视口**全被插件占满** | ✅ |
|
||||
| ② | **能退出** | 单面板形态**不渲染** `.mcnNav_close`(计数 0)⇒ 该判据在此形态无对应元素。**等价判据**:再点一次 `.mcnNav_trigger` ⇒ URL `#/mcn` → `/`、`.mcnNav_split` 1→**0**、会话区 display none→**contents** | ✅(等价判据,如实标注) |
|
||||
| ③ | **无横向溢出** | 竖屏 `scrollWidth 375 = innerWidth 375`(溢出 **0**);横屏 `667 = 667`(溢出 **0**) | ✅ |
|
||||
| ④ | **输入聚焦不缩放** | 工作台内**唯一可见表单控件** = `SELECT.v1rksel`(「全部赛道」赛道筛选)字号 **13px < 16px** ⇒ 聚焦会触发 iOS 缩放。移动端样式段只覆盖 `.mcnNav_input/.mcnNav_select/.mcnNav_textarea{font-size:16px}` ⇒ 该控件**在覆盖之外** | ❌ **未通过**(已定位到控件与缺口) |
|
||||
| ⑤ | **横屏仍可用** | 667×375 下工作台仍开启(`.mcnNav_split` 计数 1)、无横向溢出、卡片按宽屏重排 | ✅ |
|
||||
| 附 | 0.3.14 真机生效 | `style[data-plugin-css="mcn-mobile"]` 计数 **1**(该样式段只存在于 0.3.14) | ✅ |
|
||||
|
||||
**截图**(`tmp/`):`p17-shot-375x667-open.png`(竖屏开启,工作台占满)|`p17-shot-667x375-open.png`(横屏开启)|`p17-shot-375-exit.png`(二次点击后已回到会话态)。
|
||||
**用户报障("一半会话一半插件")**:直观看已修复 —— 开启后会话区 computed `display:none`。
|
||||
|
||||
---
|
||||
|
||||
## 六、本轮新抓出(⛔ 未擅自修 · 按纪律报告)
|
||||
|
||||
| 级别 | 缺陷 | 说明与建议落点 |
|
||||
|---|---|---|
|
||||
| 🔴 P0 | **共享层不自足**(§三 P0-③) | 探针已临时补链;**正式落点 = `doShare` 铺装流程**(把插件需要的平台包解析链写进共享层目录),否则下次 share 覆盖即丢 |
|
||||
| 🟡 P1 | **移动端规则覆盖缺口** | `.mcnNav_input/select/textarea` 之外的插件原生控件(如 `v1rksel`)仍是 13px ⇒ 第 ④ 条未通过。建议:把规则扩到工作台全部表单控件(或按 `input,select,textarea` 通配限定在插件容器内) |
|
||||
| 🟡 P1 | **回滚不清理 `node_modules`** | `restoreProfile()` 只还原 `package.json`/`pnpm-lock.yaml`/`cordis.patch.yml` ⇒ 留下"依赖声明已撤、软链仍在"的残影(本轮实测该残影让实例崩过一次,随后自愈) |
|
||||
| 🟡 P2 | **错误消息在跨机处被截 200 字符** | `remote-spawner.ts` 的 `Error(... text.slice(0,200))` ⇒ Manager 侧 `detail` 只能看到半截(本轮靠 worker 侧 journald 补齐)。建议放宽到 ~1200 |
|
||||
| 🟡 P2 | **`preflight-lock.sh` 对工作区根文件判【D】** | 本棒目标里的 `接续入口_*.md` 被判"未归类 ⇒ 拒开工"(脚本只认 `^(src\|poc\|test\|web\|docs\|scripts\|交接单\|skills)/`)。域本身可算(guard 的锚点算法给出 `aliyun-dsh-server/<文件名>`)⇒ 属**门禁脚本盲区**,本棒按该域锁了、未改脚本(跨库文件,只报告) |
|
||||
| 🟠 待拍板 | **`@softspark/dsh-file-preview` 被我这一棒关掉且现在不可见** | 复现第 2 件要求"全量 apply 一条 `enabled:false`",把它一起关了;它**不在共享层**,而可见性判据 = 共享层已发布 ∪ 我已启用 ⇒ 它从用户面清单消失,**平台内已无法自助重开**。恢复 = 先 `POST /api/plugins/business/share`(发布到共享层)再启用;但发布会让**所有登录用户**在插件市场看见并可自助安装 ⇒ 属可见面变化,**本棒未擅自做** |
|
||||
|
||||
---
|
||||
|
||||
## 七、复现命令
|
||||
|
||||
```bash
|
||||
# ① pnpm 参数坑(1 秒,无需真机状态)
|
||||
ssh bt-server 'rm -rf /tmp/probe && mkdir /tmp/probe && printf "{}" > /tmp/probe/package.json && cd /tmp/probe && pnpm remove foo -w --reporter silent; echo RC=$?; rm -rf /tmp/probe'
|
||||
# 期望:ERROR --workspace-root may only be used inside a workspace / RC=1
|
||||
# ② 装配链(需要 admin 会话):全量 apply 一条 enabled:false 后轮询 task
|
||||
# 期望:≈5 s success(改前:≈52 s failed)
|
||||
# ③ 共享层解析链
|
||||
ssh bt-server 'ls /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/node_modules/@deepseek-ai | wc -l' # 期望 240
|
||||
# ④ 移动端五条(本机)
|
||||
P17_SID=<sid> P17_URL='https://admin.ai1net.com/?token=<token>' P17_CLICK='.mcnNav_trigger' \
|
||||
node tmp/p17-visual.mjs
|
||||
```
|
||||
|
||||
**本轮临时脚本**(均在 `tmp/`):`p17-visual.mjs`(CDP 真机取证,通用:可换视口/点击目标)· `p17-mksess.sh`(临时 admin 会话)· `p17-apply.sh`(发起 apply 并轮询终态)· `p17-repro.sh`(复现全量关闭)。
|
||||
|
||||
---
|
||||
|
||||
## 八、清理与合规
|
||||
|
||||
| 项 | 状态 |
|
||||
|---|---|
|
||||
| 临时 admin 会话 | ✅ 已删(`DELETE FROM sessions WHERE user_agent='p17-mksess'`,见入口 §0) |
|
||||
| 47 临时物 | ✅ `/tmp/p17-*.tgz` 已删;`/opt/dsh/backups/lib-p17/` 保留(回滚素材) |
|
||||
| 实例状态 | ✅ running(pid 稳定、`/api/dsh/status` 200、无 crash-restart) |
|
||||
| 装配协议 | ✅ 未改(`link:` 仍是唯一协议) |
|
||||
| `src/web/routes/im.ts` | ✅ **零改动** |
|
||||
| 可见面 | ⚠️ 唯一变化 = mcn-suite 从"旧副本"变成"指向共享层的最新版"(本线目标本身);⛔ 未新开任何口 |
|
||||
| commit / push | ✅ 未做 |
|
||||
@@ -0,0 +1,129 @@
|
||||
# pnpm `exit 255` 根因与越权软链修法(插件投放与分库线 · 第 20 棒)
|
||||
|
||||
> **日期**:2026-09-25 11:0x–11:3x|**棒次**:第 20 棒 · 执行棒|**工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`
|
||||
> **代码基线**:`D:/github/dsh_shenxian`(生产口径 `dshs`)|**真机**:47.77.182.89(Manager `dshs` + worker `dshs-worker`)
|
||||
> **本轮唯一动作**:pnpm `exit 255` 可见化 + 根因修复(P0)。原第 2 件(基础插件判据 ①③)**已于规划侧按用户 11:0x 口径收口**,本轮未动代码。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论(三句话)
|
||||
|
||||
1. `exit 255` **不是偶发、不是并发、也不是第 19 棒推测的 store 口径**,而是 **`EPERM: chmod`**:pnpm 在 `linkBin` 阶段对一个**未声明的 extraneous 软链**的 bin 文件做 `chmod`,而该文件实体在**共享只读层**、属主是 **root** ⇒ 非属主 chmod 一律 `EPERM`。
|
||||
2. 修法 = **跑 pnpm 的那一小段里把这类软链改名移开,`finally` 无条件放回**(不 chown、不复制、不 root 跑,三条旁路均已真机排除)。
|
||||
3. 判据 ①(真机原文)②(存量 profile 装配成功 + 双向一致)③(单测钉住)**全绿**;47 已上线,106 已投件但**未重启**(零中断,待下次重启生效)。
|
||||
|
||||
---
|
||||
|
||||
## 二、判据①:真机真实报错原文(第 19 棒假设被证伪)
|
||||
|
||||
平台调用**复刻**(47 · admin profile,只把 `--reporter silent` 换成 `default`):
|
||||
|
||||
```
|
||||
cd /var/lib/dshs/users/<uid>/home/profiles/web
|
||||
setpriv --reuid <uid> --regid <uid> --clear-groups env HOME=/var/lib/dshs/users/<uid>/ws \
|
||||
pnpm install --reporter default
|
||||
⇒ PNPM_RC=255
|
||||
EPERM EPERM: operation not permitted, chmod '…/profiles/web/node_modules/xlsx/bin/xlsx.njs'
|
||||
at async chmod (node:internal/fs/promises:1087:10)
|
||||
at async linkBin (…/pnpm/dist/pnpm.cjs:102416:9)
|
||||
at async installInContext (…/pnpm/dist/pnpm.cjs:181904:9)
|
||||
```
|
||||
|
||||
**为什么 `--reporter silent` 下看不到**:silent 把 pnpm 的 stdout/stderr 全吞掉 ⇒ `stderr=- stdout=-`(第 19 棒真机读数),只剩退出码。
|
||||
|
||||
**第 19 棒的 store 假设为何不成立**(真机核对):
|
||||
- `plugin-assembly.ts:499` 的形参名叫 `homeDir`,但**调用方传的是 `wsDirOf(userRoot)`**=`<root>/ws`;
|
||||
- 该 profile 的 `.modules.yaml` 记 `storeDir=<root>/ws/.local/share/pnpm/store/v3` ⇒ **两者逐字一致**,不可能触发 `ERR_PNPM_UNEXPECTED_STORE`。
|
||||
- ⇒ 结论:**别再按 store 方向改**(`scripts/ensure-biz-plugins.cjs` 的那套口径属于"另一条链",与本缺陷无关)。
|
||||
|
||||
---
|
||||
|
||||
## 三、根因与三条旁路(为什么只能"让它看不见")
|
||||
|
||||
### 3.1 事实链
|
||||
|
||||
| # | 事实 | 取证 |
|
||||
|---|---|---|
|
||||
| 1 | `node_modules/xlsx` = 软链 → `.dsh-module-fallback/node_modules/xlsx` → **共享层实体** | `readlink -f` |
|
||||
| 2 | 该实体属主 = **root**、`links=3`(硬链到共享层 store) | `stat -c "%h %U %a"` |
|
||||
| 3 | 它**不在** `package.json` 的 `dependencies`、`pnpm-lock.yaml` 里**零命中** ⇒ **extraneous** | `cat package.json` / `grep xlsx pnpm-lock.yaml` |
|
||||
| 4 | pnpm 对顶层 extraneous 包仍会做 `linkBin` ⇒ 对 bin **无脑 `chmod 0755`**(不看当前是否已 755) | 报错栈 `pReflect → linkBin` |
|
||||
| 5 | `chmod` 要求**调用者是文件属主**(或持 CAP_FOWNER)⇒ 非属主 `EPERM`(**不是 EACCES**) | 真机原文 |
|
||||
|
||||
### 3.2 三条旁路为何都不行(逐条真机/文档排除)
|
||||
|
||||
1. **把共享层文件 chown 给用户** —— 一份实体多用户共享,A 拿到 B 就没有(破 D2 拍板口径)。
|
||||
2. **复制一份用户属主的实体** —— 同样破 D2,且**破坏模块同一性**(第 18 棒已论证:软链解到宿主同一份实体,拷贝会让单例/`instanceof` 分叉)。
|
||||
3. **让 pnpm 跳过 `linkBin`** —— pnpm 9.15.9 **没有**这个开关(真机 `pnpm install --help` 无 bin 项);且共享层里 4 个包**自身都没有 `bin` 字段** ⇒ 触发者只可能是"外部塞进来的 extraneous 软链"。
|
||||
|
||||
⇒ 剩下的正解:**跑 pnpm 的那一小段里把它移开**。
|
||||
|
||||
---
|
||||
|
||||
## 四、修法(`src/supervisor/plugin-assembly.ts`,一件文件)
|
||||
|
||||
| 新增 | 作用 |
|
||||
|---|---|
|
||||
| `quarantineSharedLayerLinks(dir, sharedRoot, declared)` | 摘除「顶层(含 `@scope/name`)∧ **是软链** ∧ **不在声明集合**(`dependencies` ∪ `dsh.profile.bundles`)∧ **解到底落在共享层内**」的软链;**改名**为 `.dsh-quarantine-<name>`(⛔ 不删、⛔ 不碰真目录/`.` 旁挂物/悬空链) |
|
||||
| `restoreQuarantinedLinks(links)` | 原样放回;原名被占 ⇒ **跳过并具名返回**(⛔ 不覆盖、⛔ 不丢隐藏项);⛔ 不抛 |
|
||||
| `withQuarantinedLinks(...)` | 包住 `runPnpm`:`finally` **无条件放回**,放回失败落 `console.error`(显形,不静默) |
|
||||
| `pnpmDiagArgs(args)` | `--reporter silent` ⇒ `--reporter default`(其余参数逐字保留、顺序不变;原本没写则补一个) |
|
||||
| `pnpmDiagRerun(...)` | 失败后**只重跑一次**诊断(同 uid/HOME/cwd,⛔ 不递归、⛔ 不重试到成功、⛔ 不抛);重跑成功本身就是"首次失败是时序"的证据 |
|
||||
| `pnpmExecError(..., diag = '')` | 诊断读数进 `message`(跨机后只剩 message 可读)+ 结构化字段 `pnpmDiag` + journald |
|
||||
|
||||
接线:`applyProfileChanges` 的 **remove 分支**与 **install 分支**的 `runPnpm` 各自包进 `withQuarantinedLinks`(声明集合在函数入口一次算好,与 `pruneOrphanDepLinks` 同一份口径)。
|
||||
|
||||
---
|
||||
|
||||
## 五、判据②:真机验收(47 · admin 存量 profile)
|
||||
|
||||
**验收对象**:`/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde`(uid `114801`,profile 建于 `HOME=ws` 时代)。
|
||||
|
||||
| 项 | 读法 | 修前 | 修后 |
|
||||
|---|---|---|---|
|
||||
| 装配调用 | 平台模块 `applyProfileChanges`(items = 共享层内已声明项) | 抛 `pnpm_failed`(255) | **`APPLY_OK`**(`enabled:[dsh-plugin-mcn-suite]`、`refused:[]`) |
|
||||
| `dependencies ↔ node_modules` | 逐项 `existsSync` | — | **`DEPS 4 MISSING []`**(双向一致) |
|
||||
| 隐藏项残留 | `readdirSync` 过滤 `.dsh-quarantine-` | — | **0** |
|
||||
| 三条越权软链 | `lstat/readlink` | 在 | **原样在盘**(`xlsx`/`react`/`react-dom` → fallback) |
|
||||
| **对照组**(同 profile、**不经**修法手动复刻) | `setpriv … pnpm install --reporter default` | 255 | **仍 255,同一条 EPERM** |
|
||||
| 平台日志 | `journalctl -u dshs -u dshs-worker` 20 min 内 `plugin-assembly` | — | **0 条** |
|
||||
| 实例健康 | `[rehydrate] probe OK … :20000` | — | ✅ |
|
||||
|
||||
**对照组的价值**:把「修法」与「偶发」区分开 —— 同一份 profile、同一条命令,**不经**修法仍 255。
|
||||
|
||||
---
|
||||
|
||||
## 六、判据③:单测(`test/plugin-assembly.test.mjs`,+5 用例)
|
||||
|
||||
- 越权软链:只摘「顶层 ∧ 未声明 ∧ 落在共享层」的软链;**声明内 `link:` / 真目录 / 非共享层链 / 悬空链一律不碰**;放回逐字还原;两轮摘放不累积副作用。
|
||||
- 放回时**原名被占 ⇒ 具名返回且不覆盖**、隐藏项**不丢**。
|
||||
- `applyProfileChanges` 跑完 ⇒ 越权软链**仍在盘**(`finally` 放回,⛔ 不留隐藏态)。
|
||||
- 失败取证:`pnpmDiagArgs` 只换 reporter;诊断读数进 `message` + `pnpmDiag`;不传 diag 时与旧版逐字相同。
|
||||
|
||||
**门禁**:`npm run build` rc=0|`npm test` = **584 tests / 582 过 / 0 败 / 2 跳过**(第 19 棒 577 ⇒ +5)|`check:layering` ✅ 无新增违规。
|
||||
|
||||
---
|
||||
|
||||
## 七、部署与回滚
|
||||
|
||||
| 机 | 文件 | md5 | 重启 | 备份 |
|
||||
|---|---|---|---|---|
|
||||
| 47 | `/opt/dshs/lib/supervisor/plugin-assembly.js` | `3c10048a7b9d0a6eceb48c0e1a385d07`(本地逐字一致) | `restart dshs dshs-worker` ⇒ 双 active / NRestarts=0 | `/opt/dsh/backups/lib-p20/47-pre-p20-20260925-111517.tgz` |
|
||||
| 106 | `/opt/dshs-cluster/lib/supervisor/plugin-assembly.js` | `3c10048a7b9d0a6eceb48c0e1a385d07` | 🔴 **未重启**(`ExecMainStartTimestamp` 仍 `09:52:11`)⇒ 新代码**待下次重启生效**(零中断取向) | `/opt/dsh/backups/lib-p20/106-pre-p20-20260925-112626.tgz` |
|
||||
|
||||
- 回滚 = 用备份 tgz 还原该文件 → `systemctl restart dshs dshs-worker`(47)/`dshs-worker`(106)。
|
||||
- ⚠️ 重启 worker 会中断该机在跑的实例(本次 47 上 2 个 main 进程 → 重启后平台 `rehydrate` 已重新拉起并探活通过)。106 因此**刻意未重启**。
|
||||
|
||||
---
|
||||
|
||||
## 八、未闭合项(⛔ 本轮不擅自做)
|
||||
|
||||
1. 🔴 **那三条软链是"历史人工兜底遗留"** —— `dsh-module-fallback` 在**工作区代码里零命中**(不是平台代码建的),且 pnpm 语义上本来会把 extraneous 清掉 ⇒ **是否清理需用户拍板**(清理会改变实例的模块解析面)。⇒ 本修法只保证"平台装配不受影响",**不等于这些软链该存在**。
|
||||
2. 🔴 **106 的新代码未生效**(worker 未重启)。是否开窗口重启 = 「中断在线用户」类,待用户拍板。
|
||||
3. 🟡 **本修法的代价(如实记)**:每次跑 pnpm 会多两次 `rename`(微秒级);pnpm 运行期间(秒级)这几条软链短暂不可见 —— 若实例恰在此刻解析它们,需等放回后自愈。
|
||||
|
||||
---
|
||||
|
||||
## 九、本轮纪律
|
||||
|
||||
⛔ 未 commit / push|⛔ `src/web/routes/im.ts` 零改动|⛔ 未手改用户 `home` 的 profile / `.pnpm`(诊断与验收均走平台代码路径或平台命令复刻)|⛔ 未扩大范围(未顺手重构 `plugin-assembly`、未动共享层属主、未清人工软链)。远端临时件(`/tmp/p20-*`)**已清零**。
|
||||
@@ -0,0 +1,202 @@
|
||||
# 两机 `lib/` 差异面核对与 106 补齐(第 22 棒 · 执行棒)
|
||||
|
||||
> **线**:插件投放与分库线 | **棒**:第 22 棒(执行棒)| **日期**:2026-09-25 11:58–12:2x
|
||||
> **一句话**:106 的 `lib/` 停在 **09-23 08:04 那次构建**(+此后少量零散补投)⇒ 代码面缺 **46 件**;已按 **47 生产部署面**(=本机构建产物,抽样逐字一致)补齐 ⇒ **代码面差异面归零(368 = 368,内容不同 0)**。⛔ 全程未重启 106。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论(先行)
|
||||
|
||||
| 判据 | 结果 | 关键读数 |
|
||||
|---|---|---|
|
||||
| ① 完整差异清单 | ✅ | 47 **389** 件 / 106 **357** 件;逐字一致 **322**;内容不同 **23**;仅 47 有 **44**;仅 106 有 **12** |
|
||||
| ② 补齐后差异面归零 | ✅ | 代码面 **368 = 368**、内容不同 **0**、单向缺失 **0**(`p22-verify.py` rc=0) |
|
||||
| ③ 待重启才生效的文件 | ✅ 已列 | **46 件全部**(106 `dshs-worker` 起于 `09-25 09:52:11`,写入在其后);其中运行时 `.js` **21 件** |
|
||||
|
||||
**判据口径(本棒立 · 后棒沿用)**:两侧 `lib` 树里各自留有历史部署快照(`.s3bak-*` / `.s0bak-*` / `*.bak-*`),
|
||||
它们**不是代码面**、两机时间戳本就不同 ⇒ 判据**只在代码面**(368 件)上要求归零。
|
||||
|
||||
---
|
||||
|
||||
## 二、判据① · 完整差异清单
|
||||
|
||||
### 2.1 方法(3 条命令,第三方可复现)
|
||||
|
||||
```bash
|
||||
# 1) 两机各出一份「路径|md5|大小|mtime」全量清单(relpath 相对各自 lib 根)
|
||||
ssh bt-server 'cd /opt/dshs/lib && find . -type f | sort | while read -r f; do m=$(md5sum "$f"); m=${m%% *}; printf "%s|%s|%s|%s\n" "$f" "$m" "$(stat -c%s "$f")" "$(stat -c%Y "$f")"; done' > tmp/p22-lib47.tsv
|
||||
ssh test106 'cd /opt/dshs-cluster/lib && find . -type f | sort | while read -r f; do m=$(md5sum "$f"); m=${m%% *}; printf "%s|%s|%s|%s\n" "$f" "$m" "$(stat -c%s "$f")" "$(stat -c%Y "$f")"; done' > tmp/p22-lib106.tsv
|
||||
# 2) 对比出清单
|
||||
python tmp/p22-diff.py tmp/p22-lib47.tsv tmp/p22-lib106.tsv tmp/p22-diff-pre.tsv
|
||||
# 3) 复核(排除备份残留,rc=0 即归零)
|
||||
python tmp/p22-verify.py
|
||||
```
|
||||
|
||||
- 两机代码根:47 = `/opt/dshs`(Manager);106 = `/opt/dshs-cluster`(Worker)。**⛔ 两机根目录名不同是既定事实**,不影响本判据(比的是各自 `lib/` 内相对路径)。
|
||||
- 原始差异面 **79 条** → 其中**代码面 46 条** + **部署备份残留 33 条**(47 侧 21 条、106 侧 12 条)。
|
||||
|
||||
### 2.2 差异面构成
|
||||
|
||||
| 类别 | 条数 | 说明 |
|
||||
|---|---|---|
|
||||
| `DIFF`(两机都有、md5 不同) | **23** | 全部是 106 停在 09-23 版;含 `business-plugins.js`、`db/*.js`、`im/hub.js`、`web/server.js` 等 |
|
||||
| `ONLY_47` 中**代码件**(106 缺) | **23** | `db/plugin-data/*`、`im/backends|connection-backend|keepalive|presence-ingest`、`web/routes/plugin-data.js`、`web/essential-plugins.js` |
|
||||
| `ONLY_47` 中**备份残留** | 21 | `.s3bak-20260920-174307/`、`.s0bak-20260920-165634/`、`net/relay/*.bak-*`、`web/server.js.bak-seq19-*` |
|
||||
| `ONLY_106`(47 无) | 12 | **全部是 106 自己的备份残留**(`.bak-20260917-155841` / `.bak-seq19-…-180218`)⇒ 代码面多出 **0** |
|
||||
|
||||
⇒ **代码面差异面 = 23 + 23 = 46 件**(21 件运行时 `.js` + 25 件 `.d.ts`/`.js.map`)。
|
||||
|
||||
### 2.3 关于 33 条备份残留(⛔ 本棒未投、未删)
|
||||
|
||||
- **不投**:它们是 47 侧**改前快照**(时间戳 09-17 / 09-20),投到 106 只会让 106 多出一批**过时代码副本**,属净变差(可维护性)。判据不是"字节级全等",而是**代码面全等**。
|
||||
- **不删**:删除属**不可逆**、且是**别处部署面的历史留档**,超出"两机差异核对"范围 ⇒ 只报告。
|
||||
|
||||
### 2.4 本次补齐的 46 件(清单 + 两侧指纹)
|
||||
|
||||
| 路径 | 类型 | 47 md5 | 106 改前 md5 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `./db/pg.js` | 运行时 .js | `8f7923fb0414` | 38375 | `b79bf5ce6af8` | 37989 |
|
||||
| `./db/plugin-data/registry.d.ts` | 声明/映射 | `adef48d3ffbf` | 5414 | `—` | — |
|
||||
| `./db/plugin-data/registry.js` | 运行时 .js | `44d3b5c424f7` | 9333 | `—` | — |
|
||||
| `./db/plugin-data/registry.js.map` | 声明/映射 | `e83f9b4c0504` | 6615 | `—` | — |
|
||||
| `./db/plugin-data/runtime.d.ts` | 声明/映射 | `31cc1acdad96` | 8561 | `—` | — |
|
||||
| `./db/plugin-data/runtime.js` | 运行时 .js | `45daf7c5b2b6` | 20309 | `—` | — |
|
||||
| `./db/plugin-data/runtime.js.map` | 声明/映射 | `d8f91115fcaf` | 15637 | `—` | — |
|
||||
| `./db/repo.js` | 运行时 .js | `fe8bc9a698ab` | 40132 | `47f82e9617ba` | 39384 |
|
||||
| `./db/schema.js` | 运行时 .js | `cf62c2b43cac` | 41287 | `1cb43ade2db8` | 40487 |
|
||||
| `./db/sqlite.js` | 运行时 .js | `f05eb1a8e816` | 14478 | `62ae95ce50a5` | 14285 |
|
||||
| `./db/types.js` | 运行时 .js | `20bd54a22f6c` | 7766 | `c1caff6f1ed4` | 7601 |
|
||||
| `./im/backends/gateway.d.ts` | 声明/映射 | `693cea8cf738` | 7131 | `—` | — |
|
||||
| `./im/backends/gateway.js` | 运行时 .js | `7f0638c89d48` | 12077 | `—` | — |
|
||||
| `./im/backends/gateway.js.map` | 声明/映射 | `61950bd7abf7` | 6470 | `—` | — |
|
||||
| `./im/backends/native.d.ts` | 声明/映射 | `c2ca6034ae13` | 2323 | `—` | — |
|
||||
| `./im/backends/native.js` | 运行时 .js | `2d14f373325c` | 3168 | `—` | — |
|
||||
| `./im/backends/native.js.map` | 声明/映射 | `8b652f7acfa9` | 1165 | `—` | — |
|
||||
| `./im/connection-backend.d.ts` | 声明/映射 | `505e22d66ff5` | 8241 | `—` | — |
|
||||
| `./im/connection-backend.js` | 运行时 .js | `19ddbb402dd9` | 3342 | `—` | — |
|
||||
| `./im/connection-backend.js.map` | 声明/映射 | `2525a78e9dcc` | 2150 | `—` | — |
|
||||
| `./im/hub.d.ts` | 声明/映射 | `9f1c235ec084` | 12414 | `b6ec9ae149d8` | 10871 |
|
||||
| `./im/hub.js` | 运行时 .js | `cf405a4b5a10` | 13468 | `f36638808680` | 11868 |
|
||||
| `./im/hub.js.map` | 声明/映射 | `1afee241b41d` | 9325 | `8361b2e081ae` | 8538 |
|
||||
| `./im/keepalive.d.ts` | 声明/映射 | `042ca5b4943c` | 5015 | `—` | — |
|
||||
| `./im/keepalive.js` | 运行时 .js | `efb3963f4e40` | 6462 | `—` | — |
|
||||
| `./im/keepalive.js.map` | 声明/映射 | `31d8cc60d92e` | 4269 | `—` | — |
|
||||
| `./im/presence-ingest.d.ts` | 声明/映射 | `7ed47172720d` | 6641 | `—` | — |
|
||||
| `./im/presence-ingest.js` | 运行时 .js | `bb9e599f0b84` | 6607 | `—` | — |
|
||||
| `./im/presence-ingest.js.map` | 声明/映射 | `8eef921f5597` | 4701 | `—` | — |
|
||||
| `./supervisor/plugin-assembly.d.ts` | 声明/映射 | `e896c0c53bfb` | 13955 | `e0d3a15b8751` | 8762 |
|
||||
| `./supervisor/plugin-assembly.js.map` | 声明/映射 | `f3eb2523919c` | 13104 | `dc9aad3f62a4` | 10274 |
|
||||
| `./supervisor/remote-spawner.d.ts` | 声明/映射 | `589b9bca7a53` | 11968 | `85ed46970499` | 10372 |
|
||||
| `./supervisor/remote-spawner.js` | 运行时 .js | `269d3d20ffce` | 20564 | `bea5d38cdc3f` | 18525 |
|
||||
| `./supervisor/remote-spawner.js.map` | 声明/映射 | `218758f06bb5` | 11105 | `6ced34702565` | 10963 |
|
||||
| `./web/dsh-install.js` | 运行时 .js | `ce82eef48c4c` | 6628 | `f707c77e505f` | 8504 |
|
||||
| `./web/essential-plugins.js` | 运行时 .js | `9e79a5fd5665` | 6606 | `—` | — |
|
||||
| `./web/routes/admin.js` | 运行时 .js | `3c5a64cb1c4d` | 19066 | `e794cad96c90` | 18184 |
|
||||
| `./web/routes/business-plugins.d.ts` | 声明/映射 | `72fee3739477` | 5640 | `d50990983ad6` | 2944 |
|
||||
| `./web/routes/business-plugins.js` | 运行时 .js | `3a752e01e4f1` | 135308 | `70d5c0f2085a` | 114041 |
|
||||
| `./web/routes/business-plugins.js.map` | 声明/映射 | `d919d78e8a53` | 73109 | `545ed8c21cf2` | 59079 |
|
||||
| `./web/routes/im.js` | 运行时 .js | `14dd9a0cf7c9` | 36738 | `d6ee03187ca1` | 27355 |
|
||||
| `./web/routes/im.js.map` | 声明/映射 | `62d1cb88348e` | 22653 | `576a3c7f8ad6` | 18494 |
|
||||
| `./web/routes/plugin-data.js` | 运行时 .js | `fac8f4dfe0d5` | 15523 | `—` | — |
|
||||
| `./web/server.js` | 运行时 .js | `fcbbb9b34764` | 75298 | `c935e916d4fe` | 74822 |
|
||||
| `./worker/agent.d.ts` | 声明/映射 | `e8799f5b6d55` | 7275 | `9cd56a9a914e` | 3445 |
|
||||
| `./worker/agent.js.map` | 声明/映射 | `6a09bb376d7f` | 36616 | `2cf708d046f3` | 23514 |
|
||||
|
||||
> 完整 79 条原始清单(含两侧 md5/大小/mtime)= `tmp/p22-diff-pre.tsv`;补齐清单 = `tmp/p22-fill-list.txt`。
|
||||
|
||||
---
|
||||
|
||||
## 三、判据② · 补齐与归零
|
||||
|
||||
### 3.1 动作与读数
|
||||
|
||||
| 步 | 动作 | 读数 |
|
||||
|---|---|---|
|
||||
| 1 | 106 全量备份(改前) | `/opt/dsh/backups/lib-p22/106-pre-p22-20260925-120615.tgz`(sha256 `9612e4c0f63e024db3a7aceaa3088c26474e886c67cdafd848c80a8eba739233`)+ 同目录改前指纹清单 `…-manifest-20260925-120615.tsv` |
|
||||
| 2 | 47 侧按清单打包 | `/tmp/p22-fill.tgz` = **232651 B** / 46 件,md5 `1ac3922cb3a3239a3f9d4cf59f7beb4f` |
|
||||
| 3 | 传输完整性 | 106 收到后 md5 **同为** `1ac3922cb3a3239a3f9d4cf59f7beb4f` ✅ |
|
||||
| 4 | 解包落地 | `tar xzf` rc=0;`lib` 文件数 **357 → 380**(+23 缺件);新建目录 `db/plugin-data`、`im/backends` 属主 **root:root 755** ✅(与既有目录一致) |
|
||||
| 5 | 复核 | 重采 106 全量清单 ⇒ **代码面 368 = 368、内容不同 0、单向缺失 0**,`p22-verify.py` **rc=0** |
|
||||
|
||||
### 3.2 基准成立性(为什么拿 47 当基准是对的)
|
||||
|
||||
- 47 = **Manager 现行运行单元**(`dshs.service` 起于 `09-25 11:47:33`)所加载的那份代码 ⇒ 生产口径。
|
||||
- 抽样验证 47 ≡ 本机构建产物 `D:/github/dsh_shenxian/lib`:`db/pg.js` `8f7923fb…`、`supervisor/remote-spawner.js` `269d3d20…`、`web/routes/business-plugins.js` `3a752e01…` **三件逐字一致**。
|
||||
- ⚠️ 但**本机构建产物 ≠ 生产部署面全等**(见 §五-2)⇒ 本棒的基准**只取 47 生产面**。
|
||||
|
||||
---
|
||||
|
||||
## 四、判据③ · 106 待重启才生效的文件
|
||||
|
||||
### 4.1 106 的运行形态(现场取证)
|
||||
|
||||
- 单元:**只有 `dshs-worker.service`**(active,`ExecMainStartTimestamp = Fri 2026-09-25 09:52:11 CST`)+ `dshs-relay.service`;**⛔ 无 `dshs.service`**。
|
||||
- 入口:`ExecStart = /usr/bin/node /opt/dshs-cluster/lib/cli.js worker --port 19000 --host 127.0.0.1 --host-id w-106 …`。
|
||||
- ⚠️ 关键:`cli.js` **顶层静态 import** 了 `./db/index.js`(→ `./pg.js`、`./sqlite.js`)与 `./web/server.js`(静态 import **14 条 routes**)⇒ **web/db 层同样在 106 worker 的模块图内**(不是"106 用不到的库存")。
|
||||
|
||||
### 4.2 待重启清单
|
||||
|
||||
- **全部 46 件**(写入时刻 12:06+ **晚于** 进程启动 09:52:11)⇒ 进程内仍是旧版;**下次 `dshs-worker` 重启后才生效**。
|
||||
- 其中 **运行时 `.js` 21 件**(其余 25 件为 `.d.ts`/`.js.map`,不参与运行)。
|
||||
- **本棒改盘对运行中进程的行为影响 = 0**:ESM 静态图在启动时已全部求值并缓存,改盘不改变已加载模块。
|
||||
- ⚠️ **残留理论窗口**:`web/routes/admin.js`、`web/routes/dsh.js`、`web/plugin-compat*.js` 内存在**动态 `import()`** ⇒ 若某目标模块在重启前**首次**被动态加载,会读到新文件,与内存中旧模块并存。未观测到异常;47 上"同版本组合"已在跑 ⇒ 判**低风险**;彻底消除 = 重启(⛔ 待用户给窗口,见入口 §5-19)。
|
||||
- 📌 与 §5-19 **同源但不同事**:§5-19 说的是"p20 的 `plugin-assembly.js` 待重启";本棒补的是**另外 46 件**的待重启面。
|
||||
|
||||
---
|
||||
|
||||
## 五、新发现(⛔ 本棒未动 · 已登记入口 §5)
|
||||
|
||||
### 5.1 106 落后面远大于第 21 棒顺带看到的那一件
|
||||
|
||||
第 21 棒只看到 `web/routes/business-plugins.js` 落后(`70d5c0f2…` / 114041 B / mtime `09-25 00:28`,而 47 为 `4109ba34…` 投前)。本棒全量核对:**代码面落后 46 件**,`business-plugins.js` 只是其中之一。该文件的落后 = **缺第 17 棒(可观测性 + `-w` 修)、第 19 棒(`essential` 标记)、第 21 棒(`reconcile` 口)三轮改动**。
|
||||
|
||||
### 5.2 🔴 本机构建产物 ≠ 生产部署面(后棒 ⛔ 别拿本机 `lib` 当基准)
|
||||
|
||||
`D:/github/dsh_shenxian/lib`(**09-25 11:44 新构建**)与两机部署面(09-23 20:59 的 IM 版)不同,且**两机缺的是同一批**(不是 106 独有):
|
||||
|
||||
| 类 | 件 | 本机 | 47 / 106 |
|
||||
|---|---|---|---|
|
||||
| 运行时 `.js` 内容不同 | `im/backends/gateway.js` | 16241 B | 12077 B |
|
||||
| 运行时 `.js` 内容不同 | `im/connection-backend.js` | 4154 B | 3342 B |
|
||||
| 运行时 `.js` 内容不同 | `web/routes/im.js` | 47262 B | 36738 B |
|
||||
| 本机独有 | `im/gateway-token.{js,d.ts,js.map}` | 7280 B(`.js`) | **两机都没有** |
|
||||
| 本机独有 | `web/essential-plugins.{d.ts,js.map}`、`web/routes/plugin-data.{d.ts,js.map}` | — | 两机只有 `.js` |
|
||||
|
||||
- 另有 31 件 `.d.ts`/`.js.map` 差异(本机构建与部署面构建不同次 ⇒ 声明/映射产物不同),**运行时 `.js` 不受影响**。
|
||||
- ⇒ 定性:本机 `lib` 含 **IM 线尚未投放的改动**(本机文件更大 + 多出新件)。**⛔ 本棒未据此改任何一侧**(超出"两机差异核对"范围;且那批件属 IM 线投放决定)。**已登记为待查项。**
|
||||
|
||||
---
|
||||
|
||||
## 六、未做 / 未闭合(⛔ 不得据本文宣称"运行面验收通过")
|
||||
|
||||
| # | 项 | 为什么没做 |
|
||||
|---|---|---|
|
||||
| 1 | **106 重启以使 46 件生效** | 中断在线用户 ⇒ **待用户给窗口**(同 §5-19) |
|
||||
| 2 | **运行面读数**(重启后 106 上实跑新代码) | 依赖第 1 项 ⇒ 本棒**只完成"文件面归零"**,⛔ 未验证运行面 |
|
||||
| 3 | 投本机独有 7 件(`im/gateway-token.*` 等) | 超出范围 + 属 IM 线未投放件 |
|
||||
| 4 | 清两侧 33 条备份残留 | 不可逆 + 别处部署面历史留档 ⇒ 只报告 |
|
||||
| 5 | 46 件在 106 上的**功能影响面**核对(哪几件 106 真的会用) | 已做模块图取证(`cli.js` → `db/index.js` + `web/server.js` 14 routes ⇒ 全在图内),未逐件做行为核对 |
|
||||
|
||||
---
|
||||
|
||||
## 七、回滚
|
||||
|
||||
```bash
|
||||
# 106 回滚(恢复改前整棵 lib)
|
||||
cd /opt/dshs-cluster && tar xzf /opt/dsh/backups/lib-p22/106-pre-p22-20260925-120615.tgz
|
||||
# 或按需只回滚单件:从该 tgz 内取对应路径(题录:./db/pg.js 等,相对 lib 根)
|
||||
```
|
||||
|
||||
- 47 侧**零改动**(本棒只读 47,只写 106)。⛔ 未 commit / push;⛔ `src/web/routes/im.ts` 零改动;⛔ 未手改用户 `home` 下 profile / `.pnpm`。
|
||||
|
||||
---
|
||||
|
||||
## 八、卫生
|
||||
|
||||
- 47 `/tmp/p22-fill.tgz`、106 `/tmp/p22-fill.tgz`、`/tmp/p22-lib106.tsv`:本棒临时件(入口 §9 口径:`tmp/` 留 7 天)。
|
||||
- 新建目录仅 `106:/opt/dsh/backups/lib-p22/`(备份)与工作区 `tmp/p22-*`。
|
||||
- 锁:本棒域锁(`aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md`)收口时释放。
|
||||
|
||||
---
|
||||
|
||||
*生成:`tmp/p22-report-gen.py`(表由清单现算,非手抄)。*
|
||||
@@ -0,0 +1,164 @@
|
||||
# 共享层台账两条语义缺陷修法(插件投放与分库线 · 第 21 棒)
|
||||
|
||||
> **日期**:2026-09-25 11:35–12:2x|**棒次**:第 21 棒 · 执行棒|**工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`
|
||||
> **代码基线**:`D:/github/dsh_shenxian`(生产口径 `dshs`)|**真机**:47.77.182.89(Manager `dshs` + worker `dshs-worker`)+ 106.54.21.172(worker `w-106`)
|
||||
> **本轮唯一动作**:共享层台账两条语义缺陷(§5-10 ㈠㈡)——「清单与磁盘不同步」+「回滚素材名实不符」。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论(三句话)
|
||||
|
||||
1. 两条缺陷**同源**:**该写进台账的值,此前来自"别处的副本",而不是盘上那份实体** —— ㈠ `version` 取自候选池行;㈡ "回滚素材"落的是**新包自拷贝**。
|
||||
2. 修法共用一条纪律:**要写进台账的值,必须从盘上那份实体现读**。落地 = 三个真值口径函数 + `doShare` 两处接线 + **一个显式自愈口**(㈠ 的存量只能靠"拿盘上真值回写清单"收口)。
|
||||
3. 四条判据**真机全绿**:清单 ≡ 磁盘真值(含"篡改 → 自愈 → 回到真值"端到端)|节点**连续两轮** `current`|回滚素材名实相符(且不含 `node_modules`)|单测 5 例钉住口径。47 已上线并重启;⛔ 未 commit/push。
|
||||
|
||||
---
|
||||
|
||||
## 二、判据①:清单字段与磁盘真值逐字相同
|
||||
|
||||
### 2.1 端到端(走**平台新口**,非直跑函数)
|
||||
|
||||
在 47 上**制造一次真实的脱钩**(㈠ 的现象),再看能不能修回来:
|
||||
|
||||
| 步 | 动作 | 真机读数 |
|
||||
|---|---|---|
|
||||
| 0 | 备份清单 | `/opt/dsh/backups/lib-p21/manifest-pre-p21.json` |
|
||||
| 1 | 把 `_dsh-local_storyforge` 的 `version` 改成 `0.2.0`(盘上是 `0.3.0`) | `TAMPERED flat=_dsh-local_storyforge version 0.3.0 -> 0.2.0` |
|
||||
| 2 | **只读**干跑 `reconcileSharedManifest()` | `DRYRUN checked=4 fixed=1`;`version ["0.2.0","0.3.0"]`(**后值 = 盘上真值**) |
|
||||
| 3 | 建临时 admin 会话(R4) | `MKSESS_USER=cce6d1cd-… / admin` |
|
||||
| 4 | `POST /api/plugins/shared/reconcile` | **`RECONCILE_HTTP=200`**;body `fixed:[{flat:_dsh-local_storyforge, version:["0.2.0","0.3.0"]}]` |
|
||||
| 5 | `GET /api/plugins/shared` 复核 | `storyforge version=0.3.0 tree=1c45c8418875 files=117`(**回到真值**) |
|
||||
| 6 | 与备份逐字 `diff` | 唯一差异 = `updatedAt`(**预期**:自愈也是一次写入) |
|
||||
| 7 | 清理 | `SESSIONS_DELETED=1`(临时会话用完即删) |
|
||||
|
||||
⚠️ **诚实记录**:第 6 步不是"零差异" —— 除 `updatedAt` 外逐字相同。`updatedAt` 的语义是"该条目最后一次写入",自愈改了三个描述字段 ⇒ 更新它是对的(且第二步起幂等:见 2.3)。
|
||||
|
||||
### 2.2 四个包的真值复核(管理面读数)
|
||||
|
||||
```
|
||||
_dsh-local_storyforge version=0.3.0 tree=1c45c8418875 files=117
|
||||
dsh-plugin-mcn-suite version=0.3.15 tree=939206c76abb files=657
|
||||
_dsh-local_im-conversation-tabs version=0.1.0 tree=678e0f36e100 files=5
|
||||
_softspark_dsh-file-preview version=2.0.0 tree=5cc4bdf7d1a9 files=881
|
||||
```
|
||||
|
||||
与盘上 `package.json` 逐个核对**一致**(`_dsh-local_storyforge :: version=0.3.0` 等,见 §四 取证命令)。
|
||||
|
||||
### 2.3 幂等(真机)
|
||||
|
||||
```
|
||||
MANIFEST_MD5_BEFORE=73435d1f81052604528537fa8064074b
|
||||
IDEMPOTENT_DRYRUN checked=4 fixed=0
|
||||
MANIFEST_MD5_AFTER =73435d1f81052604528537fa8064074b
|
||||
```
|
||||
|
||||
⇒ 真值已对齐后**一处不动**、字节不变(⛔ 不制造无谓写盘 ⇒ 不抖 `mtime`、不诱发节点重拉)。
|
||||
|
||||
### 2.4 判据①的**字面动作未做**(如实声明)
|
||||
|
||||
判据原文含「**真机跑一次 `share`** 后清单字段与磁盘真值逐字相同」。本棒**没跑那次 share**,原因:
|
||||
- 跑 `share` 只有两种走向 —— ① **幂等分支**(池内 tgz 与清单 `tgzSha256` 相同)⇒ **一个字节都不动**,验不到写入路径;② **替换分支** ⇒ 必须真发一个新版本,那会**改线上共享层内容**(影响面 = 全平台已可见该插件的用户)。
|
||||
- 等价替代(证明力不低于字面动作):**篡改 → 自愈 → 与原值逐字相同**(§2.1)+ **单测钉住写入侧口径**(§五)。
|
||||
|
||||
---
|
||||
|
||||
## 三、判据②:节点对账**连续两轮** `current`
|
||||
|
||||
在 106 上以节点身份触发两轮拉取(`POST /shared-layer/pull`,带该机自己的节点凭据;⛔ 凭据不出本机):
|
||||
|
||||
```
|
||||
PULL=200
|
||||
--- round1 --- {"ok":true,"missing":[],"stale":[],"extra":[],"pulled":[],"failed":[],"asOf":1790308081562}
|
||||
--- round2 --- {"ok":true,"missing":[],"stale":[],"extra":[],"pulled":[],"failed":[],"asOf":1790308084957}
|
||||
```
|
||||
|
||||
⇒ **连续两轮判 `current`**,⛔ 不再 `stale`/反复重拉(两轮间隔 3.4 s)。Manager 侧对账面同步复核:
|
||||
`NODE_REPORT={"hostId":"w-106","at":1790308084767,"authoritative":4,"missing":[],"stale":[],"extra":[],"failed":[]}`
|
||||
|
||||
---
|
||||
|
||||
## 四、判据③:回滚素材**名与内容自洽**
|
||||
|
||||
| 检查 | 真机读数 |
|
||||
|---|---|
|
||||
| 盘上既有素材(`mcn-suite/0.3.14.tgz`)内容版本 | `ASSET_CONTENT_VERSION=0.3.14` ⇒ **文件名 = 内容版本** ✓ |
|
||||
| `packSharedTree()` 在**真共享层包**上跑(产物落 `/tmp`,⛔ 共享层一字未动) | `SRC_TRUTH_VERSION=0.3.15` → `PACKED_CONTENT_VERSION=0.3.15` ⇒ 名实相符 ✓ |
|
||||
| 素材是否夹带 `node_modules` | `ARCHIVE_NODE_MODULES_HITS=0` ✓(`--exclude=node_modules` 生效) |
|
||||
| 体积 | `PACK_SIZE=1996116`(≈ 池内 tgz 1982036,同量级 ⇒ 未把依赖树打进去) |
|
||||
|
||||
**接线审计**(部署后的 `lib/` 里,证明新函数**真的被 `doShare` 调用**):
|
||||
|
||||
```
|
||||
1787: const asset = rollbackAssetPath(app.config.backupDir, flat, oldVersion);
|
||||
1790: packSharedTree(dir, asset);
|
||||
1806: version = packageVersionAt(incoming) ?? version;
|
||||
1815: backupTgz = rollbackAssetPath(app.config.backupDir, flat, version);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、判据④:单测钉住两处口径
|
||||
|
||||
`test/shared-layer-sync.test.mjs` 新增 **5 例**(第 ③ 组「台账两条语义缺陷」):
|
||||
|
||||
| 用例 | 钉住什么 |
|
||||
|---|---|
|
||||
| `台账口径(㈠)` | `version` 取**盘上实体现读**:最浅 `package.json` 优先;读不到 / 无 `version` / 非法 JSON ⇒ `null`(⛔ 不编造、⛔ 不抛) |
|
||||
| `台账自愈(㈠)` | 脱钩 ⇒ 三件(`version`/`treeSha256`/`fileCount`)一起修;**其余字段原样保留**;再跑幂等 |
|
||||
| `台账自愈: 双向只报告` | 清单有盘上无 ⇒ `missingOnDisk`(⛔ 不删条目);盘上有清单无 ⇒ `extrasOnDisk`(⛔ 不新增);旁挂文件不算 `extra` |
|
||||
| `回滚素材(㈡)` | 命名 = **上一版版本号**;内容 = 旧实体;⛔ 不含 `node_modules` |
|
||||
| `反证:不许落进被归档目录` | 落点在被归档树内 ⇒ 抛 `pack_into_self` |
|
||||
|
||||
---
|
||||
|
||||
## 六、门禁与部署
|
||||
|
||||
| 项 | 读数 |
|
||||
|---|---|
|
||||
| `npm run build` | rc=0 |
|
||||
| `npm test` | **589 tests / 587 过 / 0 败 / 2 跳过**(第 20 棒 584 ⇒ **+5 用例**) |
|
||||
| `npm run check:layering` | ✅ **无新增违规**(现存 5 条全在基线内) |
|
||||
| 部署 | 47 **一件**(`lib/web/routes/business-plugins.js`,md5 `3a752e01e4f16b4e6d6c47c305362a51` 与本地**逐字一致**) |
|
||||
| 重启 | `systemctl restart dshs` ⇒ `active` · `NRestarts=0` · 门户 `PORTAL_HTTP=200` |
|
||||
| 新口注册 | `POST /api/plugins/shared/reconcile` 无凭据 ⇒ **401**(⇒ 路由已注册且门禁生效;404 才是没注册) |
|
||||
| 备份 | `/opt/dsh/backups/lib-p21/47-pre-p21-20260925-114554.tgz` + `manifest-pre-p21.json` |
|
||||
|
||||
⛔ **未做**:commit / push|改 `src/web/routes/im.ts`(零改动)|手改用户 `home` 下的 profile / `.pnpm`|清理 `.dsh-module-fallback` 人工软链(§5-18 待拍板)。
|
||||
|
||||
---
|
||||
|
||||
## 七、未闭合 / 本棒新发现
|
||||
|
||||
1. 🟡 **106 侧的 `lib/web/routes/business-plugins.js` 与 47 不一致**(本棒顺带读数):47 投前 `4109ba34…`(124222 B)/ 投后 `3a752e01…`(新),而 **106 = `70d5c0f2…`(114041 B,mtime 09-25 00:28)** ⇒ 106 上该文件**落后好几个版本**。本棒**只投 47**(改动是 Manager 侧台账逻辑,106 是 worker;⛔ 不擅自改别处的部署面)。⇒ 建议下一棒做一次**两机 `lib` 差异面核对**。
|
||||
2. 🟡 **106 的新代码仍未生效**(§5-19 延续):`dshs-worker` 未重启 ⇒ 重启前的 106 用户仍会撞第 20 棒修掉的 `EPERM`。⛔ 重启会中断 106 在线实例 ⇒ **待用户给窗口**。
|
||||
3. 🔴 **㈡ 的"完整替换路径"未在真机跑通**:47 现有包在备份面**都已有同名素材**(上传钩子时代落的)⇒ 新逻辑走"已存在则跳过"⇒ 真机上**打包分支未被触发**(其正确性由单测 + §四 的等价复现保证)。要真机跑通需**真发一个新版本** ⇒ 会改线上共享层内容 ⇒ 本棒未做。
|
||||
4. 🔴 **待拍板(延续)**:§5-18 历史人工兜底软链是否清理|§5-14 `@softspark/dsh-file-preview` 可见性(用户 11:0x 口径已收口标记问题,但它已在共享层 ⇒ 全员可见)。
|
||||
5. ⚠️ **已知边界(写进代码注释)**:新旧**同版本号**重投时,"上一版素材"与"当前版本快照"**同名** ⇒ `backupTgz` 会指向旧内容;该场景"回到上一版"本就无意义,故不造名区分。
|
||||
|
||||
---
|
||||
|
||||
## 八、清理与卫生
|
||||
|
||||
- 47 `/tmp`:`p21-e2e.sh` / `p21-e2e.ok` / `p21-verify.sh` / `p21-mksess*` ⇒ **全部已删**(`TMP_P21_CLEAN`)。
|
||||
- 临时 admin 会话:`SESSIONS_DELETED=1` ⇒ **残留 0**(R4)。
|
||||
- 备份面保留(回滚素材):`/opt/dsh/backups/lib-p21/`(2 件)。
|
||||
- 服务状态:`dshs=active` · `dshs-worker=active`。
|
||||
- 本工作区中间产物 ⇒ `tmp/p21-*.sh`(收口清理,见 §0 收口行)。
|
||||
|
||||
---
|
||||
|
||||
## 九、复现命令(第三方可照跑)
|
||||
|
||||
```bash
|
||||
# ① 清单 ↔ 磁盘真值(只读)
|
||||
ssh bt-server 'cd /opt/dshs && node --input-type=module -e "
|
||||
const m=await import(\"/opt/dshs/lib/web/routes/business-plugins.js\");
|
||||
const fs=await import(\"node:fs\");const root=\"/var/lib/dshs/bundled-plugins\";
|
||||
const {report}=m.reconcileSharedManifest(root, JSON.parse(fs.readFileSync(root+\"/.manifest.json\",\"utf8\")));
|
||||
console.log(\"checked=\"+report.checked+\" fixed=\"+report.fixed.length, JSON.stringify(report.fixed));"'
|
||||
|
||||
# ② 节点两轮对账(在 106 上,用本机凭据)
|
||||
ssh test106 'set -a; . /etc/dshs-worker.env; set +a; for i in 1 2; do
|
||||
curl -s -X POST -H "x-dsh-agent-token: $DSHS_CLUSTER_AGENT_TOKEN" \
|
||||
http://127.0.0.1:19000/shared-layer/pull -w " HTTP=%{http_code}\n"; done'
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# 第 19 棒交付件 · 基础插件「默认开启+不可关闭」机制 + 回滚缺失侧修复
|
||||
|
||||
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`|**线**:插件投放与分库线(入口 `接续入口_插件投放与分库线_20260922.md`)
|
||||
> **轮次**:第 19 棒 · 执行棒(2026-09-25 10:2x–10:5x)|**代码仓**:`D:\github\dsh_shenxian`(生产口径 `dshs`)
|
||||
> **结论一句话**:第 1 件**完成并上线**;第 2 件**机制代码面全落并上线**,真机判据 **2 ✅ / 1 ✗ / 1 未取证** ⇒ **⛔ 不宣称验收通过**;同时把 pnpm `exit 255` 的根因从"原因不明"推进到**有具体错误码指向**(未修,交第 20 棒)。
|
||||
|
||||
---
|
||||
|
||||
## 一、第 1 件 · 回滚"缺失侧"(P0)✅ 完成
|
||||
|
||||
**问题**(第 18 棒真机实证):`off` 撞 pnpm 偶发失败 ⇒ `package.json` 里声明还在、`node_modules/<pkg>` 软链已被删 ⇒ 实例启动 `Error: dsh: cannot resolve profile bundle "dsh-plugin-mcn-suite"` ×5 → `crash-loop-circuit-open`(冷却 600 s)。旧 `restoreProfile()` 只做"删多余",**不重建缺失**。
|
||||
|
||||
**修法**(`src/supervisor/plugin-assembly.ts`):新增 `linkRefsOf(dir)` / `rebuildMissingDepLinks(dir)`,接入 `restoreProfile()`(在 `pruneOrphanDepLinks` 之后)+ `applyProfileChanges()` 收尾,并把重建结果作为 `repaired[]` 回给调用方。
|
||||
|
||||
**四条不变量**(判据,单测钉住):
|
||||
|
||||
| # | 不变量 | 实现要点 |
|
||||
|---|---|---|
|
||||
| 1 | **只补 `link:` 声明** | 遍历 `dependencies` 里 `link:<绝对路径>` 的条目;`file:` / registry 依赖一律不动 |
|
||||
| 2 | **只补"缺"** | `lstatSync` 命中即 `continue`(已正确 ⇒ kept,不重写) |
|
||||
| 3 | **⛔ 不建悬空链** | 目标 `statSync` 不是目录 ⇒ 跳过(宁可缺,不留指向空气的软链) |
|
||||
| 4 | **不碰真目录、不跑 pnpm** | 纯 `symlinkSync`;子路径 `@scope/x` 先 `mkdirSync(recursive)`;`node_modules` 不存在 ⇒ 整体跳过;属主 `chownSync` 尽力而为(非 root 静默跳过)、⛔ 回滚路径上不许再抛 |
|
||||
|
||||
**单测**:`test/plugin-assembly.test.mjs` 第 ⑨ 组 2 例(缺链重建 / 不造半安装态)+ 第 ⑩ 组 2 例(落盘拒绝 / 拒绝码同名);`S3 请求体形状` 补 `essential: false`。`npm test` = **577 过 / 0 败 / 2 跳过**(第 18 棒基线 575 ⇒ +2)。
|
||||
|
||||
---
|
||||
|
||||
## 二、第 2 件 · 基础插件「默认开启 + 用户不可关闭」(用户 09:3x 拍板)
|
||||
|
||||
### 2.1 第 0 步:真机确证"dsh 自带插件清单"(⛔ 不许跳的那一步)
|
||||
|
||||
| 查什么 | 47 上的实测结果 |
|
||||
|---|---|
|
||||
| `@deepseek-ai/dsh` 的 `package.json` | **不声明任何插件清单** —— 无 `dsh.plugins` / `bundled` 字段,只有 `configTrees` |
|
||||
| `node_modules/@deepseek-ai/*` | 是**库依赖**(`dsh-base` / `dsh-web-app` 等运行时 / UI 包),不是"自带业务插件" |
|
||||
| `@softspark` 在 dsh 包内 | **零命中** |
|
||||
| **可比对的平台语义(唯一)** | 门户 `/opt/dshs/web/admin.html`「运行环境」页用 `i.removable ? pa.removable : pa.required` 渲染,文案 = **「平台必备」/「可移除」** 二分,并有"「平台必备」项请勿删" |
|
||||
|
||||
⇒ 判定:**"基础插件"不是 dsh 的既有概念,无"自带清单"可枚举**;机制**必须数据驱动**(台账身份标记),⛔ 不硬编码包名清单。
|
||||
|
||||
### 2.2 落地形态(上线件)
|
||||
|
||||
- **身份存台账**:`src/db/schema.ts` **v17** —— `business_plugins.essential`(SQLite `INTEGER NOT NULL DEFAULT 0` / PG `BOOLEAN NOT NULL DEFAULT FALSE`);`types.ts` / `repo.ts` / `pg.ts` / `sqlite.ts` / `adapter.ts` 五件加列读出口与 `setBusinessPluginEssential`。
|
||||
- **门禁两处(双保险)**:① 路由 `POST /api/plugins/mine/apply`:对基础插件的 `enabled:false` **直接拒** ⇒ HTTP **409** `{error:'essential_plugin', pluginId, message}` + 写 `disable_denied_essential` 审计;② 装配层 `applyProfileChanges()`:结构性跳过并回 `refused: [{id, reason:'essential_plugin'}]`(⇒ 即便别的调用方绕过路由,也关不掉)。
|
||||
- **播种三条路径**:审批钩子 `POST /api/admin/users/:id/approve`(fire-and-forget)/`POST /api/plugins/business/essential/sweep`(admin,`{userIds?}` 省略 = 全部公开用户)/`/mine/apply` 顺带。
|
||||
- **跨机**:`remote-spawner.ts` 请求体带 `essential` ⇒ `worker/agent.ts` 严格解析(只认布尔 `true`)。
|
||||
- **门户 UI**:`poc/business-plugins/lib/client.js` —— 勾选框 `disabled`(基础插件不给操作入口)、状态徽章改渲染 **「平台基础插件 · 不可关闭」**(身份标注优先于启停徽章)+ `title`;版本 `0.3.25 → 0.3.26`。
|
||||
|
||||
### 2.3 真机判据(47,临时 admin 会话,用完即删)
|
||||
|
||||
| 判据 | 读数 | 结论 |
|
||||
|---|---|---|
|
||||
| ④ 关闭请求 = 具名拒绝且状态不变 | `http=409` + `{"error":"essential_plugin", …}`;拒绝前后 `/mine` **完全一致** | ✅ **通过** |
|
||||
| ② 台账标记生效 | admin 列表 `@softspark/dsh-file-preview essential=True version=2.0.0` | ✅ **通过** |
|
||||
| ① 默认播种 ⇒ 该实例默认 `enabled` | sweep 回 `{"ok":false,"seeded":1,"failed":1, error:"pnpm_failed"}`;`/mine` 仍 `enabled=False` | 🔴 **未通过**(被 pnpm 挡住) |
|
||||
| ③ 门户 UI 无关闭入口 | 0.3.26 tgz **已 pack、未投放** ⇒ 无读数 | 🔴 **未取证**(⛔ 无读数不宣称通过) |
|
||||
|
||||
---
|
||||
|
||||
## 三、附录 A · pnpm `exit 255` 根因新证据(⛔ 未修 · 交第 20 棒)
|
||||
|
||||
**取证命令(第三方可复现)**:
|
||||
|
||||
```bash
|
||||
ssh bt-server "journalctl -u dshs-worker --since '45 min ago' --no-pager | grep plugin-assembly"
|
||||
```
|
||||
|
||||
**读数**(Sep 25 10:52:09–10:52:21,**12 秒内连续 4 次** ⇒ 在该 profile 上**稳定复现,不是"偶发"**):
|
||||
|
||||
```
|
||||
[plugin-assembly] pnpm 失败 code=255 uid=114801 cwd=…/users/<uid>/home/profiles/web args=install --reporter silent stderr=- stdout=-
|
||||
```
|
||||
|
||||
**已排除的两块**:① **权限** —— 共享层链 `755`(`/var/lib/dshs` 711 → `bundled-plugins` 755 → 包目录 755),uid `114801` 对新旧包目录 `package.json` **A/B 双读均通过**;② **残留** —— apply 失败后 `dependencies` **已回滚干净**(无 `@softspark/dsh-file-preview` 残留)、`node_modules` 无半安装态。
|
||||
|
||||
**新证据(指向具体错误码)**:
|
||||
|
||||
| 事实 | 读数 |
|
||||
|---|---|
|
||||
| profile 记的 store | `node_modules/.modules.yaml` ⇒ `storeDir=/var/lib/dshs/users/<uid>/ws/.local/share/pnpm/store/v3`(**HOME=ws 时代**产物,Sep 11) |
|
||||
| 平台跑 pnpm 时的 HOME | `src/supervisor/plugin-assembly.ts:499` ⇒ `env HOME=${homeDir}`(= `<user>/home`) |
|
||||
| 该 HOME 下的默认 store | `/var/lib/dshs/users/<uid>/home/.local/share/pnpm/store/v3` ⇒ **No such file or directory** |
|
||||
| 同仓已有的解法 | `scripts/ensure-biz-plugins.cjs:existingStoreDir()` —— 注释**明写** `ERR_PNPM_UNEXPECTED_STORE`(档案 57 实测):pd 记录 store 与解析到的 store 不一致时,pnpm **直接拒绝** |
|
||||
|
||||
⇒ **高度指向 `ERR_PNPM_UNEXPECTED_STORE`**(立即拒绝特征 ≈2 s 与读数吻合);**正解 = 平台路径复刻脚本里的 store 口径**(读 `.modules.yaml` ⇒ 传 `--store-dir`),而非只补 reporter。**为什么一直"看起来像偶发"**:只有 **HOME 换代(ws → home)前建的存量 profile** 会命中。
|
||||
|
||||
---
|
||||
|
||||
## 四、未取证 / 未修清单(如实)
|
||||
|
||||
| # | 项 | 状态 |
|
||||
|---|---|---|
|
||||
| 1 | 基础插件判据 ①(默认播种 ⇒ `enabled`) | 🔴 未通过(pnpm) |
|
||||
| 2 | 基础插件判据 ③(门户 UI 无关闭入口) | 🔴 未投放、未取证 |
|
||||
| 3 | pnpm `exit 255` | 🔴 未修(根因已成形,交第 20 棒) |
|
||||
| 4 | `npm run verify` 尾段 `verify-platform-admin-section.mjs` | ⚠️ **既有缺陷**(同文件 `require()` + top-level `await` ⇒ `ERR_AMBIGUOUS_MODULE_SYNTAX`;单独跑同样失败,与本棒无关) |
|
||||
| 5 | `@softspark/dsh-file-preview` 已进共享层 ⇒ **所有登录用户可见**(可自助启用);若"先启用后标 essential"则用户**无法自行关闭** | ⚠️ 副作用,已上报(撤回口 = `…/:id/essential {essential:false}`) |
|
||||
| 6 | `@dsh-local/business-plugins` 0.3.26 的 UI 改动 | ⚠️ **未投放**(需 pnpm 修通后铺装) |
|
||||
|
||||
---
|
||||
|
||||
## 五、部署与卫生读数
|
||||
|
||||
- 47 部署 **11 件** / 106 **2 件**,md5 与本地**逐字一致**;备份 `/opt/dsh/backups/lib-p19/47-pre-p19-20260925-104848.tgz`、`106-pre-p19-20260925-105012.tgz`。
|
||||
- 47 `systemctl restart dshs` ⇒ **active · NRestarts=0 · 门户 200**;**v17 迁移已落**(`max(version)=17`),池内 `essential` **全 `f`**(存量行为零变化)。
|
||||
- ⚠️ 部署途中曾把 `lib/` 内容误落 `/opt/dshs` 根(散进 `supervisor/ web/ db/ worker/`)⇒ 已逐一 `rm` + `rmdir` 复原(四目录均系本次误建)后按 `-C /opt/dshs/lib` 重投;期间 47 **未用新代码重启**。
|
||||
- 清理:临时 admin 会话**残留 0**;⛔ 未 commit / push;⛔ `src/web/routes/im.ts` 零改动;⛔ 未手改用户 `home` 的 profile / `.pnpm`。
|
||||
|
||||
---
|
||||
|
||||
## 六、补充(2026-09-25 11:0x · 用户口径定案后,本文件第二节结论已更新)
|
||||
|
||||
用户原话:「**dsh 自带的就是,admin 添加的就不是**」⇒ 判定 = **按来源**(`@deepseek-ai/dsh` 自身声明的依赖 = 自带),⛔ 不是按"平台必备"。据此:
|
||||
|
||||
- ✅ **基础插件名单 = 空**:业务插件台账全量 **5 项**(`@dsh-local/im-conversation-tabs`、`@dsh-local/storyforge`、`@softspark/dsh-file-preview`、`dsh-plugin-mcn-suite`、`dsh-univer-office`)**全属 admin/平台添加**;`@softspark` 在**两机**的 dsh 安装树 + 全局 npm 根**零命中**(dsh 包内提到它的文件 0 个)。
|
||||
- ✅ **`@softspark/dsh-file-preview` 的 `essential` 标记已撤销**(真机 `changed:true`;台账复读 essentials **空**;用户面仍可见、可自助启停)⇒ 第二节的"判据 ①③"**已无主体**;门户 `0.3.26`("平台基础插件"徽章)**暂不投放**,产物留在 `poc/business-plugins/dsh-local-business-plugins-0.3.26.tgz`。
|
||||
- ✅ **新核实(免掉一次改造)**:dsh 自带的包**本来就关不掉** —— 装配入口对清单里每一项都查台账,不在候选池 ⇒ **404 `not_in_pool`**(`src/web/routes/business-plugins.ts:2299-2302`)⇒ **绕过 UI 自造请求也关不掉**;门户清单又不渲染它们 ⇒ **UI 层与接口层同时成立,无需新增代码**。
|
||||
- ⛔ 仍待办(未变):pnpm `exit 255` 可见化 + 根因修复 —— 已交第 20 棒(automation `421a8bad-…`)。
|
||||
- 取证脚本:`tmp/p19b-quota.sh`(台账全量 → 撤销标记 → 复核 → 用户面清单 → 清理临时会话)。
|
||||
@@ -0,0 +1,118 @@
|
||||
// 插件数据面 · **接入验收(静态面)** —— 用真实插件包跑平台真校验器(2026-09-25)
|
||||
//
|
||||
// 为什么要有这个脚本:`test/plugin-data-runtime.test.mjs` 验的是**端口判据**(用假执行器),
|
||||
// 它证明不了"一个真实插件的声明能不能过平台校验、库名换算对不对、DDL 生成得对不对"。
|
||||
// 本脚本拿**真插件包**(默认 `dsh-plugin-mcn`)调**平台真实现**(`lib/db/plugin-data/schema.js`),
|
||||
// 把接入的静态面一次跑完 ⇒ 可复现、可回归。
|
||||
//
|
||||
// 覆盖四项:
|
||||
// ① 声明解析与校验(`parseDeclFromDir`,与门户「检测第 3 项」同一个函数);
|
||||
// ② 库名换算 + 白名单复校(`pluginDbNameOf` / `assertPluginDbName`);
|
||||
// ③ 建库内 DDL 逐条(`declarationDdl`)—— 证明"建库后库里长什么样";
|
||||
// ④ 红线机器判据(`DB-03 §二`:包内 ⛔ 不得出现 SQL / DB 驱动)—— **现状读数**,供接入排期用。
|
||||
//
|
||||
// 跑法:`node 交付物/插件接入验收-静态面-20260925.mjs [插件包目录]`
|
||||
// ⚠️ 本脚本**只读**:不建库、不连 PG、不改插件包、不上传。
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
/** 平台代码仓(真实现的来源)—— 与工作区无关,按绝对路径取。 */
|
||||
const REPO = process.env.DSHS_REPO ?? 'D:/github/dsh_shenxian'
|
||||
const PKG_DIR = process.argv[2] ?? 'D:/dshworkspace/plugin_package/dsh-plugin-mcn'
|
||||
const PKG_NAME = process.env.PKG_NAME ?? 'dsh-plugin-mcn'
|
||||
|
||||
const schema = await import(pathToFileURL(join(REPO, 'lib/db/plugin-data/schema.js')).href)
|
||||
|
||||
/* ── 输出小件(结构化、可 grep)───────────────────────────────────────────── */
|
||||
|
||||
const rows = []
|
||||
const say = (tag, text) => { rows.push(`${tag} ${text}`); console.log(`${tag} ${text}`) }
|
||||
const PASS = (t) => say(' ✅', t)
|
||||
const FAIL = (t) => say(' ❌', t)
|
||||
const INFO = (t) => say(' ·', t)
|
||||
|
||||
console.log(`═══ 插件数据面 · 接入验收(静态面)═══`)
|
||||
console.log(`插件包:${PKG_DIR}`)
|
||||
console.log(`包 名:${PKG_NAME}`)
|
||||
console.log(`校验器:${REPO}/lib/db/plugin-data/schema.js(平台真实现)\n`)
|
||||
|
||||
/* ── ① 声明解析与校验(与门户「检测第 3 项」同源)─────────────────────────── */
|
||||
|
||||
const parsed = schema.parseDeclFromDir(PKG_DIR, PKG_NAME)
|
||||
INFO(`声明来源(origin)= ${parsed.origin}`)
|
||||
if (parsed.level !== 'ok' || parsed.decl === null) {
|
||||
FAIL(`声明未通过校验:level=${parsed.level}`)
|
||||
for (const f of parsed.findings) FAIL(` ${f.target} [${f.code}] ${f.detail}`)
|
||||
if (parsed.note !== null) INFO(`note:${parsed.note}`)
|
||||
console.log('\n结论:❌ 接入未通过(声明层)')
|
||||
process.exit(1)
|
||||
}
|
||||
const decl = parsed.decl
|
||||
PASS(`声明通过校验:${decl.tables.length} 张表 · schemaVersion=${decl.schemaVersion} · source=${decl.source}`)
|
||||
for (const t of decl.tables) {
|
||||
PASS(` 表 ${t.name}(scope=${t.scope} · ${t.columns.length} 列 · ${t.indexes.length} 索引 · 物理名 ${t.physicalName})`)
|
||||
}
|
||||
|
||||
/* ── ② 库名换算 + 白名单复校 ────────────────────────────────────────────── */
|
||||
|
||||
const dbName = schema.pluginDbNameOf(PKG_NAME)
|
||||
try {
|
||||
schema.assertPluginDbName(dbName)
|
||||
PASS(`库名换算:${PKG_NAME} ⇒ ${dbName}(过白名单 ${schema.PLUGIN_DB_RE.source})`)
|
||||
} catch (err) {
|
||||
FAIL(`库名未过白名单:${String(err.message)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (decl.dbName !== dbName) FAIL(`⚠️ 声明自算的库名(${decl.dbName}) 与换算(${dbName}) 不一致`)
|
||||
else PASS('声明自算库名与换算一致(这正是运行时「库名不一致即拒」判据的正面用例)')
|
||||
|
||||
/* ── ③ 建库内 DDL 逐条 ──────────────────────────────────────────────────── */
|
||||
|
||||
const ddl = schema.declarationDdl(decl)
|
||||
console.log(`\n ── 建库后库内 DDL(${ddl.length} 条)──`)
|
||||
for (const line of ddl) console.log(` ${line.replace(/\s+/g, ' ').slice(0, 150)}`)
|
||||
const hasPk = ddl.filter((d) => d.startsWith('CREATE TABLE')).every((d) => /PRIMARY KEY \("[a-z_]+", "[a-z_]+"\)/.test(d))
|
||||
if (hasPk) PASS('每张表都是「(归属列, 声明第一列)」复合主键 —— ⛔ 无代理自增 id')
|
||||
else FAIL('有表的主键不是「(归属列, 第一列)」形态')
|
||||
const hasAcl = ddl.filter((d) => d.startsWith('CREATE TABLE')).every((d) => d.includes('"user_id"') || d.includes('"room_id"'))
|
||||
if (hasAcl) PASS('归属列已由内核补齐(scope:user ⇒ user_id)')
|
||||
else FAIL('缺归属列')
|
||||
|
||||
/* ── ④ 红线机器判据(现状读数)──────────────────────────────────────────── */
|
||||
|
||||
const SQL_RE = /CREATE TABLE|ALTER TABLE|DROP TABLE|better-sqlite3|from 'pg'|require\('pg'\)/
|
||||
function scan(dir, depth = 0) {
|
||||
const hits = []
|
||||
if (depth > 4) return hits
|
||||
let entries
|
||||
try { entries = readdirSync(dir) } catch { return hits }
|
||||
for (const name of entries) {
|
||||
if (name === 'node_modules' || name.startsWith('.')) continue
|
||||
const abs = join(dir, name)
|
||||
let st
|
||||
try { st = statSync(abs) } catch { continue }
|
||||
if (st.isDirectory()) { hits.push(...scan(abs, depth + 1)); continue }
|
||||
if (!/\.(js|mjs|cjs|ts)$/.test(name)) continue
|
||||
let text
|
||||
try { text = readFileSync(abs, 'utf8') } catch { continue }
|
||||
const lines = text.split('\n')
|
||||
lines.forEach((line, i) => {
|
||||
if (SQL_RE.test(line) && !line.trimStart().startsWith('*') && !line.trimStart().startsWith('//')) {
|
||||
hits.push(`${abs.replace(PKG_DIR, '.')}:${i + 1}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
return hits
|
||||
}
|
||||
const sqlHits = scan(PKG_DIR)
|
||||
console.log('')
|
||||
INFO(`红线扫描(DB-03 §二)命中 ${sqlHits.length} 处 —— 这是**现状读数**,不判本次声明成败:`)
|
||||
for (const h of sqlHits.slice(0, 12)) console.log(` ${h}`)
|
||||
if (sqlHits.length > 12) console.log(` …(另有 ${sqlHits.length - 12} 处)`)
|
||||
if (sqlHits.length === 0) PASS('包内零 SQL / 零 DB 驱动 ⇒ 已符合接入前置条件')
|
||||
else INFO('⚠️ 存量本地库(better-sqlite3)仍在 ⇒ 接入数据面须先改读写走内核 API(见交付件 §六)')
|
||||
|
||||
console.log(`\n结论:✅ 声明层接入验收通过(库名 ${dbName});红线项数 ${sqlHits.length}(需按接入计划处理)`)
|
||||
@@ -0,0 +1,123 @@
|
||||
# 插件接入验收(静态面·用 MCN 插件)+ MCN 工作台移动端独占屏幕(2026-09-25)
|
||||
|
||||
> **一句话**:① 拿**真插件**(`dsh-plugin-mcn`)走了一遍接入的**静态面验收** —— 声明过校验、库名换算正确、
|
||||
> DDL 正确,同时抓出**两条会让人返工的实测结论**;② 修掉"MCN 工作台在移动端一半会话一半插件、没法用"
|
||||
> —— 窄屏改为**独占**;③ `08-插件开发与对接规范` 新增 **§4-补 移动端 / 窄屏兼容(硬要求)**。
|
||||
> **两件都未部署**(本机无 PG、无容器 ⇒ 真机验收与部署合并为下一棒)。
|
||||
|
||||
---
|
||||
|
||||
## 一、接入验收(静态面)—— 用真插件跑平台真校验器
|
||||
|
||||
**跑法(可复现)**:
|
||||
|
||||
```bash
|
||||
node 交付物/插件接入验收-静态面-20260925.mjs # 默认插件 = D:/dshworkspace/plugin_package/dsh-plugin-mcn
|
||||
node 交付物/插件接入验收-静态面-20260925.mjs <插件包目录> # 换插件
|
||||
```
|
||||
|
||||
判据来源**不是**本仓库单测,而是**平台真实现** `lib/db/plugin-data/schema.js`(与门户「检测第 3 项」同一个函数)
|
||||
⇒ 这就是"插件会话照文档做"会遇到的同一套校验。脚本**只读**:不建库、不连 PG、不改插件包、不上传。
|
||||
|
||||
### 验收结果(全绿)
|
||||
|
||||
```
|
||||
声明来源 origin = yaml-file ← 必须由 package.json 的 dsh.data.schema 指向
|
||||
✅ 声明通过校验:3 张表 · schemaVersion=1 · source=./dsh.data.yaml
|
||||
✅ hot_accounts scope=user · 12 列 · 2 索引 · 物理名 p_dsh_plugin_mcn_hot_accounts
|
||||
✅ account_videos scope=user · 15 列 · 2 索引 · 物理名 p_dsh_plugin_mcn_account_videos
|
||||
✅ account_analysis scope=user · 5 列 · 1 索引 · 物理名 p_dsh_plugin_mcn_account_analysis
|
||||
✅ 库名换算:dsh-plugin-mcn ⇒ dshs_pl_dsh_plugin_mcn(过白名单 ^dshs_pl_[a-z][a-z0-9_]{0,40}$)
|
||||
✅ 声明自算库名与换算一致(运行时「库名不一致即拒」判据的正面用例)
|
||||
✅ 每张表都是「(归属列, 声明第一列)」复合主键 —— ⛔ 无代理自增 id
|
||||
✅ 归属列已由内核补齐(scope:user ⇒ user_id)
|
||||
```
|
||||
|
||||
**建库后库内 DDL(8 条)**:3 张表 + 5 个索引,形如
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS "p_dsh_plugin_mcn_hot_accounts" (
|
||||
"user_id" TEXT NOT NULL, "created_at" BIGINT NOT NULL, "updated_at" BIGINT NOT NULL,
|
||||
"account_key" TEXT NOT NULL, … , PRIMARY KEY ("user_id", "account_key"));
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "p_dsh_plugin_mcn_hot_accounts_account_key_uq_1" ON … ("account_key");
|
||||
```
|
||||
|
||||
### 🔴 验收抓出的两条实测结论(**会让人返工,已回写文档**)
|
||||
|
||||
1. **只把 `dsh.data.yaml` 放进包里 = 不生效**。本次第一跑 `origin = none` —— 因为没有 `package.json` 的
|
||||
`dsh.data.schema` 指向。平台**不自动探测**该文件名(`schema.ts:701-746` 现读)。
|
||||
⇒ `08 规范 §3-1` 已写这条(**我这次是没先读文档才踩的,反证该条写得对**);同时提醒:`files` 字段
|
||||
也要带上 `dsh.data.yaml`,否则 `npm pack` 不打包它。
|
||||
2. **存量表的自增 `id` 不能照搬**。数据面主键 = `(归属列, 声明第一列)`,**没有代理 id 列**
|
||||
⇒ MCN 原有的 `id INTEGER PRIMARY KEY` 必须换成**业务键**(本次声明用 `account_key` / `aweme_id` /
|
||||
`analysis_key`,取值规则 = `<账号名>` 或 `<账号名>:<时间戳>`)。这是"存量插件接入"最先撞的一堵墙。
|
||||
|
||||
### 验收同时得到的"接入前置条件"读数
|
||||
|
||||
**红线扫描(`DB-03 §二`)命中 21 处**,全在 `lib/db.js`(`CREATE TABLE` × 11 + 迁移语句等)
|
||||
⇒ **MCN 当前形态不合规**(DB-03 红线 6:⛔ 不得以实例本地库为权威)。接入路径 = 把 `db.js` 的读写
|
||||
换成经内核 API(`/api/im/data/*` 或 SDK)⇒ 属**插件侧改造**,工作量大,需单独排棒。
|
||||
|
||||
---
|
||||
|
||||
## 二、MCN 工作台 · 移动端独占屏幕
|
||||
|
||||
### 病象与根因
|
||||
|
||||
**病象**(用户实测):手机上点开 MCN 工作台 ⇒ **一半会话窗口、一半插件窗口**,两侧都不可用。
|
||||
|
||||
**根因**(代码级,`dsh-plugin-mcn/lib/client.js`):
|
||||
工作台把宿主的会话区搬进一个 `flex-direction:row` 的容器(`.mcnNav_split`),
|
||||
再追加一个宽度 **50%**(`.mcnNav_panel`,行内 `style.width`)的面板 ⇒
|
||||
**窄屏上两栏各半**,而面板内表格内容宽 **720px**、操作按钮也摆不下 ⇒ 不可用。
|
||||
插件此前**没有任何视口断点**(只有主题判断)。
|
||||
|
||||
### 改法(纯样式 · 单文件 · 独立成段)
|
||||
|
||||
新增一段独立样式 `data-plugin-css="mcn-mobile"`(⛔ 不碰既有三段样式),一条
|
||||
`@media (max-width:900px)` 内含四组规则:
|
||||
|
||||
| 组 | 规则 | 为什么必须这么写 |
|
||||
|---|---|---|
|
||||
| ① **独占** | `.mcnNav_split{display:block}`|会话区与 `.mcnNav_resizer` `display:none!important`|面板 `width:100%!important` | 面板宽度是**行内 style** ⇒ 只有 `!important` 盖得住;宿主给会话区设了 `display:contents` ⇒ 隐藏也必须 `!important` |
|
||||
| ② 触摸目标 | 按钮/标签 ≥44px;图标按钮 44×44 | WCAG 2.5.5 / iOS HIG |
|
||||
| ③ 输入字号 | input/select/textarea `font-size:16px` | <16px 时 iOS Safari 聚焦会**放大整页** |
|
||||
| ④ 窄屏适配 | 弹窗 `width:calc(100vw-32px)`、`min-width:0`|宽弹窗与对比面板改竖排|统计条换行、字号收小|表格单元格间距放宽 | 原先 `min-width:320px` +固定宽度在 320px 视口上必然横向溢出 |
|
||||
|
||||
**为什么取 900px**(而非"手机断点"768):≤900px 时两栏各 <450px,表格/表单都不成立
|
||||
⇒ 桌面窄窗口与平板共享同一条判据,不维护第二套阈值。
|
||||
**宽屏(>900px)行为逐字不变** ⇒ 只做正向迭代。
|
||||
|
||||
**退出路径**:复用面包屑上既有的关闭按钮(已放大到 ≥44px 触摸目标)⇒ 关掉即恢复会话区,
|
||||
⛔ 不新增控件、⛔ 不动 React 结构(改动面 = 一段 CSS 数组,可单独回滚)。
|
||||
|
||||
### 本轮做过的校验(**不是**真实浏览器验收)
|
||||
|
||||
```
|
||||
node --check lib/client.js → ✅ 语法通过(改动前后均过)
|
||||
mcn-mobile 段存在 / @media 规则 1 条 → ✅
|
||||
```
|
||||
⚠️ **未做**:真实浏览器/真机视觉验收(见下)。
|
||||
|
||||
---
|
||||
|
||||
## 三、文档(同批)
|
||||
|
||||
| 文件 | 变更 |
|
||||
|---|---|
|
||||
| `01-规范/08-插件开发与对接规范.md` | **新增 §4-补 移动端 / 窄屏兼容(硬要求)**(断点表 + 四条硬判据 + 三条 ⛔ + 六条自测 + 参考实现指针)|`§7` 自测清单补「移动端专项」|头部状态块补一行|`§11` 变更记录补 2026-09-25 |
|
||||
| 同上(上一轮同批) | `§3-4` 数据面取数链路重写 + `§6` 错误码 + `§7` 取数自测 |
|
||||
|
||||
---
|
||||
|
||||
## 四、未做(⛔ 不许看成已闭环)
|
||||
|
||||
1. 🔴 **未部署、未真机验收**(本机无 PG、无容器 ⇒ 数据面建库/取数在本机跑不了)。合并成一棒:
|
||||
**部署 47 → 数据面真机自测(四拒一空 + insert/find 回读)→ MCN 工作台移动端视觉验收
|
||||
(375×667 独占 / 能退出 / 无横向溢出 / 聚焦不缩放 / 横屏可用)**。
|
||||
2. **MCN 插件未改造为"数据面接入"**:仅补了**声明 + package.json 指向**(`dsh.data.yaml`、`dsh.data` 键、
|
||||
`files` 带上该文件)—— 读写仍走本地 SQLite(红线扫描 21 处)。⚠️ **未上传、未投放**
|
||||
⇒ 在线用户零影响。⚠️ **注意**:一旦把带声明的版本传到共享层,平台检测会判它"有数据面"
|
||||
⇒ **未建库前所有用户都无法启用**(门禁 ②)。**顺序必须是:先建库 ⇒ 再投放**。
|
||||
3. **移动端改动只覆盖 MCN 工作台**。其它"占用宿主区域"型插件(如门户入口/会话标签页)
|
||||
是否同样有窄屏问题 ⇒ **未普查**(§4-补 已把判据写成硬要求,可用于逐个复查)。
|
||||
@@ -0,0 +1,89 @@
|
||||
# 插件数据面 · **运行时取数口**落地(2026-09-25)
|
||||
|
||||
> **一句话**:插件会话说的"平台面向插件的数据库接入面没建成、取数这条链路不通"**属实**;
|
||||
> 本单把缺失的那一半(**运行时取数**)补齐 —— 库建好了但插件读不到写不进,现在通了。
|
||||
> **归属线**:插件投放与分库线(插件数据面取数口)|**会话**:`aliyun-dsh-server-插件数据面`
|
||||
> **代码基线**:`D:\github\dsh_shenxian`(生产口径 `dshs`)|⛔ 未 commit / 未 push / **未部署**
|
||||
|
||||
---
|
||||
|
||||
## 一、缺口的真身(先定性,再动手)
|
||||
|
||||
| 层 | 本单前状态 | 证据 |
|
||||
|---|---|---|
|
||||
| 建库/管理面 | ✅ 已建成(09-23) | `src/db/plugin-data/{schema,diff,datastore}.ts`;47 真机建库 `created` 71 ms |
|
||||
| **运行时取数** | 🔴 **零实现** | `ImDataPort` 全仓 **4 处命中**(接口定义 + 引入 + 两个可选字段),**无 `implements`/无实例化**;`new ImSdkHost()` 只在 `test/` 出现;**全仓无一处传 `data:`** ⇒ 插件调 `insert` 一律 `not-registered` |
|
||||
| 平台替代通路 | 🔴 无 | `datastore` 系列路由全是 `requireAdmin` **管理动作**,没有业务数据读写口 |
|
||||
|
||||
缺口成因是**接缝两不管**:IM 线 D 单原话「落成的是 `ImDataPort` 注入式端口(**真库端口属建库面,本棒不越界**)」
|
||||
⇒ 运行时实现被推给建库面,而建库面只做了管理面。两边都以为对方做。
|
||||
|
||||
## 二、交付物(5 新 + 2 改)
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `src/db/plugin-data/runtime.ts`(新) | **端口实现**:SQL 生成(标识符一律引用)、**归属强制注入**、房间 ACL、游标分页、配额、逐次审计、失败具名(⛔ 不抛给插件) |
|
||||
| `src/db/plugin-data/registry.ts`(新) | **装配**:声明现解(按 TTL 缓存,⛔ 不信记录字段)+ 库名**三处一致性**校验 + 每插件库连接池(`max=2`,照定稿 §五)+ 就绪判读 |
|
||||
| `src/web/routes/plugin-data.ts`(新) | **实例可达的 HTTP 面**:`/api/im/plugin-data/status` + `/api/im/data/*`(查/计/写/改/删) |
|
||||
| `sdk/im-plugin-client/index.mjs` + `package.json`(新) | **插件侧 SDK v1(REST 面)**:零依赖、可 vendoring、⛔ 不含平台实现与凭据 |
|
||||
| `test/plugin-data-runtime.test.mjs`(新 18 例) | 端口判据回归(假执行器,⛔ 不连 PG,可机械复现) |
|
||||
| `test/im-plugin-client.test.mjs`(新 6 例) | SDK 契约(头注入 / 失败折形 / 未配置即拒) |
|
||||
| `package.json`(改) | 两组新测试挂进 `test` / `verify` 清单 |
|
||||
| `src/web/server.ts`(改) | 注册 `pluginDataRoutes`(1 行 + 注释) |
|
||||
|
||||
**文档**(同批落文):`01-规范/08-插件开发与对接规范.md §3-4`(**重写 —— 插件会话照这节写**)|
|
||||
`03-数据库/DB-03-插件数据面规范.md §五`(落地状态 + **`id` 语义** + 三条偏差)+ §十(待定项按实况更新)|
|
||||
`01-规范/09-IM插件SDK与扩展点契约.md §3-1`、`02-架构设计/数据-分库与权威存储架构.md §六`(加落地指针)。
|
||||
|
||||
## 三、四条关键判据(本单的"不许含糊")
|
||||
|
||||
1. **`id` = 声明里的第一列**(主键是 `(归属列, 第一列)` 的复合键,**无代理 id 列**)⇒ 第一列必须选稳定唯一业务键;
|
||||
该列**不可被 `patch` 改**。这条此前**没有任何文档写明**,是本次从 `createTableSql` 反推后**写进 DB-03 §五** 的。
|
||||
2. **归属由内核写,插件覆盖不了**:`scope:user` 给他人 `user_id` ⇒ `bad-args`;`scope:room` 必带 `room_id`
|
||||
且过成员判定(未注入成员判定 ⇒ **fail-closed**,不是放行)。
|
||||
3. **库名只信平台**:台账库名与声明换算**不一致即拒**(`db-name-mismatch`)⇒ 防"猜库名"与其后的一切。
|
||||
4. **失败具名**:契约失败一律 **200 + `{ok:false,reason,detail}`**(与 `ImDataPort` 同形);`4xx` 只留给身份/就绪。
|
||||
|
||||
## 四、验收证据(可复现)
|
||||
|
||||
```
|
||||
npm run build → rc=0(tsc 干净)
|
||||
node --test test/plugin-data-runtime.test.mjs → 18/18 过
|
||||
node --test test/im-plugin-client.test.mjs → 6/6 过
|
||||
node scripts/check-layering.mjs → ✅ 无新增违规
|
||||
```
|
||||
|
||||
⚠️ **全量 `npm test` 现状**:567 例中 **7 例失败,全部是环境性 `spawnSync <git|tar> EBUSY`**
|
||||
(本例只发生在调用 `execFileSync` 的子进程上;本单新增的 24 例零失败)。
|
||||
受影响文件 = `test/im-sdk.test.mjs`(3 例)+ `test/shared-layer-sync.test.mjs`(4 例),
|
||||
**与本单改动无关**(那两处代码路径本单未碰);在无该限制的环境下应可全绿。
|
||||
|
||||
## 五、权限影响评估(R5:新增对外面,故须出件)
|
||||
|
||||
- **新增面**=两个端点族:`/api/im/data/*`(业务数据读写)+ `/api/im/plugin-data/status`(只读判读)。
|
||||
- **鉴权**=**会话优先 + 实例凭据补位**(与 IM 面同序、同一本 `app.imInstanceTokens` 登记簿;
|
||||
头名与拒绝原因码取自 `src/im/instance-token.ts`,⛔ 不另写字面量)。
|
||||
- **可见面收窄三条**:① 只有**已建库**(台账 `state='ready'`)的插件能取数;② 归属列强制注入
|
||||
⇒ 越权得 0 行;③ 房间表复用内核成员表,非成员得 0 行。
|
||||
- **审计**:逐次落 `plugin_data_audit`(谁 / 自称哪个插件 / 动作 / 表 / 影响行数 / 拒绝原因);
|
||||
⛔ **不记行内容**(不把插件数据抄进平台日志)。
|
||||
- 🔴 **已知残余**:同一用户的实例内,插件 A 可**自称**插件 B(越不出用户边界)⇒ 补法 = 每插件令牌;
|
||||
**本期以逐次审计兜底**(可对账、可追责)。
|
||||
|
||||
## 六、未做(⛔ 不许看成"已闭环")
|
||||
|
||||
1. 🔴 **未部署到 47 / 106,未做真机验收** —— 代码与单测就绪 ≠ 生产可用。部署清单:
|
||||
`scp` 构建产物 → `restart dshs` → **真机自测四拒一空**(写他人 `user_id`/room 表缺 `room_id`/
|
||||
缺主键列/带 `created_at` 应各拒;非成员读 room 表应得 0 行)+ 一次成功的 `insert` + `find` 回读。
|
||||
2. **跨节点形态仍不通**:worker 上的实例 → Manager 侧数据面缺"实例→控制面"身份通道(`DB-03 §六` 自认)
|
||||
⇒ 当前可用形态 = **同机 / 本机**。
|
||||
3. 本期**未提供**:`im.data.tx`(同库多表事务)、聚合查询、软删 + 30 天保留、`memberRooms` 接线、
|
||||
与 agent 同族的写入预算。
|
||||
4. 本单**刻意不改** `src/web/routes/im.ts`(IM 线 D 单的验收判据 2 冻结了该文件 diff)
|
||||
⇒ 鉴权口径在 `plugin-data.ts` 独立实现。⚠️ **长期成本**:IM 线若改鉴权语义,本文件必须同步跟进;
|
||||
将来要"一份实现两处用",须把鉴权提成 `src/web/im-auth.ts` 由两边 import(那要先解 D 单那条冻结判据)。
|
||||
|
||||
## 七、下一棒(建议,等排)
|
||||
|
||||
**部署 + 真机验收**(一条,做完即闭环):按上面 §六-1 清单在 47 落地并逐条取证;
|
||||
若同时排"跨节点",则先做身份通道(属 IM 线 / 序47 的既有缺口,⛔ 不在本单范围)。
|
||||
@@ -0,0 +1,169 @@
|
||||
# 插件数据面取数口 · **47 真机验收** + 抓出接线缺陷(2026-09-25 · 第 15 棒)
|
||||
|
||||
> **一句话**:把第 14 棒落地的「运行时取数口」部署到 47 并**真机跑通**(四拒一空 + 回读 **7/7 PASS**),
|
||||
> 过程中抓出**一条会让取数口对全部插件失效的接线缺陷**(已修 + 已复验);
|
||||
> 第 2 件(MCN 移动端视觉验收)**未通过 —— 前提不成立**:移动端修复落在**未投放的包**里。
|
||||
> **归属线**:插件投放与分库线|**会话**:插件投放与分库线-第15棒|**交接单**:无(本棒为执行棒)
|
||||
|
||||
---
|
||||
|
||||
## 一、本轮做了什么
|
||||
|
||||
| # | 事项 | 结果 |
|
||||
|---|---|---|
|
||||
| 0 | 开工门禁 + 域锁 | preflight 判【D】2 个 → 归类后抢锁成功(域 `aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md` + `aliyun-dsh-server/.workbuddy`) |
|
||||
| 1 | 部署取数口到 47 | ✅ 5/5 md5 逐字一致 + `restart dshs` + 服务 active、门户 200 |
|
||||
| 2 | 数据面取数**真机验收** | ✅ **7/7 PASS**(四拒 + 一空 + insert→find 回读;归属列真值直连核) |
|
||||
| 3 | 抓出并修复**接线缺陷** | ✅ 已修 `src/web/routes/plugin-data.ts` + 重投 + 复验 |
|
||||
| 4 | MCN 移动端视觉验收 | 🔴 **未通过 —— 前提不成立**(修复不在已投放的包里,详见 §五) |
|
||||
| 5 | 夹具清理 | ✅ **零残留**(池 5 / 插件库 0 / 台账 0 / 会话 0 / tgz 已删) |
|
||||
|
||||
---
|
||||
|
||||
## 二、部署面证据(47 · `/opt/dshs`)
|
||||
|
||||
| 文件 | 47 与本地 md5 | 说明 |
|
||||
|---|---|---|
|
||||
| `lib/web/server.js` | `fcbbb9b3476499ba2d97841f0e6ebaed` | 与 47 原版**只差 7 行**(就是 `pluginDataRoutes` 的 import + `register` +注释)—— 已逐行 diff 确认 |
|
||||
| `lib/db/plugin-data/runtime.js` | `45daf7c5b2b6163e3d1e1fd110dca36d` | 新增 |
|
||||
| `lib/db/plugin-data/registry.js` | `44d3b5c424f7bc27a930454a945f6f06` | 新增 |
|
||||
| `lib/web/routes/plugin-data.js` | `a03394088f4ad90e571d4b5ede7a5f95` → 修复后 `fac8f4dfe0d5710705de6d614275c77e` | 新增 → 修复 |
|
||||
| `sdk/im-plugin-client/index.mjs` | `4a1a160156a90cc07696332fcc99d0cd` | 新增(落 `/opt/dshs/sdk/`) |
|
||||
|
||||
- **备份**:`/opt/dsh/backups/lib-p15/47-pre-p15-20260925-074345.tgz`(投放前)。
|
||||
- **重启**:`systemctl restart dshs` → `active`、`NRestarts=0`、门户 `200`。
|
||||
- **部署前先做了漂移比对**(避免把别的棒的东西带上线):`business-plugins.js` / `worker/agent.js` / `leased-spawner.js` / `config.js` 四件**与本地逐字一致** ⇒ 只投必要项。
|
||||
- ⚠️ **106 未部署**(本轮范围 = 47;取数口是 Manager 侧路由 ⇒ 106 不影响本轮验收)。
|
||||
|
||||
---
|
||||
|
||||
## 三、真机验收读数(47 · 调用者 = `guest`,插件身份头 = 包名)
|
||||
|
||||
夹具 = 一次性探测包 `dsh-plugin-pdprobe`(user 域 + room 域各一表;声明过平台**真校验器** → `level=ok`)。
|
||||
|
||||
| 断言 | 期望 | 实测(HTTP 200 + 契约体) |
|
||||
|---|---|---|
|
||||
| ① 写他人 `user_id` | 拒 | `ok:false, reason:bad-args`,detail=`不得写入他人的 user_id(归属列由内核注入)` |
|
||||
| ② room 表缺 `room_id` | 拒 | `ok:false, reason:bad-args`,detail=`房间维度表必须带 room_id` |
|
||||
| ③ 缺主键列 | 拒 | `ok:false, reason:bad-args`,detail=`缺少主键列「probe_key」(它是本表的行标识)` |
|
||||
| ④ 带 `created_at` | 拒 | `ok:false, reason:bad-args`,detail=`审计列「created_at」由内核维护,⛔ 不可自写` |
|
||||
| ⑤ 非成员读 room 表 | **0 行** | `ok:true`,`value:[]` ⇒ **0 行** |
|
||||
| ⑥ `insert` 正常行 | ok | `ok:true` |
|
||||
| ⑦ `find` 回读一致 | 1 行且字段一致 | `ok:true`,`value` 1 行,`note='v1'`、`payload={a:1}` |
|
||||
|
||||
**归属注入直连核**(不靠响应自证):`p_dsh_plugin_pdprobe_probe_user.user_id` **全部等于**
|
||||
调用者本人 `4092b965-…`;`probe_room` 行数 = **0**。
|
||||
|
||||
**DDL 侧证**(迁移回显):`PRIMARY KEY ("user_id", "probe_key")` ⇒ **`id` = 声明第一列**、`归属列 + 第一列` 复合键
|
||||
(无代理自增 id)—— 与 `DB-03 §五` 口径一致,**首次在真机上被看见**。
|
||||
|
||||
**对照**(负例):给**无声明**的真插件取数 ⇒ `409 plugin-data-unavailable / no-declaration`(不是 500,不是"假装成功")。
|
||||
|
||||
---
|
||||
|
||||
## 四、🔴 抓出的接线缺陷(P0 级 · 已修 + 已复验)
|
||||
|
||||
**现象**:库建好(`state=ready`)、插件身份头合法、鉴权通过 —— 但取数一律
|
||||
`409 no-declaration`:`插件「dsh_plugin_pdprobe」不在候选池或包内无数据面声明`。
|
||||
|
||||
**根因(两个名字空间被混用)**:
|
||||
|
||||
| 名字空间 | 取值 | 谁在用 |
|
||||
|---|---|---|
|
||||
| **池键 / 台账键** | **原始包名**:`dsh-plugin-pdprobe`、`@dsh-local/storyforge` | `business_plugins.id`、`plugin_datastores.plugin_id`(建库口、门户都用它) |
|
||||
| **`pluginId`(归一化)** | 去 scope + 下划线化:`dsh_plugin_pdprobe` | `pluginIdOf()`;用于**算库名/表名前缀** |
|
||||
|
||||
取数口把**归一化后的 id** 交给了 `PluginDataRegistry`,而 registry 内部拿这个值去
|
||||
`findBusinessPlugin` / `findPluginDatastore` **精确查**这两张表 ⇒ **永远查不到** ⇒
|
||||
**任何插件**调取数口都只会得到 `no-declaration`(不是这一个夹具的问题)。
|
||||
|
||||
**为什么单测没抓到**:`test/plugin-data-runtime.test.mjs` 用**假执行器**,直接构造声明对象,
|
||||
**绕过了路由层的 id 归一化** ⇒ 这条路径只有在真机上才会被走到。
|
||||
|
||||
**修法(最小)**:`src/web/routes/plugin-data.ts` 两处
|
||||
- `resolvePluginPort`:`pluginId: pluginIdOf(pkgName)` → `pluginId: pkgName`(原始包名)
|
||||
- `/plugin-data/status`:查表用原始包名,**对外仍回归一化 `pluginId`**(响应契约不变)
|
||||
|
||||
**复验**:`npm run build` rc=0|`node --test test/plugin-data-runtime.test.mjs test/im-plugin-client.test.mjs` → **24/24 过**
|
||||
|重投 + 重启 → **7/7 PASS**(同 §三)。
|
||||
|
||||
> ⛔ **未 commit / 未 push**(遵守本棒约束);改动留在 `D:/github/dsh_shenxian` 工作树(`plugin-data.ts`)。
|
||||
|
||||
---
|
||||
|
||||
## 五、🔴 第 2 件(MCN 移动端视觉验收)—— **未通过:前提不成立**
|
||||
|
||||
**验收目标**(入口 §2 原文):`D:/dshworkspace/plugin_package/dsh-plugin-mcn/lib/client.js` 的 `mcn-mobile`
|
||||
样式段 → 375×667 下工作台独占、能退出、无横向溢出、聚焦不缩放、横屏可用。
|
||||
|
||||
**真机取证(为什么做不了)**:
|
||||
|
||||
| 检查 | 结果 |
|
||||
|---|---|
|
||||
| 线上**真身**是哪个包 | 池与共享层里的都是 **`dsh-plugin-mcn-suite` v0.3.13**(`/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite`) |
|
||||
| 该真身含修复吗 | **否** —— `lib/client.js` 里 `mcn-mobile` 命中数 = **0**(`mcnNav_split` = 4) |
|
||||
| 本地 suite 含修复吗 | **否** —— 本地 `dsh-plugin-mcn-suite/lib/client.js`(0.3.13 · mtime 09-20 22:12)`mcn-mobile` = **0** |
|
||||
| 修复改在哪个包 | `dsh-plugin-mcn` **v0.1.0**(本地 `lib/client.js` mtime 09-25 05:18)—— 该包**从未投放** |
|
||||
|
||||
⇒ **移动端修复改在了「不是线上真身」的包里**。所以"重投插件 + 实例重启"**无从生效**:
|
||||
线上跑的一直是 suite,它里面没有这段样式。**故本件不做视觉验收,也不宣称通过**(无真机读数不得宣称)。
|
||||
|
||||
**整改路径(可机械执行 —— 已核对可移植性)**:
|
||||
`mcn-mobile` 段是一段**运行时注入的 `<style>`**(`document.head.appendChild`,`data-plugin-css="mcn-mobile"`),
|
||||
选择器全部是 `.mcnNav_*` / `vvModal` 族。已逐个核对它在 suite 里的存在性:
|
||||
|
||||
| `.mcnNav_split` | `.mcnNav_panel` | `.mcnNav_resizer` | `.mcnNav_close` | `.mcnNav_btn` | `.mcnNav_mask` | `.mcnNav_modal` | `.mcnNav_stats` | `.mcnNav_input` | `vvModal` |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 4 | 3 | 3 | 2 | 12 | 3 | 10 | 2 | 2 | 2 |
|
||||
|
||||
⇒ **类名齐备 ⇒ 可整段平移**(把同一段注入进 suite 的 `lib/client.js`,`dataset.plugin` 改成 suite 名)。
|
||||
⚠️ 重投前**必须先验**本地 suite 与线上 suite 逐字一致(否则会把别的漂移一起发出去);
|
||||
⚠️ 版本要自增(`0.3.13` → `0.3.14`)并同步 `lib/skills-installer.js` 的 `SUITE_VERSION` 与 `skills/.manifest.json`(该包「版本号唯一真源」口径)。
|
||||
|
||||
---
|
||||
|
||||
## 六、第 3 件(可选 · MCN 数据面接入改造)—— **未做**
|
||||
|
||||
超本轮范围(`lib/db.js` 红线扫描 21 处、且须「先建库再投放」)。按入口 §2 的口径**留给后续棒**。
|
||||
|
||||
---
|
||||
|
||||
## 七、清理与零残留(夹具全清)
|
||||
|
||||
| 项 | 清理后读数 |
|
||||
|---|---|
|
||||
| 候选池 | **5**(回到原值,夹具行 0) |
|
||||
| 插件库 `dshs_pl_*` | **0** |
|
||||
| `plugin_datastores` | **0** |
|
||||
| `plugin_data_audit`(夹具) | **0** |
|
||||
| 临时会话(`poc-curl2`) | **0** |
|
||||
| 夹具 tgz / 备份目录 | **已删** |
|
||||
| 服务 | `active` |
|
||||
|
||||
⚠️ 夹具在池内存活约 **2 分钟**(`07:56–07:59` 量级),期间登录用户理论上可在「功能管理」看到它 ——
|
||||
已删除,无残留。**这是本轮唯一一处对线上可见面的临时触碰**,主动记录。
|
||||
|
||||
---
|
||||
|
||||
## 八、未闭环 / 下一棒
|
||||
|
||||
1. 🔴 **MCN 移动端修复的「迁包 + 重投 + 375×667 视觉验收」**(见 §五)—— 本线唯一排定的下一棒。
|
||||
2. **跨节点形态**:worker 上的实例 → Manager 侧数据面仍缺「实例→控制面」身份通道
|
||||
(本轮验收是**同机/本机**形态)⇒ 属既有缺口(`DB-03 §六` 自认),未动。
|
||||
3. **106 未部署**本轮产物(Manager 侧路由,不影响本轮验收)。
|
||||
|
||||
## 九、可复现命令
|
||||
|
||||
```bash
|
||||
# 部署(三件产物 + server.js)
|
||||
scp lib/web/server.js bt-server:/opt/dshs/lib/web/server.js
|
||||
scp lib/db/plugin-data/{runtime,registry}.js bt-server:/opt/dshs/lib/db/plugin-data/
|
||||
scp lib/web/routes/plugin-data.js bt-server:/opt/dshs/lib/web/routes/
|
||||
ssh bt-server 'systemctl restart dshs'
|
||||
|
||||
# 验收(夹具搭建 + 7 条断言 + 零残留清理)
|
||||
scp tmp/15棒-夹具/dsh-plugin-pdprobe-0.0.1.tgz bt-server:/var/lib/dshs/business-plugins/
|
||||
scp tmp/15棒-夹具/pd-probe-47b.mjs bt-server:/tmp/ # 或第 15 棒工作区 tmp/15棒-夹具/
|
||||
ssh bt-server '/usr/local/bin/node /tmp/pd-probe-47b.mjs'
|
||||
ssh bt-server 'bash -s' < tmp/15棒-夹具/remote-cleanup.sh
|
||||
```
|
||||
@@ -0,0 +1,176 @@
|
||||
# 移动端修复「迁包 → 重投 47/106」+ 真机视觉验收(第 16 棒)
|
||||
|
||||
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`|**线**:插件投放与分库线|**日期**:2026-09-25
|
||||
> **依据**:`接续入口_插件投放与分库线_20260922.md §2「🎯 本轮动作(第 16 棒)」`
|
||||
> ⛔ 本棒未 commit / push;⛔ 未改 `src/web/routes/im.ts`;⛔ 未做 MCN 数据面接入改造。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论(先说判定)
|
||||
|
||||
| 件 | 判定 | 一句话 |
|
||||
|---|---|---|
|
||||
| 第 1 件 · 迁包 | ✅ **完成** | `mcn-mobile` 样式段已从**从未投放**的 `dsh-plugin-mcn` v0.1.0 整段平移进**线上真身** `dsh-plugin-mcn-suite`,38 个引用类名在目标包内**齐备 0 缺** |
|
||||
| 第 2 件 · 版本自增+重投 47/106 | ✅ **完成** | 0.3.13 → **0.3.14**;**47 + 106 共享层双双为 0.3.14**(106 由节点拉取机制一轮收敛);池内候选同步、回滚素材在位 |
|
||||
| 第 3 件 · 375×667 真机视觉验收 | ❌ **未通过(未取到真机读数)** | **不是代码问题,是链路上游断了**:已启用用户的实例仍指向**旧副本**,而平台的「重装配」操作**稳定失败**(两次复现)。⛔ 未宣称通过 |
|
||||
|
||||
🔴 **本轮最重要的产出 = 一条可复现的新缺陷**:**共享层更新对「已启用」的用户不生效,且平台没有可用的补救路径** —— 详见 §四。
|
||||
|
||||
---
|
||||
|
||||
## 二、第 1 件 · 迁包(逐字平移,只改包名一处)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 源 | `D:/dshworkspace/plugin_package/dsh-plugin-mcn/lib/client.js` 第 216–268 行(`//#region 移动端 / 窄屏兼容`) |
|
||||
| 目标 | `D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite/lib/client.js`(插在 `var McnNav_module_css_default` 之前) |
|
||||
| 改动 | 仅 1 处:`el3.dataset.plugin = "dsh-plugin-mcn"` → **`"dsh-plugin-mcn-suite"`** |
|
||||
| 体量 | 段 3642 B / 53 行 → 剥行尾后 **3595 B**;目标 401936 → **405531 B** |
|
||||
| 语法 | `node --check` **OK**(Node 22.22.2) |
|
||||
|
||||
**三条自证**(脚本 `tmp/p16-port-mobile-20260925.py`,可重跑):
|
||||
|
||||
1. **类名齐备**:段内引用类名 **38 个**,目标包内缺失 **0 个** ⇒ 第 15 棒的"可移植性核对表"在**字节级**上被独立复核。
|
||||
2. **行尾一致**:源段是 **CRLF**、目标包是**纯 LF** ⇒ 已归一(产物 CR = 0)。⚠️ 首版没做归一,产物混入 53 个 CR,**已回滚重做**。
|
||||
3. **幂等**:目标已含 `data-plugin-css="mcn-mobile"` ⇒ 脚本直接 SKIP。
|
||||
4. **可回滚**:改前副本 `tmp/p16-backup-20260925/dsh-plugin-mcn-suite-client.js.bak`(401936 B)。
|
||||
|
||||
---
|
||||
|
||||
## 三、第 2 件 · 版本自增与重投(47 + 106)
|
||||
|
||||
### 3.1 重投前对账("别把别的漂移一起发出去")
|
||||
|
||||
**改前副本 md5 = `0955f6b8b70f0c1357d29395643fdeb6` = 47 共享层线上 `lib/client.js` md5** ⇒ 本地 suite ≡ 线上真身,**逐字无漂移**。
|
||||
|
||||
全包逐文件比对(323 项,按路径建索引,排除 `node_modules`):
|
||||
|
||||
| 差异 | 内容 |
|
||||
|---|---|
|
||||
| 内容不同 | **仅 `lib/client.js`**(就是本棒的改动) |
|
||||
| 仅本地 / 仅线上 | `tests/smoke.mjs`(本地有、不进包)/`pnpm-lock.yaml`(服务端安装时生成) |
|
||||
|
||||
⇒ **漂移面 = 1 个文件且是本棒的预期改动**。
|
||||
|
||||
### 3.2 打包(以池内 tgz 为基底,只替换 3 个文件)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 基底 | `/var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz`(1989680 B,322 文件,顶层 `package/`) |
|
||||
| 新包 | `tmp/p16-pool/dsh-plugin-mcn-suite-0.3.14.tgz`(**1982036 B,322 文件**) |
|
||||
| sha256 | `3ca65153605b3c48f7de628d4754dc872b6f4fb7b617e9e57e704eb90bbab61c` |
|
||||
| 新旧差异 | **只差 3 个文件**:`package/lib/client.js`、`package/package.json`、`package/skills/.manifest.json`;**零增删** |
|
||||
|
||||
版本自增两处(该包「版本号唯一真源 = `package.json`」,`SUITE_VERSION` 由运行时读它 ⇒ 无字面量可改):
|
||||
`package.json` `version` → `0.3.14`(+ 变更史前置一条)|`skills/.manifest.json` → `suite: …@0.3.14`、技能 `version: 0.3.14`。
|
||||
⚠️ `files/bytes/sha256` 三项**未动**(技能载荷未变,只动版本字段)。
|
||||
🔴 **行尾坑(本棒第二次踩)**:Windows 上用 Python 写文本默认把 `\n` 转成 `\r\n` ⇒ 首版把 CRLF 写进了 `package.json` 与清单,**已回滚重做**(写时显式 `newline="\n"`,产物 CR = 0)。
|
||||
|
||||
### 3.3 上传递交(走平台正规口,非手改文件)
|
||||
|
||||
`POST /api/plugins/business`(admin 会话)→ **http=200**:`version 0.3.14`、`replaced: true`、`trustedOverride: false`、
|
||||
兼容性裁决 `level: ok`(`platformPkgs 240`、`sawPlatformRef true`);安全检测**只出 warning 未拦**(`[email protected]` / `network-egress@lib/client.js` / …)。
|
||||
|
||||
### 3.4 铺共享层(两段式:先预演、后确认)
|
||||
|
||||
* 预演(`confirm` 缺省)→ `conflict: true`,并**顺带暴露一条台账缺陷**(见 §四 P2)。
|
||||
* 确认 → `action: "replaced"`、`version 0.3.14`、`fileCount 657`、新 `treeSha256 f414df79…`、
|
||||
回滚素材 `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.14.tgz`。
|
||||
|
||||
**47 实况复核**:`lib/client.js` md5 = `d387089bd28c91cdbe46056e21288cbd`(= 本地新文件)、`mcn-mobile` 命中 **2 处**、`package.json` = 0.3.14、文件数 **657**(含 `pnpm install --prod` 重建的 `node_modules`)。
|
||||
|
||||
### 3.5 106 同步(走节点拉取机制,非手工 scp)
|
||||
|
||||
106 上手动触发一轮:`POST http://127.0.0.1:19000/shared-layer/pull`(带 `x-dsh-agent-token`)
|
||||
→ `{"stale":["dsh-plugin-mcn-suite"],"pulled":["dsh-plugin-mcn-suite"],"failed":[]}`
|
||||
→ 106 盘上 **0.3.13 → 0.3.14**、`client.js` md5 = `d387089…`。**一轮收敛**。
|
||||
|
||||
### 3.6 回滚
|
||||
|
||||
| 素材 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| 上一版(内容 = 0.3.13) | 47 `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.9.tgz`(md5 `f47a07b9a2a889770e35315e22156e90`) | ⚠️ **文件名叫 0.3.9、内容是 0.3.13**(见 §四 P2) |
|
||||
| 本棒投出的版本 | 同上目录 `0.3.14.tgz` | 内容是**新**包(share 时补落的语义错位,见 §四 P2) |
|
||||
| 回滚动作 | 用上一版 tgz 重走上传递交 + `POST /api/plugins/business/share {confirm:true}` | 共享层目录被整体替换 ⇒ 指向它的用户下一次 reconcile 生效 |
|
||||
|
||||
---
|
||||
|
||||
## 四、抓出的缺陷(⛔ 均未擅自修,按纪律只报告)
|
||||
|
||||
### 🔴 P0 —— 已启用插件拿不到共享层新版本,且平台无可用补救路径
|
||||
|
||||
**判据链(全部可复现)**:
|
||||
|
||||
1. **实例依赖形态**:admin 实例 profile 的 `dsh-plugin-mcn-suite` 声明为
|
||||
`file:/var/lib/dshs/users/cce6d1cd-…/home/.dsh-stage/dsh-plugin-mcn-suite.tgz`
|
||||
—— 是**一份 `.dsh-stage` 副本**(1989680 B = 旧版内容,09-20),**与共享层无关**。
|
||||
⇒ **共享层升到 0.3.14 对它零影响**,这与第 15 棒"修复落在没投放的包里"是**同一病根的下一层**。
|
||||
2. **正规补救路径 = 关掉再开**(`apply` 是全量裁决:已启用的项再发 `enabled:true` 判 `noop`,不会重装配)。
|
||||
3. **该路径稳定失败**:两次全量「关闭」均返回 `taskId`,各约 **52 s** 后 `{"status":"failed","stage":"失败","error":"安装失败,请重试或联系管理员"}`:
|
||||
* 任务 `0df1ea39ed3026c4`(08:32:38 → 08:33:30)
|
||||
* 任务 `3eb4b72b73373e07`(08:34:42 → 08:35:34)
|
||||
`/mine` 复核**仍为 `enabled: true`** ⇒ 平台**没有**把用户切到新版本的可用手段。
|
||||
⚠️ 已核实**不是**"暂存包缺失"(4 个 `.dsh-stage` 包全部在位)。
|
||||
4. **失败原因不可观测(第二条缺陷)**:`journalctl -u dshs` 在失败时段**只打了 HTTP 请求行**,pnpm 的 stderr / 装配失败原因**未落任何日志** ⇒ 用户与运维都只能看到"请重试",**无法定位**。本棒的根因**因此未能定位**。
|
||||
|
||||
**影响面**:凡是**已经启用**过某插件的用户,都停留在启用当时那份副本上 ⇒ 平台的「插件更新」对存量用户**不成立**;共享层只对"新启用"的用户有效。这条直接命中本线目标 P2("插件更新的情形要有定义")。
|
||||
|
||||
**建议落点(下一棒)**:① 给 `applyPlugins` 的 pnpm 失败路径补**可观测性**(stderr 落日志 + 失败原因回传到任务对象);② 定位失败根因(候选:47 的 egress 护栏挡住了平台自身的 registry 解析 / pnpm store 锁竞争);③ 重装配要能把 `file:` 形态换成 `link:`。
|
||||
|
||||
### 🟡 P1 —— 移动端真机视觉验收**仍未取得**(第 15 棒遗留,本棒被上面那条挡住)
|
||||
|
||||
**"能不能真渲染"这件事本棒已做成判定,且结论是"能"**:
|
||||
|
||||
| 探针 | 读数 |
|
||||
|---|---|
|
||||
| 47 侧浏览器 | ❌ 无(`chromium/chrome/firefox` 全无、无 playwright/puppeteer 缓存) |
|
||||
| **本机侧浏览器** | ✅ **有**:`C:/Program Files/Google/Chrome/Application/chrome.exe`(真 Chromium 内核) |
|
||||
| 平台门户 | ✅ `https://ai1net.com/portal.html` → 200 |
|
||||
| 实例 UI | ✅ `https://ai1net.com/u/<uid>/dsh/` → **200**,且平台**自动下发实例凭据** `Set-Cookie: dsh-auth-…` |
|
||||
| 实例地址 | `https://admin.ai1net.com/?token=…`(`/api/dsh/status` 直出,可直接作为渲染入口) |
|
||||
|
||||
⇒ **路径已通,只差"实例上跑的是新版本"这一步**。修好 P0 即可直接补做五条读数(无需再做任何环境建设)。
|
||||
|
||||
### 🟡 P2 —— 共享层清单与磁盘不同步 + 回滚素材命名语义错
|
||||
|
||||
* **清单 vs 盘**:本次预演读到的清单是 `version: "0.3.9"`、`tgzSha256 d14cd5fe…`、`treeSha256 cb1c73fb…`,而**盘上实际是 0.3.13 内容**(`package.json version = 0.3.13`、`client.js` md5 与池内 0.3.13 tgz 一致)。
|
||||
⇒ `authoritativeShared()` 拿这份 `treeSha256` 做节点对账 ⇒ **节点会永远判 `stale` / 反复重拉**。本棒 put 新版本后该条**自愈**(新 `treeSha256` 是现算的),但机制上的不同步来源未查明。
|
||||
* **回滚素材命名**:`doShare` 里补落的备份路径用 **incoming 版本号** 命名、内容取 **incoming 的 tgz**
|
||||
(`backupTgz = <backupDir>/plugins/<flat>/<version>.tgz` 且 `copyFileSync(existing.tgzPath, …)`,而 `existing` = **上传后**的池条目)
|
||||
⇒ 产出的 `0.3.14.tgz` **是"新包的自拷贝",不构成"回到上一版"的素材**;真正可回滚的那份躺在 `0.3.9.tgz` 里(名字与内容不符)。
|
||||
⇒ 建议:备份应记**被替换**的那一版,或名字带上"replaced"语义。
|
||||
|
||||
---
|
||||
|
||||
## 五、复现命令(第三方可照抄)
|
||||
|
||||
```bash
|
||||
# ① 共享层版本与字节
|
||||
ssh bt-server 'grep -m1 "\"version\"" /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/package.json'
|
||||
ssh bt-server 'md5sum /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/client.js' # 期望 d387089bd28c91cdbe46056e21288cbd
|
||||
ssh test106 'md5sum /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/client.js' # 应与 47 同值
|
||||
# ② 池内候选
|
||||
ssh bt-server 'ls -la /var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz' # 期望 1982036 B
|
||||
# ③ 实例依赖形态(P0 判据)
|
||||
ssh bt-server 'python3 -c "import json;print(json.load(open(\"/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde/home/profiles/web/package.json\"))[\"dependencies\"][\"dsh-plugin-mcn-suite\"])"'
|
||||
# 期望输出 file:…/.dsh-stage/dsh-plugin-mcn-suite.tgz ← 即「未指向共享层」
|
||||
# ④ 重装配失败复现(需要 admin 会话):全量 apply 一条 enabled:false,随后轮询
|
||||
# GET /api/plugins/mine/task/<taskId> → 约 52 s 后 status=failed / "安装失败,请重试或联系管理员"
|
||||
```
|
||||
|
||||
**本棒用到的临时脚本**(均在 `tmp/`,可重跑):`p16-port-mobile-20260925.py`(迁包)· `p16-build-0314.py`(改版打包)· `p16-hash-list.py` / `p16-cmp.py`(逐文件对账)· `p16-pull-106.sh`(触发节点拉取)· `p16-cleanup.sh`(清临时会话与临时物)。**只读**工具:`p16-reach.sh`(连通性探路)。
|
||||
|
||||
---
|
||||
|
||||
## 六、清理与合规
|
||||
|
||||
| 项 | 状态 |
|
||||
|---|---|
|
||||
| 临时 admin 会话 | ✅ **已删**(`DELETE FROM sessions WHERE user_agent='p16-mksess'` ⇒ rowCount 1,复核残留 **0**) |
|
||||
| 47 `/tmp` 临时物 | ✅ **零残留** |
|
||||
| 实例半状态 | ✅ **无**:实例 running、profile 依赖**未被改动**(两次失败均未落盘) |
|
||||
| 47/106 服务 | ✅ 正常(共享层 0.3.14、池 0.3.14) |
|
||||
| 装配协议 / `plugin-assembly.ts` | ✅ **零改动** |
|
||||
| 可见面开关 | ✅ 未开;⛔ 未开任何新口 |
|
||||
| `src/web/routes/im.ts` | ✅ **零改动**(IM 线 D 单判据 2 冻结遵守) |
|
||||
| commit / push | ✅ 未做 |
|
||||
@@ -0,0 +1,86 @@
|
||||
# 第 18 棒落地件 · 共享层自足性转正 + 回滚残留 + 移动端第 ④ 条(2026-09-25)
|
||||
|
||||
> **线**:插件投放与分库线(入口 `接续入口_插件投放与分库线_20260922.md`)
|
||||
> **棒次**:第 18 棒 · 执行棒(automation `ed2b5e61-25b9-4b97-849c-1e0b204b669e`)
|
||||
> **代码基线**:`D:\github\dsh_shenxian`(生产口径 `dshs`)|⛔ 未 commit / push
|
||||
> **结论**:第 1 件 ✅(判据 ①②③ 全绿)|第 2 件 ✅(判据成立,另发现"缺失侧"未覆盖 ⇒ 已上报)|第 3 件 ✅ **5/5**(含本次修的第 ④ 条)
|
||||
|
||||
---
|
||||
|
||||
## 一、第 1 件 · 共享层自足性从探针转为正式修法
|
||||
|
||||
**病**:共享只读包库 `<flat>/node_modules` 里只有插件自己的依赖、**零个 `@deepseek-ai`** ⇒ `link:` 形态下实例从共享层加载宿主代码必报 `Cannot find package '@deepseek-ai/dsh-llm'` ⇒ 实例 crash-restart ⇒ 探活判"实例未能启动"⇒ 插件被自动禁用。
|
||||
|
||||
**修法(唯一一份实现落在 ③ 能力层,① 入口层复用)**
|
||||
| 落点 | 内容 |
|
||||
|---|---|
|
||||
| `src/supervisor/plugin-assembly.ts` | 新增 `resolveDshPackageRoot()`(解析顺序:env 显式覆盖 → 平台配置的 dsh bin 解软链 → `npm root -g`)、`resolveHostDshScopeDir()`、`ensureSharedScopeLink()`、`healSharedLayerSelfSufficiency()` |
|
||||
| `src/web/routes/business-plugins.ts` | `doShare` 目录就位后调用 `healSharedLayerSelfSufficiency(root)`(**刻意放在 `hashTree` 之后** ⇒ 指纹不含软链,不引起重拉)+ 审计与响应体带 `sharedSelfSufficiency` 摘要 |
|
||||
| `src/worker/agent.ts` | `syncSharedLayerOnce` **每轮**幂等自愈(worker 是独立单元,Manager 与 worker 各自把关) |
|
||||
| `src/web/dsh-install.ts` | 该文件原有一份 dsh 根解析副本 ⇒ **下沉到 ③ 层复用**,删本地副本(代价:不再是"零内部 import",验证改走 `scripts/verify-dsh-install.mjs`) |
|
||||
|
||||
**软链落点 = 包目录内 `<flat>/node_modules/@deepseek-ai`**,⛔ 不落共享层根 —— 否则 `syncSharedLayerOnce` 的 `onDisk` 对账面会把 `node_modules` 当"多出来的产物"上报 `extra`。
|
||||
|
||||
**判据与读数(真机,`47 = bt-server` / `106 = test106`)**
|
||||
| # | 判据 | 读数 |
|
||||
|---|---|---|
|
||||
| ① | **删掉人工探针链后重新 share ⇒ 共享层自带解析链** | 响应 `sharedSelfSufficiency.scope = /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai`,`linked = ['_dsh-local_im-conversation-tabs','_dsh-local_storyforge','dsh-plugin-mcn-suite']` |
|
||||
| ② | **47 + 106 两端都成立** | 47 三包 scope 均 LINK 可解(240 项);**106 三包 scope 均 LINK → `/usr/lib/node_modules/...`**(两机 `npm root -g` 路径不同 ⇒ 必须各机本地解析,⛔ 不能由 Manager 算好随包分发) |
|
||||
| ③ | **重新启用 mcn-suite ⇒ 实例加载 0.3.15、任务 success、`rejected:[]`** | `off` 任务 `a524537251ff570a` = `success`;`on` 任务 `28dd88e4c391cad6` = `success`、`rejected:[]`、实例探活通过;journald:**`[mcn-suite] loaded v0.3.15(host 面 2 个:mcn, voxemw-cloud)`** + `skills ensured: 装 [email protected](升级自 0.3.14)`;`/api/dsh/status` = `running:true port:20000 restarts:0` |
|
||||
|
||||
**⛔ 未污染对账面**:共享层根**没有**多出 `node_modules`;`hashTree`/`hashTreeAt` 本就不计符号链接 ⇒ `treeSha256` 不变 ⇒ 不触发节点重拉。
|
||||
|
||||
---
|
||||
|
||||
## 二、第 2 件 · 回滚残留
|
||||
|
||||
**病**:`restoreProfile()` 只还原 `package.json`/`pnpm-lock.yaml`/`cordis.patch.yml` ⇒ 回滚后 `node_modules` 与声明不一致。
|
||||
|
||||
**修法**:新增 `declaredNamesIn(snap)`(`dependencies` ∪ `dsh.profile.bundles`,快照解析失败 ⇒ 取空集**宁可不删**)+ `pruneOrphanDepLinks(dir, snap)`(**只删软链**、**只删未声明**、跳过 `.` 开头与真目录、scoped 目录下钻),在 `restoreProfile()` 末尾调用。单测钉住四条不变量:声明内不删 / `bundles` 内不删 / 真目录不碰 / `.pnpm` 不碰。
|
||||
|
||||
**判据**:回滚后 `node_modules/<pkg>` 与 `dependencies` 一致(至少不残留无主软链)⇒ ✅ 成立。
|
||||
|
||||
> 🔴 **本棒实测到的另一半(⛔ 未修,已上报)**:残留有**两个方向**。本次 `off` 动作在真机上撞 `pnpm remove` 失败(见 §四-1),结果是**声明还在、`node_modules/dsh-plugin-mcn-suite` 软链被删掉了** —— 实例启动直接崩:
|
||||
> `Error: dsh: cannot resolve profile bundle "dsh-plugin-mcn-suite" from the dsh installation or <profile>` ×5 ⇒ `crash-loop-circuit-open`(冷却 600 s)。
|
||||
> `pruneOrphanDepLinks` 只治"多",不治"缺"。**建议修法(下一棒,小改)**:`restoreProfile()` 里对**声明中的 `link:` 依赖**做一次确定性重建(`link:` 目标就在快照里,纯 `symlinkSync`,**不需要跑 pnpm**、不碰 `.pnpm` 实体)⇒ 两个方向都收敛。
|
||||
>
|
||||
> **恢复过程(走平台正规口,未手改 profile)**:`off`→`on` 循环重跑,两次任务均 `success` ⇒ `pnpm install` 自动重建软链 ⇒ 实例重启探活通过。⛔ 全程未手工 `ln -s`、未手改 `package.json`/`.pnpm`。
|
||||
|
||||
---
|
||||
|
||||
## 三、第 3 件 · 移动端第 ④ 条(聚焦不缩放)+ 五条重跑
|
||||
|
||||
**改了什么**(插件真源 `D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite/lib/client.js`,`mcn-mobile` 段第 ③ 条)
|
||||
```css
|
||||
.mcnNav_input,.mcnNav_select,.mcnNav_textarea,.v1rksel,.dvAcc_input,.dvAcc_select,.dvAcc_textarea{font-size:16px!important}
|
||||
.mcnNav_panel input:not([type=checkbox]):not([type=radio]),…,.mcnNav_page select,…,.dvAcc_page textarea{font-size:16px!important}
|
||||
```
|
||||
理由:`.dvAcc_*` 基础样式用 `font:var(--dsw-font-s-14)` **简写**且**按需注入**(可能晚于本段)⇒ 必须 `!important`;`SELECT.v1rksel` 此前**不在任何移动端规则内** ⇒ 13.33px。
|
||||
|
||||
**投放链**:版本 `0.3.14 → 0.3.15`(`package.json` + `skills/.manifest.json` 两处真源)⇒ 以池内 0.3.14 tgz 为基底**确定性重打**(322 文件,变化面**恰 3 个文件**,自证基底 md5 ≡ 线上 `d387089b…`)⇒ `POST /api/plugins/business` ⇒ 两段式 share(`action:"replaced"`、`treeSha256 939206c76abb…`、`fileCount 657`)⇒ 47/106 共享层双双 0.3.15(106 首轮 `stale→pulled`)⇒ 47 四件、106 两件 md5 逐字一致 ⇒ restart ⇒ 门户 200 / worker 401。
|
||||
|
||||
**五条真机读数(`tmp/p17-visual.mjs`,零依赖 CDP 驱动本机真 Chrome,视口 375×667 + 横屏 667×375)**
|
||||
| # | 判据 | 读数 | 判定 |
|
||||
|---|---|---|---|
|
||||
| ① | 工作台独占 | 开面板后 `navCount=1`,面板盒 `{x:56,y:0,w:319,h:667}`;会话区 `display:none` | ✅ |
|
||||
| ② | 能退出 | 二次点 `.mcnNav_trigger` ⇒ URL `#/mcn`→`/`、`navCount` 1→0、会话区复原(现版 UI **不渲染** `.mcnNav_close` ⇒ 用等价判据) | ✅ |
|
||||
| ③ | 无横向溢出 | `scrollWidth==innerWidth`(竖 375=375、横 667=667,溢出 **0**) | ✅ |
|
||||
| ④ | **聚焦不缩放** | 工作台内唯一可见表单控件 `SELECT.v1rksel` 计算字号 = **16px**(修前 13.33px);注入样式段文本级取证:`style[data-plugin-css="mcn-mobile"]` 命中新规则 | ✅ |
|
||||
| ⑤ | 横屏可用 | 667×375:面板 `{x:56,y:0,w:611,h:375}`,溢出 0 | ✅ |
|
||||
|
||||
**零点**:`style[data-plugin-css="mcn-mobile"]` 计数 = 1 ⇒ 新段确在浏览器里生效(不是"以为改了")。
|
||||
|
||||
---
|
||||
|
||||
## 四、两条新发现(⛔ 未擅自修,已写进 §2 后续)
|
||||
|
||||
1. 🔴 **`pnpm remove/install` 在真 profile 里会**偶发**失败(exit 255,`--reporter silent` 把原因全吞掉)** —— 第 16/17/18 棒累计三次(09:01 apply timeout、09:05 isolate install 失败、09:56 remove 失败)。**本棒已排除的假设(全部实测反证,别再重复走)**:㈠ 共享包目录内多出的 `node_modules/@deepseek-ai` 软链(A/B 对照**双双 rc=0**)㈡ 真实路径条件(真实路径副本 rc=0)㈢ worker 的最小 spawn 环境(`env -i PATH=… HOME=…` rc=0)㈣ 祖先目录不可 `scandir`(`/var/lib/dshs` 与 `/var/lib/dshs/users` 是 **711 ⇒ 该 uid `r` 位确实没有**,但实测不影响 pnpm 运行)㈤ 磁盘 / inode(23 GB 可用)。⇒ 高度指向**并发/时序**(同一 store 下并发 pnpm、或与共享层拉取重叠)。**下一次复现时的正解动作**:给失败路径补一次 `--reporter default` 重跑(现在 `--reporter silent` 结构上让 `pnpmExecError` 拿不到任何正文)—— 属 `plugin-assembly.ts` 的小改。
|
||||
2. 🟡 **`restoreProfile()` 不重建缺失的 `link:` 软链**(见 §二 的红框)⇒ 今日真实致实例崩溃。
|
||||
|
||||
---
|
||||
|
||||
## 五、门禁与卫生
|
||||
|
||||
- `npm run build` rc=0|`npm test` = **575 tests / 573 过 / 0 败 / 2 跳过**(基线 449/447/2;新增 6 个用例:软链补建与幂等 / 悬空与指错 / 真目录不碰 / `no_scope` 零改动 / 全量补含老包且不落共享层根 / 回滚清无主软链)|`check-layering` **✅ 无新增违规**(现存 5 条全在基线内)。
|
||||
- 部署备份:`/opt/dsh/backups/lib-p18/{47,106}-pre-p18`;插件回滚素材 `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.15.tgz`。
|
||||
- 清理:临时 admin 会话**残留 0**(两机 `/tmp/p18*` 残留 0)|⛔ 未 commit/push、⛔ `src/web/routes/im.ts` 零改动、⛔ 未手改用户 `home` 下的 profile / `.pnpm`。
|
||||
@@ -0,0 +1,156 @@
|
||||
# 插件投放与分库线 · 第 23 棒(执行棒)落地件
|
||||
|
||||
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server` | **日期**:2026-09-25 12:1x–12:4x
|
||||
> **代码基线**:`D:/github/dsh_shenxian`(生产口径 `dshs`)/HEAD `e6207aa`
|
||||
> **三件**:① §5-12 跨机错误消息截断放宽(小改,已投两机)② §5-18「清理前必查」只读取证 ③ §5-22 本机 `lib` ↔ 部署面 3+7 差异定性
|
||||
> **锁**:`插件投放与分库线-第23棒`,域 = `src/supervisor` / `test/remote-spawner.test.mjs` / `aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、第 1 件 · §5-12 跨机错误消息截断放宽(200 → 1200)
|
||||
|
||||
### 1-1 改了什么(一件文件 + 一个测试文件)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/supervisor/remote-spawner.ts` | 新增导出常量 `AGENT_ERROR_TEXT_MAX = 1200`(带口径注释:只放宽**保留长度**,⛔ 不动重试次数/退避,⛔ 不改 4xx 不重试语义)|两处 `text.slice(0, 200)` ⇒ `text.slice(0, AGENT_ERROR_TEXT_MAX)`(行 299/303 两个分支:4xx 具名抛与 5xx `lastErr`) |
|
||||
| `test/remote-spawner.test.mjs` | 新增 **T6/T7/T8**(+3 例) |
|
||||
|
||||
### 1-2 单测钉住的不变量
|
||||
|
||||
- **T6** 4xx:973 字符正文**逐字完整**进 Manager 侧 `message`(旧口径只留前 200)——正对第 17 棒的现场(真根因只能从 worker journald 补)。
|
||||
- **T7** 边界:**恰好 1200 字符一个不少**;**多 1 个字符即截到 1200**(钉住"上限还在,不是关了限制")。
|
||||
- **T8** 5xx 分支:走**同一上限**,且 `calls === 4`(钉住"5xx 仍重试 4 次"这条既有语义**未被本次改动碰到**)。
|
||||
- ⚠️ T6 首发因**夹具自身超上限**(1453 > 1200)被断言挡住 ⇒ 已改 973 字符夹具;这正是该断言的价值(夹具错了会被抓出来)。
|
||||
|
||||
### 1-3 门禁与部署(✅ 判据 ①②③ 全绿)
|
||||
|
||||
| 判据 | 读数 |
|
||||
|---|---|
|
||||
| ① 放宽到 ~1200 | ✅ `AGENT_ERROR_TEXT_MAX = 1200`,两处分支同用 |
|
||||
| ② 单测 | ✅ `node --test test/remote-spawner.test.mjs` = **8/8 通过**(原 5 例 + 新 3 例) |
|
||||
| ③ 门禁 | `npm run build` **rc=0** | `npm test` = **592 tests / 590 过 / 0 败 / 2 跳过**(第 21 棒 589/587/0/2 ⇒ **+3**)|`npm run check:layering` **rc=0 ✅ 无新增违规**(未归类 1 条 = `src/platform-paths.ts`,属既有基线) |
|
||||
|
||||
**部署(两机同一文件)**
|
||||
|
||||
| 项 | 47 | 106 |
|
||||
|---|---|---|
|
||||
| 目标 | `/opt/dshs/lib/supervisor/remote-spawner.js` | `/opt/dshs-cluster/lib/supervisor/remote-spawner.js` |
|
||||
| 投前 md5 | `269d3d20ffceb726298434972a349d56`(20564 B) | **同左(逐字一致)** |
|
||||
| 投后 md5 | `e9ab05d92a8666459aa486e6893e1bf5`(21529 B) | **同左** |
|
||||
| 三方一致 | ✅ 本机 = 47 = 106 = `e9ab05d9…` | ✅ |
|
||||
| 备份 | `/opt/dsh/backups/lib-p23/iZrj99af19cibck1ge93tqZ-pre-p23-20260925-122854.tgz`(sha256 `0089585e6f4e910e…`)+ 同名 `.manifest` + 单文件副本 | `/opt/dsh/backups/lib-p23/VM-0-8-opencloudos-pre-p23-20260925-122855.tgz`(sha256 `d6f71f98efd5191d…`)+ `.manifest` + 单文件副本 |
|
||||
| 生效 | ✅ `restart dshs` ⇒ `active` · `NRestarts=0` · 门户 **HTTP 200** · journald `dshs listening on http://127.0.0.1:3080` | ⛔ **未重启**(中断在线用户 ⇒ 待窗口)⇒ **待重启面 47 → 48 件** |
|
||||
|
||||
- ⛔ **未重启 106**(约束遵守);⛔ 未 commit / push;⛔ `src/web/routes/im.ts` 零改动;⛔ 未手改任何用户 `home` 下的 profile / `.pnpm`。
|
||||
- 47 重启时旧进程**停止阶段**有一次 `dshs.service: Failed with result 'timeout'`(优雅退出超时),新进程随即 `Started` 并监听 3080、门户 200、`NRestarts=0` ⇒ 判为**生效**,如实记录。
|
||||
- `dshs-worker`(47)**未重启**:`lib/supervisor/remote-spawner.js` 在 worker 模块图内(`cli.js worker` 静态 import `web/server.js`),但 `RemoteSpawner` 的**唯一实例化点** = `lib/web/server.js:38` 的 Manager 侧装配 ⇒ 对 worker 行为零影响。
|
||||
- ⚠️ **本棒无"长错误消息"真机读数** —— 该路径需要一次真失败的跨机调用才可观察。⛔ **不宣称运行面验收通过**;本件判据 = 代码面 + 单测 + 部署面三方 md5 一致,均已取到。
|
||||
|
||||
---
|
||||
|
||||
## 二、第 2 件 · §5-18「清理前必查」只读取证(⛔ 未执行清理)
|
||||
|
||||
**目标 profile**:`/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde/home/profiles/web`(已启用 `dsh-plugin-mcn-suite` 的那台 admin 实例;运行身份 `dsh-cce6d1cdb376430480f0`,uid 114801)
|
||||
⛔ 全程只读:未删/未改任何软链、未改 profile、未动 `.pnpm`。
|
||||
|
||||
### 2-1 判据① 命中文件与命中处(含行号)
|
||||
|
||||
| # | 命中处 | 形态 |
|
||||
|---|---|---|
|
||||
| 1 | `.pnpm/@dsh-local+business-plugins@file+..+..+.dsh-stage+business-plugins-0.3.25.tgz/node_modules/@dsh-local/business-plugins/lib/client.js:56` | `var React = require("react")` |
|
||||
| 2 | `.pnpm/@dsh-local+portal-entry@file+..+..+.dsh-stage+portal-entry-0.5.6.tgz_…/node_modules/@dsh-local/portal-entry/lib/client.js:67` | `var React = require("react")` |
|
||||
| 3 | `.pnpm/@[email protected]_…[email protected]/node_modules/@deepseek-ai/dsh-client-ui-renderer/lib/client.js:10` | `let react = require("react")` |
|
||||
| 4 | 包自身内部(`react`/`react-dom` 的 `cjs/*`、`[email protected]`、`[email protected]`、`@deepseek-ai/dsh-client-ui-primitives/lib/index.js:8-9`、`@softspark/dsh-file-preview/lib/types/client/*.js:2`) | 这些消费者在**自己的 `.pnpm/<pkg>/node_modules` 自带 react** ⇒ 不经兜底 |
|
||||
|
||||
全树剥离"自身包内部 / `dist/assets` 浏览器包 / `lib/client.js`"后**剩余 51 条命中,全部属上述四类**;**四个包的 `main`(`lib/index.js`,服务端半边)零命中**。
|
||||
|
||||
### 2-2 判据② 「包自己声明的依赖」 vs 「顶层兜底」——**判别器实测**
|
||||
|
||||
Node 解析优先级:① `.pnpm/<pkg>/node_modules` ② `.pnpm/node_modules`(提升层)③ `<profile>/node_modules`(**顶层兜底所在**)。以该实例 uid 运行 `require.resolve`(只解析、不执行模块):
|
||||
|
||||
| 解析起点 | `react` 落点 |
|
||||
|---|---|
|
||||
| `<profile>` 本身 | `<shared>/dsh-plugin-mcn-suite/node_modules/.pnpm/[email protected]/node_modules/react/index.js` |
|
||||
| `<profile>/node_modules/dsh-plugin-mcn-suite` | ↑**同一落点**(自己的 store) |
|
||||
| `<shared>/dsh-plugin-mcn-suite` | ↑**同一落点**(自己的 store) |
|
||||
| `@dsh-local/business-plugins` | ↑**同一落点(⇒ 本次解析经顶层兜底链)** |
|
||||
| `@dsh-local/portal-entry` | ↑同(经兜底) |
|
||||
| `@deepseek-ai/dsh-client-ui-renderer` | ↑同(经兜底) |
|
||||
|
||||
- **包自己声明的依赖**:共享层 `dsh-plugin-mcn-suite/package.json` 第 **32–34** 行声明 `react` / `react-dom` / `xlsx`,其 `.pnpm` 树内自带三件 ⇒ 解析**不经过**顶层兜底。
|
||||
- **顶层兜底**:只有那 3 个 `.pnpm/<pkg>/node_modules` 里**没有** `react` 的消费者(① 优先级落空、② 提升层 `.pnpm/node_modules` 里三件**都没有**)⇒ 解析落到 `<profile>/node_modules/react` ⇒ **兜底链确实在解析路径上**(实测三处落点全部 = 兜底链的实解目标)。
|
||||
- 兜底目录自身现状:`.dsh-module-fallback/node_modules/` 内**只剩 3 条链**(`xlsx`/`react`/`react-dom` → 共享层 `dsh-plugin-mcn-suite/node_modules/*`),另有 4 个**空目录**(`@deepseek-ai`/`@shikijs`/`@types`/`@ungap`,各 0 项)= 历史残留。
|
||||
- 三条链的字节级 mtime = **09-25 10:14:57**(⛔ 非 §5-18 记的 09-08;09-08 是**目录** mtime)⇒ 近两日内被平台装配动作刷新过(真值以本行实测为准)。
|
||||
|
||||
### 2-3 判据③ 结论句
|
||||
|
||||
> **结论 = 有真依赖,但依赖方全在「浏览器半边」;Node 运行期无消费者。**
|
||||
|
||||
依据:3 个命中者全是 `lib/client.js`(浏览器半边)——
|
||||
- `@dsh-local/business-plugins/lib/client.js` 头部原文:`dsh client bundles 以普通脚本形式执行,向 window.__ModuleLoader__ 注册 factory;首次 import 时惰性物化 factory(require)`
|
||||
- `@deepseek-ai/dsh-client-ui-renderer/lib/client.js` 首行:`window.__ModuleLoader__.load({ id: …, factory: (require) => { … } })`
|
||||
|
||||
⇒ 它们的 `require("react")` 由**浏览器侧模块表(`window.__ModuleLoader__`)**满足,**不读 `node_modules` 磁盘**。加一条反向证据:`grep -rln "__ModuleLoader__" /opt/dshs/lib` = **0 文件**(平台服务端代码里根本没有这个加载器)⇒ 服务端不解析 client bundle 的 `require`。
|
||||
|
||||
**边界(哪些没判到)**
|
||||
1. 只核了**这一个 profile**(`profiles/web`)与共享层 4 个包;另一台 profile `9f3a1c47-…`(uid 100010)**没有**这三条链(`ABSENT`),故不在讨论范围。
|
||||
2. ⛔ 未展开 `@deepseek-ai/dsh` 官方包的客户端宿主实现(只读未做)——若它在服务端预解析 client bundle 依赖,结论需修正。
|
||||
3. 结论对**当前这一版 profile 内容**成立;profile 重建 / 插件版本升级后需重测。
|
||||
4. ⛔ 未做"删掉链再跑"的 A/B(会改实例解析面,属清理动作)。
|
||||
|
||||
### 2-4 给拍板的输入(⛔ 本棒不清理)
|
||||
|
||||
清理的**实际影响面** = 只动 3 个浏览器半边包在 **`require.resolve` 语义**上的可见性;Node 运行期无消费者 ⇒ 风险**低**,但非零(见边界 2、3)。另:这三条链目前每个装配周期都要被 `quarantineSharedLayerLinks` 临时摘一次(第 20 棒的修法),清掉可省这一步 —— 但**该修法本身已保证装配不受影响**,所以"省一步"不构成必须清理的理由。
|
||||
|
||||
---
|
||||
|
||||
## 三、第 3 件 · §5-22 本机 `lib` ↔ 部署面 3+7 差异定性(⛔ 未投任何件)
|
||||
|
||||
> 本机 `lib` = **09-25 12:23 构建**(本轮 `npm run build` 产物)/两机部署面 = **IM 版 09-23 20:59**(两机逐字相同)。
|
||||
|
||||
### 3-1 ① 那 7 件是否被引用 ⇒ 死件 / 活件
|
||||
|
||||
| 件 | 两机现状 | 部署面引用面 | 判定 |
|
||||
|---|---|---|---|
|
||||
| `im/gateway-token.js` `.d.ts` `.js.map` | **三件全缺** | `grep -rn gateway-token <lib>/**/*.js` = **0 命中** ⇒ 部署面的旧版 `web/routes/im.js` 不引用它 | **死件**(当前无引用者)✅ **不是活缺口** |
|
||||
| `web/essential-plugins.d.ts` `.js.map` | 缺(**`.js` 两机在**:6606 B / 09-25 10:45:57) | 两个引用者已能解析:`web/routes/admin.js:10`、`web/routes/business-plugins.js:20` (`from '../essential-plugins.js'`) | **纯声明/映射边车**(`runtime` 零影响) |
|
||||
| `web/routes/plugin-data.d.ts` `.js.map` | 缺(**`.js` 两机在**:15523 B / 09-25 08:04:57) | 引用者已能解析:`web/server.js:55` (`from './routes/plugin-data.js'`) | 同上 |
|
||||
|
||||
🔴 **一条必须记住的耦合**(本件新出):**本机** `lib/web/routes/im.js:42` 已 `import … from '../../im/gateway-token.js'` ⇒ 新版 `im.js` 与 `im/gateway-token.*` **必须同批投**;⛔ 单投 `im.js` 会让实例在启动期 `ERR_MODULE_NOT_FOUND`。当前两机构成"旧 `im.js` + 无 `gateway-token`"的自洽态,**没有人处于半投状态**。
|
||||
|
||||
### 3-2 ② 3 件运行时 `.js` 谁更新
|
||||
|
||||
| 件 | 本机(12:23 构建) | 两机(09-23 20:59) | 谁更新 |
|
||||
|---|---|---|---|
|
||||
| `im/backends/gateway.js` | 16241 B | 12077 B | **本机** |
|
||||
| `im/connection-backend.js` | 4154 B | 3342 B | **本机** |
|
||||
| `web/routes/im.js` | 47262 B | 36738 B | **本机** |
|
||||
|
||||
- 两机三件**逐字相同**(md5 相同、mtime 均 `2026-09-23 20:59:31`)⇒ 两机部署面 = **同一版 IM 构建**(scp 保留源 mtime,故该时间 = **源构建时间**,非投放时间)。
|
||||
- 代码仓 `git log` **无法作区分判据**:IM 面整体落在同一 commit `e6207aa`(`chore(仓库对齐): 文档库结构治理 + IM/插件线落地`)⇒ **以构建时间 + 体积为判据**。
|
||||
- 差异成因 = 本机含 IM 线 09-23 之后的改动,含本件新出的 `gateway-token` 抽取。
|
||||
|
||||
### 3-3 ③ 结论表与建议
|
||||
|
||||
| 归类 | 件 | 本线可否补 |
|
||||
|---|---|---|
|
||||
| **IM 线未投放件**(⛔ 不归本线) | `im/gateway-token.{js,d.ts,js.map}`(3 件,须与新版 `im.js` 同批) | ⛔ **不补**(单投即事故;同批属 IM 线投放面) |
|
||||
| **IM 线未投放件**(⛔ 不归本线) | 3 件运行时 `.js` 的新版本 | ⛔ **不补**(`src/im/**` + `src/web/routes/im.ts` 属 IM 线 lane,且 `im.ts` 有冻结口径) |
|
||||
| **声明/映射边车(死件)** | `web/essential-plugins.{d.ts,js.map}`、`web/routes/plugin-data.{d.ts,js.map}`(4 件) | 可补但**无收益**:两机运行面为纯 node,**无 TS 消费者**;`.js.map` 只影响调试栈 ⇒ **建议不投**(投 = 制造 4 件永远用不上的文件) |
|
||||
|
||||
> **本线无"部署面活缺口"** ⇒ 第 3 件**无需任何落地动作**;后续若要投,只能由 **IM 线**按"`im.js` + `gateway-token.*` 同批"排棒。
|
||||
|
||||
---
|
||||
|
||||
## 四、卫生与未闭合项
|
||||
|
||||
**卫生(逐条遵守)**:⛔ 未 commit / push |⛔ `src/web/routes/im.ts` 零改动 |⛔ 未重启 106 |⛔ 未手改用户 `home` 下 profile / `.pnpm` |⛔ 未清 `.dsh-module-fallback` 软链(只取证)|⛔ 未改 `D:/github/dsh_shenxian/lib` 之外的别线产物 |✅ 47 `/tmp` 无残留(探针脚本已 `trap` 清理且已复核)|✅ 取证脚本全部落在本工作区 `tmp/`。
|
||||
|
||||
**未闭合**
|
||||
1. 🔴 **106 待重启面 = 48 件**(原 47 件 + 本轮 1 件)⇒ 生效待用户给窗口。
|
||||
2. ⚠️ 第 1 件**无真机长错误消息读数**(需一次真失败的跨机调用)⇒ 未宣称运行面验收。
|
||||
3. ⚠️ 第 2 件结论有 4 条边界(见 §2-3),其中"官方客户端宿主是否在服务端预解析依赖"未展开。
|
||||
4. ⚠️ 第 3 件两机 `lib` 树里的 **33 条部署备份残留**仍未处置(第 22 棒记,属"部署卫生"独立议题)。
|
||||
|
||||
**复现命令**:`tmp/p23-item2-probe{,2,3,4,5}.sh`(第 2 件五批)|`tmp/p23-item3-probe.sh`(第 3 件,两机同一脚本)|`tmp/p23-deploy-backup.sh <代码根>`(投前备份)。
|
||||
Reference in new issue
Block a user