164 lines
10 KiB
Markdown
164 lines
10 KiB
Markdown
# 共享层台账两条语义缺陷修法(插件投放与分库线 · 第 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'
|
|||
|
|
```
|