[修复]FlintHub 升级「conflict 死锁」彻查与修复(2026-10-05)

👑Lv.11 元老 🌞 骨干会员
2026-10-23 23:44:05

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/待办事宜/FlintHub_升级conflict死锁_修复.md。

FlintHub 升级「conflict 死锁」彻查与修复(2026-10-05)

症状:升级时反复报 「存在本地已修改的文件(conflict 1 个),需人工确认后再升级」,

且无论重试多少次都不会好转 —— 这是一个永久死锁,不是偶发。


一、结论(一句话)

那 1 个 conflict 就是 version.php。它是升级器自己必然要改写的版本元数据,

三方天然不一致(本地=当前版 / 基线=更早版 / 远端=新版),被 Updater::diff() 误判成「本地已修改」;

而 backup() 见 conflict 即拒、planOps() 又只在 modify 里认它 → 版本号永远写不进去 → 死锁。


二、证据链(全部只读复刻,未改任何生产文件)

2.1 现场事实

项值说明
version.php(磁盘)V1.1.4sha 与 release/core/flinthub1.1.4/version.php 逐字节一致
data/runtime/update_baseline.jsonversion = 1.1.0,at = 2026-10-02T11:42:17基线停在 1.1.0
审计日志 action LIKE 'update%'只有一条成功:V1.0.46 → 1.1.0(2026-10-02)此后再无成功升级
release/core/latest.json指向 1.1.7站长想升到 1.1.7

2.2 只读复刻 Updater::diff(manifest-1.1.7, baseline)

ADD (0):  MODIFY (0):  DELETE (0):
CONFLICT (1):
   - version.php
same = 312   skipped = 4(data/.htaccess, data/web.config, protected/.htaccess, protected/web.config)

三方 sha:

来源sha256实际对应
本地5ef3f11b…= 1.1.4 镜像 → 磁盘是 1.1.4
远端30a4cf9b…= 1.1.7 清单
基线93735d31…= 1.1.0 镜像

→ 本地既不是远端、也不是基线 ⇒ 被判 conflict。

2.3 diff() 的判据(app/Helpers/Updater.php)

foreach ($remote as $p => $sha) {
    if (!isset($local[$p]))            { $add[]    = $p; continue; }
    if ($local[$p]['sha256'] === $sha) { $same++;           continue; }
    if ($base !== null && isset($base[$p]) && $local[$p]['sha256'] !== $base[$p]) {
        $conflict[] = $p;      // ← version.php 必然命中这里
    } else {
        $modify[]   = $p;
    }
}

2.4 死锁链(为什么无法自愈)

conflict(version.php)
 └─ UpdateController::backup() :523  见 conflict 即 return
      ⚠️ 文案说「需人工确认」,但**全站没有任何「确认 / 强制继续」入口**
 └─ 即使绕过:Updater::planOps() :1696
      $version = in_array('version.php', $diff['modify'], true) ? 'version.php' : null;
      → 它在 conflict 里 ⇒ plan.version = null ⇒ version.php **永远不写**
 └─ 升级不成功 ⇒ writeBaseline() 不执行 ⇒ 基线**永远停在旧版本**
 └─ 下次 diff 照样 conflict  ← 回到起点(死锁)

2.5 只读模拟三连

场景conflictversion.php 会被写吗
① 现状(基线 1.1.0)1不会 ✗
② 基线为 null(= 删 update_baseline.json)0会 ✓ ← 这就是「删基线」为何一直有效
③ version.php 豁免 conflict0会 ✓


三、为什么上一轮(10-04)修过还会出现

上轮修的是另一个独立成因 —— 「分批 apply 的游标错位 → 磁盘与基线脱节」,那条已生效。

本轮的成因是 version.php 的三方天然不一致,且上轮的测试场景恰好不会暴露它:

当时基线 == 当前版本,version.php 落入 modify,一切正常。

只有「基线 ≠ 当前版本」时才触发。

触发条件(通用,不限本机)

只要 version.php 被非升级器的方式更新过,基线就落后于版本号 ⇒ 必然 conflict ⇒ 死锁:

  • 宝塔手动覆盖代码 / 上传 zip 解压
  • 开发机直接编辑(工作区即站点根)
  • writeBaseline() 曾静默失败(原来没检查返回值)
  • 从旧包手工部署后再改用升级器

四、修复(方案 A + 两处加固)

4.1 主修复:version.php 豁免 conflict(app/Helpers/Updater.php · diff())

// 内容不同:本地是否偏离了官方基线?
if ($p === 'version.php') { $modify[] = $p; continue; }   // ★ 新增:版本元数据不参与 conflict
if ($base !== null && isset($base[$p]) && $local[$p]['sha256'] !== $base[$p]) {
    $conflict[] = $p;
} else {
    $modify[] = $p;
}

理由:version.php 在 WHITELIST_FILES 里、由 apply() 在所有普通操作之后最后单独写 ——

它是「升级器自己管理的元数据」,升级本来就要改它;用户对它的任何「修改」都会被覆盖(既有设计如此)。

⚠️ 其他文件的 conflict 语义完全不变 —— 本地真改过的普通文件仍会拦下要求人工确认。

4.2 加固①:writeBaseline() 返回值检查(UpdateController.php · apply())

原代码 $this->writeBaseline($manifest, $version); 忽略返回值 → 写失败只进 error.log,

升级却报「成功」,留下「基线落后」的隐患。现改为:

$blOk = $this->writeBaseline($manifest, $version);
if (!$blOk) {
    AuditLog::log('update_baseline_fail', 'core', 0,
        '升级 ' . $version . ' 已写盘,但基线写入失败(下次升级可能提示冲突)');
}

