Files
dsh_ai1net_server/交付物/共享层台账语义缺陷修法-20260925.md
T
admin 318430c9e9 chore(工作区): 插件投放与分库线收口入库(第 35 棒 + 22:44 拍板落盘)
范围 = 本线(插件投放与分库线)产物 + 记忆类,共 27 件:
- 接续入口_插件投放与分库线(1 件):§0 新增 22:44 拍板行;§2 第 36 棒范围改 5 步、条件步转无条件
- 交付物(22 件):MCN 数据面接入阶段一/阶段二系列(含 B 案落地与 shadow 读数)、
  P0 修通与移动端真机验收、pnpm-EPERM、两机 lib 差异、共享层台账语义、
  基础插件身份与回滚、插件接入验收、插件数据面取数口、移动端迁包与字号扩面、跨机错误消息
- 记忆(4 件):2026-09-24 / 2026-09-25 日志、MEMORY.md、本棒 automation 执行记录

⛔ 未含他线在途改动(只报告、不代提交):机制层 CODEBUDDY.md / state.py /
.codebuddy/rules/server-ops.md、接续入口_IM线、接续入口_StoryForge验收线、
其余 29 个 automation 目录、docs/规则与载体/、归档/、接续包_*。
2026-09-25 23:06:29 +08:00

165 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 共享层台账两条语义缺陷修法(插件投放与分库线 · 第 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'
```