并在响应里补 baseline_written 字段。⚠️ 升级已写盘成功,故不中断流程(锁照常释放、状态照常更新)。

4.3 加固②:conflict 提示补「可操作指引」(新增私有方法 conflictMessage())

原文案只说「需人工确认」,但系统里根本没有确认入口 → 用户无路可走。现补上文件清单 + 解决路径:

存在本地已修改的文件(conflict 1 个),需人工确认后再升级:version.php。

这些文件在站点上的内容与官方基线不一致(多为手工改动,或从旧包直接覆盖而来)。

若确认这些改动可以丢弃,请先备份 data/runtime/update_baseline.json,再把它删除后重试

—— 下次升级成功会自动重建基线。

(两处调用:backup() 的拦截、apply() 后的复核;超过 5 个文件时截断并加「等」。)


五、验证结果(四组探针,全部零残留)

#探针结果
1只读复刻 diff()(正向 + 反向断言)10 PASS / 0 FAIL —— version.php 不在 conflict 里且落入 modify;planOps.version = 'version.php';普通文件仍被判 conflict(保护未削弱);插件作用域无副作用;基线/version.php 未被改动
2沙箱端到端(真实 Updater.php + 最小站点,skill §7 范式)12 PASS / 0 FAIL —— 修复前判据复现 conflict;修复后 diff→planOps→apply→writeBaseline→再 diff 完全收敛(conflict/modify/add/delete 全 0);沙箱零残留
3文案方法(反射跳过后台门禁)7 PASS / 0 FAIL —— 数量/文件名/指引齐备;>5 截断加「等」;全角标点未吞变量
4门禁php -l 8.2 + 8.0 双版本全绿;fh_i18n_check 55 PASS / 0 FAIL;php_var_cjk_check OK

★ 探针 2 的「二次 diff 完全收敛」是关键 —— 它证明修复后下次升级不会再被拦(自愈成立)。


六、✅ 已完成:重建发布包(站长选 A → 出 1.1.8)

本次改的是核心代码,而 release/core/flinthub-core-1.1.7.zip(16:02 出的)不含本修复:

$ diff -rq release/core/flinthub1.1.7 .
Files .../app/Controllers/Admin/UpdateController.php and ./app/Controllers/Admin/UpdateController.php differ
Files .../app/Helpers/Updater.php and ./app/Helpers/Updater.php differ
Files .../version.php and ./version.php differ        ← 预期差异(升级时才写)

不重建包的后果(两条,都会发生):

  1. 站长升级时 diff 会报 conflict 2 个(Updater.php + UpdateController.php,因为包内是旧版)

→ 比现在的 1 个更糟;

  1. 即使绕过,升级会用包内的旧版覆盖站点 → 修复被冲掉,下次照样死锁。

→ 违反红线「发布包必须与站点代码一致」。

站长决定(2026-10-05):选 A —— 出 1.1.8(版本号递增,不动已发布的 1.1.7)。

6.1 出包结果(2026-10-05 18:22)

FH_RELEASE_KEY=D:/keys/flinthub/flinthub-2026.key \
FH_RELEASE_KEY_PASS="$(tr -d '\r\n' < /d/keys/flinthub/.fh_passphrase)" \
  php cli/release_build.php --type=core --version=1.1.8 --sign --key-id=flinthub-2026 \
      --notes=.workbuddy-ai/tmp/fh_118_notes.md

  • 门禁 9 项:#1~#8 全 PASS(#8 签名链路 PASS),#9 SKIP(仅插件包)。
  • 产物:
文件大小sha256
release/core/flinthub-core-1.1.8.zip1,855,424 B / 318 文件 + 42 目录e242bedb1aed5db4935d15ca9f8d0cada5cae5dc99b33153921a2e8a313054d6
release/core/manifest-1.1.8.json317 条目6108be45c08e983dc8f83fc8942bae4af94a9bddc480bb211ae4a310449d1c2f
release/core/latest.jsoncore → 1.1.8(全量包)—

  • manifest 签名:RS256 / key_id=flinthub-2026 / 256 B(站点端自验通过)。
  • 改动说明.md §四:用 --notes 写入了本版修复的完整说明(现象 / 根因 / 修复 / 加固)。
  • 1.1.7 → 1.1.8 差异:diff -rq 实测正好 3 个代码文件(Updater.php、UpdateController.php、version.php)+ 改动说明.md。

6.2 发布后一致性校验(三件套,全绿)

校验结果
fh_manifest_mirror_check.php9 PASS / 0 FAIL(镜像↔manifest↔latest + 站点端验签)
fh_pkg_vs_site.php316/316 逐字节一致(0 不一致 / 0 缺失)
fh_zip_vs_site_118.php(直读 zip 条目 ↔ 站点)316 一致 / 0 不一致 / 0 缺失(排除 version.php、改动说明.md)
manifest_zip_invariant_probe.php1.1.8:哈希不一致 0

  • data/runtime/update_check.json:不存在(无需清理)。
  • 站点根 version.php 仍为 V1.1.4(正确 —— 打包器明确不改站点根那份,包内写 V1.1.8)。

七、给用户的一句话

修复包 = flinthub-core-1.1.8.zip(含 Updater.php 豁免 + UpdateController.php 两处加固)。

若站点当前卡在 conflict:升级到 1.1.8 后不需要删基线 —— 升级成功后会自动重建基线。

(若暂时不便更新代码,才用「备份后删除 data/runtime/update_baseline.json」这一临时绕过手段。)

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| Views 5 | Replies 0

All Replies (0)

No replies yet

Please Log in