[开发]FlintHub 发布流程手册(发布端)

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

本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/待办事宜/FlintHub_发布流程手册.md。

FlintHub 发布流程手册(发布端)

适用:FlintHub 1.1.0 起的内置升级体系

读者:站长本人 / 任何代跑发布流程的 agent

本文件位置:待办事宜/ —— ⚠️ 该目录不在发布包内(打包器白名单只有 app/ assets/ lang/ cli/ docs/ + 18 个根级文件),

可自由编辑、不会被分发给用户。

最后更新:2026-10-04(新增 §12 插件发布)


0. 30 秒速查

cd /d/phpstudy_pro/WWW
PHP="D:/phpstudy_pro/Extensions/php/php8.2.9nts/php.exe"
PASS=$(tr -d '\r\n' < /d/keys/flinthub/.fh_passphrase)     # 口令文件在站点根之外

# ⓪ 先写「更新内容」(必做!首行 `## <一句话主题>` 会自动成为后台升级页的那行摘要)
#    片段建议放 .workbuddy-ai/tmp/notes-.md,写法见步骤 2

# ① 预演(只报告,不写盘)—— 必跑,先看 8 项门禁
FH_RELEASE_KEY=D:/keys/flinthub/flinthub-2026.key FH_RELEASE_KEY_PASS="$PASS" \
  "$PHP" cli/release_build.php --type=core --version=1.1.1 \
  --notes=.workbuddy-ai/tmp/notes-1.1.1.md --dry-run

# ② 正式构建(含 RSA 签名)
FH_RELEASE_KEY=D:/keys/flinthub/flinthub-2026.key FH_RELEASE_KEY_PASS="$PASS" \
  "$PHP" cli/release_build.php --type=core --version=1.1.1 \
  --notes=.workbuddy-ai/tmp/notes-1.1.1.md --sign --key-id=flinthub-2026

# ③ 构建后必做(两条,缺一不可)
"$PHP" -r '$f="data/runtime/update_check.json"; if(is_file($f)){unlink($f);echo "已清 6h 缓存\n";}else{echo "无缓存\n";}'
"$PHP" .workbuddy-ai/tmp/fh_pkg_vs_site.php          # 必须输出「包与站点代码逐字节一致」

👉 出插件包(--type=plugin)口径不同,别套上面这段 —— 见 §12 插件发布:

多了「activated 翻转 → 出包 → 重建索引 → 还原」一步,且不需要清 update_check.json。


1. 环境前提

项值
站点根(同时就是仓库)D:/phpstudy_pro/WWW
本机 CLI PHPD:/phpstudy_pro/Extensions/php/php8.2.9nts/php.exe(无 pcntl / sodium / opcache)
生产环境2 核 2G VPS + 宝塔 + Nginx + Linux + PHP 8.2.33
发布源目录release/core/(在站点根内,必须对公网可读,见 §9)
私钥目录D:/keys/flinthub/(站点根之外,绝不进包)
公钥表app/Helpers/release_keys.php(随包分发,站点端用它验签)

⚠️ 站点根不是 git 仓库。没有版本控制兜底,所有留档靠 release/ 里的包与 update/ 里的历史目录。


2. 目录与产物布局

D:/phpstudy_pro/WWW/                       ← 站点根 = 仓库
├── cli/
│   ├── release_build.php                  ★ 发布端打包器(本流程主角)
│   ├── release_keygen.php                 密钥生成器(一次性)
│   ├── update_probe.php                   只读差异探针(CLI)
│   ├── update_fetch_probe.php             取包 + 预检探针(CLI)
│   └── env_check.php                      环境自检(站点 500 也能跑)
├── release/                               发布源(对公网可读)
│   └── core/
│       ├── flinthub-core-.zip        扁平 zip(顶层 = 站点根相对路径)
│       ├── manifest-.json            清单(含每文件 sha256 + 签名)
│       ├── latest.json                    ★ 当前发行版(**扁平结构**,见 §5.3)
│       └── flinthub/                 镜像目录(包内容的解包副本,审计凭证)
│           ├── ...(站点根相对结构)
│           ├── version.php                ← 打包器写入**目标版本**
│           └── 改动说明.md                ← 打包器生成
├── update/                                老式 updateN 手工包留档(**已停止维护**,见 §6)
│   └── 99/                                最后一次维护:1.0.46 → 1.1.0
└── 待办事宜/                              计划与文档(**不在包内**)
    └── FlintHub_发布流程手册.md            ← 本文件

D:/keys/flinthub/                          ← ⚠️ 站点根之外
├── flinthub-2026.key                      加密 PKCS#8 私钥
├── flinthub-2026.pub.pem                  公钥备份
└── README-keys.txt                        备份与口令管理提醒

2.1 产物说明

产物作用谁读它
flinthub-core-<ver>.zip站点端下载的升级包;解压到站点根即到位Updater::fetch()
manifest-<ver>.json逐文件 sha256 + 差异依据 + 签名Updater::fetchManifest()
latest.json「当前发行版是哪个」;站点端先读它再决定要不要拉包Updater::fetchLatest()
flinthub<ver>/镜像(审计凭证);⚠️ 内含 install.php,不要上传到生产人工审计

2.2 插件发布相关目录(详见 §12)

D:/phpstudy_pro/WWW/
├── release/plugins/index.json              ★ 插件市场公开清单(对公网可读,schema 2)
└── data/market/                            ⚠️ 受控目录(公网不可读)
    ├── packages/flinthub-plugin--.zip   包体(经 /api/market/download 鉴权下发)
    ├── manifests/manifest--.json        清单(已签名)
    └── mirrors/flinthub-plugin--/       本地镜像(审计凭证,**不上传**)

⚠️ 插件包体绝不进 release/ —— release/ 公开、data/ 受控,这是 F-1 硬边界(见 §12.5)。


3. 一次性准备(换发布机时才需要)

3.1 生成签名密钥

FH_RELEASE_KEY_PASS='<你的口令>' "$PHP" cli/release_keygen.php --key-id=flinthub-2026

  • 私钥写到 D:/keys/flinthub/<key-id>.key(加密 PKCS#8,脚本会断言目标目录在站点根之外)。
  • 公钥合并写入 app/Helpers/release_keys.php → [key_id => PEM],随包分发。
  • ⚠️ 口令只从环境变量读,绝不落盘、绝不走命令行参数(避免进 shell 历史 / ps 列表)。
  • ⚠️ 私钥必须离线备份 2 份;口令存密码管理器。丢了口令 = 私钥作废。
  • 查看当前内置了哪些 key_id:"$PHP" cli/release_keygen.php --list

现有密钥:key_id = flinthub-2026,公钥指纹 sha256:5e95ba314481d19ab0a447d067e20598dcb31d4cb51d205f64a00caea01978b4。

3.2 口令怎么给

推荐写成站点根之外的单个文件,脚本里用 $(tr -d '\r\n' < <文件>) 读入环境变量:

PASS=$(tr -d '\r\n' < /d/keys/flinthub/.fh_passphrase)
FH_RELEASE_KEY_PASS="$PASS" ...

⚠️ 该口令文件只是本机便利措施,与 README-keys.txt 里「本目录不含口令」的约定冲突 → 用完请删,

或改用密码管理器手工输入。


4. 标准发布流程(逐步)

步骤 1 · 确认「本次到底改了什么」

站点根不是 git 仓库,没有 git diff 可用。判定变更集的正确做法(三来源并集):

  1. 上一包内每个文件的 sha256 vs 站点根 → 「内容不同」= 必进包。
  2. mtime 扫描上一包建成时刻之后被改的文件(弱证据,用于发现新文件)。
  3. 新 API 反查:若本次新增了函数/常量/类,反查「谁引用它」补齐。

⚠️ 绝不能只用 mtime(会漏项)或只看 update/N 目录(它可能已过期)。

步骤 2 · 写「更新内容」(必做,别省)

每次发布都必须给 --notes=<file> —— 不给的话:

  • latest.json.core.notes 为空 → 后台「升级」页远端版本下方那行「更新内容」什么都不显示,站长升级前看不到这次改了什么;
  • 包内 改动说明.md 的「四、本版新增」只会写一句模板话(「本版为增量包…只含与基线有差异的文件」),等于没写。

插件流程(§8)一直带 --notes,核心流程这里以前漏了,别再有样学样地省掉。

准备一个 markdown 片段(建议放 .workbuddy-ai/tmp/notes-<ver>.md),要求:

  • 3~6 条面向站长的祈使句,说清「这次多了什么 / 修了什么」,不写内部术语、不写文件名;
  • 首行写 ## <一句话主题> —— 它会自动成为 latest.json.core.notes(后台升级页展示的那行,硬截断 600 字);
  • 想单独控制那行摘要,另用 --notes-brief=<file>(优先级高于 ## 首标题;见打包器 renderBrief())。

示例(1.1.22 实况):

## 插件商店新增「授权前置自检」,一键定位无法授权的原因

- 后台「插件商店 → 授权」新增「授权前置自检」,逐项检测回跳地址 / 主社区地址 / 出网与证书 / 会话与二次验证
- 检测出网与证书时会区分 TLS 证书失败与网络不可达,并原样显示报错,便于站长自查
- 自检为只读,不修改任何配置

步骤 3 · 预演(必跑)

FH_RELEASE_KEY=... FH_RELEASE_KEY_PASS=... \
  "$PHP" cli/release_build.php --type=core --version= \
  --notes=.workbuddy-ai/tmp/notes-.md --dry-run

  • 打印:包内文件数、排除清单(按原因分组)、8 项门禁结果。
  • 想额外看文件清单:加 --list。
  • 任何一项门禁 FAIL 就不要继续,先修。
  • 预演完顺手确认 latest.json.core.notes 非空(步骤 2 的 ## 首标题被正确摘出)。

步骤 4 · 正式构建

FH_RELEASE_KEY=... FH_RELEASE_KEY_PASS=... \
  "$PHP" cli/release_build.php --type=core --version= \
  --notes=.workbuddy-ai/tmp/notes-.md --sign --key-id=flinthub-2026

⚠️ 必须加 --sign —— 站点端 Updater::verifyManifest() 默认拒签,

未签名的包会在 preflight 阶段被拒(reason=unsigned)。

产物与 sha256 会打印在最后(记下来,写进交接说明)。

步骤 5 · 构建后必做(两条)

  1. 清 data/runtime/update_check.json

它是「检查更新」结果的 6 小时缓存,里面存着旧包的 sha256。

包一换不清 → 站点端非 force 检查会读到旧哈希 → 取包校验失败。

> 「检查更新」按钮走 force=1 可绕过;locateManifest() 恒 ttl=0;但普通检查会踩。

  1. 跑「包 ↔ 站点逐字节一致」

```bash

"$PHP" .workbuddy-ai/tmp/fh_pkg_vs_site.php

```

必须输出 ✅ 包与站点代码逐字节一致(排除 version.php / 改动说明.md 两件生成物)。

⚠️ 不一致 = 升级即降级(把站点代码覆盖回旧版)。

步骤 6 · 验证

套件命令期望
只读差异探针"$PHP" cli/update_probe.php --no-cache本地版本 → 远端版本、差异五分类、预检 10 项、conflict 0
取包 + 预检"$PHP" cli/update_fetch_probe.php包 sha256 校验通过、预检通过、守卫零变化
★ 直读 zip"$PHP" .workbuddy-ai/tmp/zip_vs_site.php <ver>逐条目 ↔ 站点逐字节一致(version.php 按预期差异放行)
镜像一致性"$PHP" .workbuddy-ai/tmp/fh_manifest_mirror_check.php9 PASS(无参自动探测最新版本)
验签"$PHP" .workbuddy-ai/tmp/fh_p4_sign_verify.php14 PASS
验签 + 取包见附录 A.4ok=true signed=true key_id=flinthub-2026
后台升级页冒烟bash .workbuddy-ai/tmp/fh_ui_smoke.sh24 PASS / 0 FAIL
首页 / 主题页冒烟bash .workbuddy-ai/tmp/fh_theme_smoke.sh9 PASS / 0 FAIL
i18n 三语比对"$PHP" .workbuddy-ai/tools/fh_i18n_check.php53 PASS / 0 FAIL
全角变量检查"$PHP" .workbuddy-ai/tools/php_var_cjk_check.php <改动文件...>无命中

步骤 7 · 部署上线

上传(宝塔文件管理器 / FTP / rsync 均可,保持相对路径):

release/core/flinthub-core-.zip
release/core/manifest-.json
release/core/latest.json

不要上传 release/core/flinthub<ver>/(镜像目录,内含 install.php)。

部署后:站点端会自行按需拉包,无需重启。若改了 PHP 配置或新装了扩展才需要重启 PHP。


5. 版本号与发布类型规则

5.1 版本号

  • 单一版本源 = version.php 的 FLINTHUB_VERSION,形如 V1.1.0(带 V)。
  • 站点根那份不随建包改动;打包器只往镜像里写目标版本。
  • Updater::normVersion() 会剥掉前导 V/v 再比较。
  • ⚠️ 陷阱:version_compare('1.1.0-B2','1.1.0-beta1') = -1 —— 大写 B2 不被识别为 beta 别名。版本号里不要用大写预发布标记。

5.2 发布类型(当前实际支持)

类型命令说明
全量包--version=<ver>(不加 --base)full=true。含站点根白名单内全部文件。任意版本可直升
增量包--version=<ver> --base=<base>⚠️ 当前是半成品,见 §10.1,不要用于生产

保留策略(pruneBeta() 自动执行,只清预发布包):

  • 版本号含 - 的(如 1.1.0-beta2)→ 只保留最新 3 个(--keep-beta=N 可调),同步删 zip + manifest-<ver>.json + 镜像目录。
  • 版本号不含 - 的(正式版)→ 永不自动删除。

5.3 latest.json 结构(★ 2026-10-02 起扁平)

{
  "schema": 1,
  "generated_at": "2026-10-02T02:14:36+00:00",
  "core": {
    "version": "1.1.0",
    "base_version": null,
    "package":  { "url": "/release/core/flinthub-core-1.1.0.zip", "size": 1801748, "sha256": "72f7e7f0…" },
    "manifest": { "url": "/release/core/manifest-1.1.0.json", "sha256": "728176f4…" }
  },
  "plugins": { "index_url": "/release/plugins/index.json" }
}

  • ⚠️ core 直接就是发行对象,不再有 stable / beta 嵌套。
  • ⚠️ 只有一个发行版,不区分正式/测试通道;后台也没有通道选择器。
  • 旧格式(带嵌套)会被站点端按「官网暂未发布可用版本」处理。

5.4 站点端版本校验(preflight 第 10 项)

  • 全量包(full=true)→ SKIP,任意版本可直升。
  • 增量包 → 要求 base_version ≤ 当前版本 < 目标版本(区间,不是严格相等)。

6. update/N 老式手工包(已停止维护)

update/99/ 是最后一次维护的老式增量包(V1.0.46 → V1.1.0),1.1.0 起不再随版本更新。

如果仍要同步它:

"$PHP" .workbuddy-ai/tmp/fh_u99_sync.php            # 把仓库改动文件复制进去 + 写版本号
"$PHP" .workbuddy-ai/tmp/fh_u99_diff2.php           # 子集差异复核(只应剩 version.php 刻意不同)
"$PHP" .workbuddy-ai/tmp/fh_u99_manifest_check.php  # ★ 文档里的 SHA256 块逐条校验

⚠️ update/99/version.php 刻意写 V1.0.99(不是 V1.1.0):覆盖后站点自报 V1.0.99

→ 在线升级页才会看到「远端 1.1.0 可用」→ 可实测完整升级链路。

⚠️ update/N 是手工目录,不受打包器白名单保护 —— 放进去什么就分发什么。同步后必须:

  1. 列出目录全部文件,逐个问「它该不该被分发」。

绝不能进任何分发目录的两类:

  • 一次性/调试脚本(如 _fh_diag.php):往往自带硬编码 token 当门禁,token 随包公开 = 等于无门禁;

还会输出站点绝对路径 / error_log 路径 / 运行时状态 = 信息泄露。且极易过期(改结构后仍按旧格式解析 → 诊断结论是错的)。

  • 站点私有文件(如 config.php):每站 DB 路径 / SITE_URL / 密钥都不同,整份覆盖 = 毁站。
  1. 改动说明.md 的文件清单必须与目录实际内容一致。

★ 清单没提、目录里却有 = 高危信号,一律先当遗留物处理。


7. 8 项发布门禁(打包器自动跑)

#门禁内容
#1i18n 键集合三语(zh / zh_tw / en)键集合零差异 + 键序一致
#2PHP 语法双版本(8.2 + 8.0)全量 php -l
#3编码BOM 0 / CRLF 0 / 非 UTF-8 0(有白名单例外)
#4调试残留无 var_dump / print_r / dd( 等
#5卫生无 config.php / .bak / .zip / .sqlite / .log
#6包结构复核顶层目录与根级文件符合预期
#7路径安全包内容 ⊆ 升级器白名单 + 路径大小写一致
#8签名链路自检私钥 ↔ 公钥配对、签名往返自验通过

门禁 #7 是防「装得上、升不了」的关键:包里若有升级器白名单之外的文件,

它会静默出现在 diff 的 skipped 里 —— 用户装了包,那个文件却永远不生效。

打包器必须复用 Updater 的常量(WHITELIST_DIRS / WHITELIST_FILES / ASSETS_EXCLUDES / BLACKLIST_*),

不许自抄清单,否则两边会漂移。


8. 白名单(决定「什么会进包」)

Updater 的常量是唯一真相源:

WHITELIST_DIRS   = ['app/', 'assets/', 'lang/', 'cli/', 'docs/']
WHITELIST_FILES  = ['index.php','cron_trigger.php','install.php','sw.js','version.php',
                    '.htaccess','web.config','nginx-server.conf','nginx-baota.conf','robots.txt',
                    'README.md','INSTALL.md','CHANGELOG.md','LICENSE',
                    'favicon.ico','favicon.svg','logo.svg','flinthub-badge.svg']
ASSETS_EXCLUDES  = ['assets/uploads/', 'assets/themes/']
BLACKLIST_EXT    = ['zip', 'bak', 'log', 'sqlite']
BLACKLIST_SUFFIX = ['-wal', '-shm']

  • 不会进包:config.php(站点私有)、data/ 与 protected/ 的实际内容(只带 .htaccess / web.config 自防护文件)、

assets/uploads/(用户上传)、assets/themes/(目录主题)、plugins/(插件是独立发布包)、

release/、update/、待办事宜/、审计/、根级散落文件。

  • 会进包但升级器会 skip:data/.htaccess、data/web.config、protected/.htaccess、protected/web.config

—— 这 4 个「防护骨架文件」供全新安装与手工补,升级器硬黑名单不写它们(门禁 #7 对它们豁免)。


9. 红线清单(务必遵守)

#红线原因
1config.php 永不进包站点私有,覆盖 = 毁站。改它要同步改 install.php::configTemplate()
2私钥永不进站点根 / 仓库 / 发布包泄露 = 签名体系失效
3口令只走环境变量,不落盘、不进命令行参数避免 shell 历史 / ps 列表泄露
4/release/ 必须对公网可读它是下游所有安装的更新源。⚠️ 禁止一把 deny all
5不上传镜像目录 release/core/flinthub<ver>/内含 install.php,有执行面
6包必须与站点代码逐字节一致否则升级 = 降级
7构建后必清 data/runtime/update_check.json6h 缓存存着旧包 sha256 → 取包校验失败
8latest.json 保持扁平站点端已不认 stable/beta 嵌套
9改过 改动说明.md 的 SHA256 清单后必须逐条校验手改极易出错(改错对象 / 重复行 / 错哈希)
10升级包不写数据库、不删数据回滚 = 覆盖备份即可
11Windows 上别拿生产站当靶子跑 applyrename() 无法覆盖「正在执行的入口脚本」→ 拒绝访问(5);Linux 无此问题
12改插件静态资源必须 bump 版本号?v= 是唯一的破缓存手段


10. 已知未实现 / 待办

10.1 ⚠️ 打包器 --base=<ver> 是半成品,不要用于生产

现状:--base 只影响元数据(manifest.base_version、manifest.full=false、改动说明文案),

完全不做文件过滤 —— 产出的包声称是「增量包」,实际仍是全量内容。

而且改动说明里会自动写一句「本版为增量包(基线 V<base> → V<ver>),只含与基线有差异的文件」

—— 这句话目前是假的。

要落地还需(用户已批准的方案):

  1. --base 真按基线过滤(只留新增 / 内容变化),并把「基线有、本次无」写进 manifest.deleted。
  2. 累积增量包命名加 -cum- 前缀(区分全量包,prune 才安全)。
  3. 实现累积增量包清理(同基准下只留最新一个,同步删 manifest-*.json)。
  4. 以上都落地后,才把「只含差异文件」这句话保留在文案里。

站点端已就绪:preflight 第 10 项、diff() 的 full 守卫都已支持累积增量包。

10.2 其他

  • ✅ release/core/ 的 beta1/beta2 开发期产物已于 2026-10-02 手工清理(zip + 镜像目录 + manifest 共 6 项)。

现发布源只剩正式版:flinthub-core-1.1.0.zip + flinthub1.1.0/ + manifest-1.1.0.json + latest.json。

备份留档在站点根之外 D:/phpstudy_pro/_flinthub_beta_backup_20261002/(14 MB)。

今后 beta 仍按 §5.2 规则「留最近 3 个」自动保留;清理是手工动作,打包器不会自动删正式版。

  • assets/css/themes/themes.zip 是仓库里的游离文件;不会进包(门禁 #5 排除 .zip),无害。
  • 根级散落着 config2.php、install2.php、version.php.bak、cb_dbg.log、update.zip、

install.zip、nginx-baota.zip、flinthub1.0.0.zip 等历史文件 —— 都不进包,但建议清理以免误传。


11. 故障排查

症状先查什么
后台升级页「远端版本 / 清单签名」恒显示未知① data/runtime/update_check.json 是否为旧缓存(清掉重试);② protected/error.log 有无 warning 污染 JSON 响应
/admin/update/check 前端报 SyntaxError: Unexpected token '<'同上 —— 老 config.php 的 SERVER_PORT warning 被 display_errors=On 打进了 JSON 体。已由 index.php / cron_trigger.php 顶部抑制修复
取包后 sha256 不一致,但包看起来是好的忘了清 update_check.json(缓存里的旧哈希)
升级后站点代码变旧包与站点不一致 —— 跑 fh_pkg_vs_site.php,重新构建包
包内某文件在升级里永远不生效该文件不在 Updater 白名单 → 门禁 #7 应拦下;跑 --dry-run 看是否 PASS
apply() 报「落盘失败(write/rename)」只在 Windows 本机测试时出现(入口脚本自身被覆盖);Linux 生产无此问题
想二次 apply 却报 409回滚会释放升级锁 → 必须重新预检取锁;且成功升级后 last_backup 被清空,需重新备份
升级失败但 error.log 没内容写操作失败是返回值,落点在审计表 data/meta/business.sqlite 的 audit_logs(action LIKE 'update%')


12. 插件发布(Hub 官方路径)

适用:把 plugins/<目录名>/ 作为官方插件发布到 Hub 插件市场(商店里出现卡片 + 安装/升级按钮)。

对照:§4 讲的是核心包(--type=core);本节是插件包(--type=plugin)—— 流程同构,口径不同,别混。

权威细则:.workbuddy-ai/skills/flinthub-release-package/SKILL.md 的 §0.5b(含逐条实证);本节是它的成文版。

最后更新:2026-10-04(首个完整走完本节流程的实例:floor_reply 1.3.1)

12.1 两条插件发布路径(边界别混)

路径入口允许根级文件产物落点
后台发布(Hub 官方,本节)cli/release_build.php --type=plugin✅ 允许根级 改动说明.mddata/market/{packages,mirrors,manifests}/
前台投稿(用户投稿)/plughub/submit❌ 禁止任何根级文件(每文件须 plugins/<name>/ 开头)由 Hub 侧接收

⚠️ 前台投稿包的硬约束(单根 plugins/<name>/、表单字段约束表)见 docs/插件开发规范.md §11.35。

12.2 四步流程

cd /d/phpstudy_pro/WWW
PHP="D:/phpstudy_pro/Extensions/php/php8.2.9nts/php.exe"
. .workbuddy-ai/tmp/fh_key_env.sh      # 提供 FH_RELEASE_KEY / FH_RELEASE_KEY_ID / 口令

# ① 改版本号(plugin.json 的 version;见 §12.3)
# ② 翻转 activated → false(★插件专属,见 §12.4)
"$PHP" .workbuddy-ai/tmp/fh_flip_activated.php <目录名> flip

# ③ 出包(★必须 --sign)
"$PHP" cli/release_build.php --type=plugin --name=<目录名> --version= \
      --sign --key-id="$FH_RELEASE_KEY_ID" --notes=<改动说明.md 路径>

# ④ 重建公开清单
"$PHP" cli/release_build.php --type=index

# ⑤ 还原 activated(★必须在 ③④ 之后)
"$PHP" .workbuddy-ai/tmp/fh_flip_activated.php <目录名> restore

# ⑥ 验收(见 §12.7)
"$PHP" .workbuddy-ai/tmp/fh_plugin_verify.php <目录名> 
python .workbuddy-ai/tmp/verify_market_release.py

⚠️ ③④ 必须在 flip 与 restore 之间(否则包内 activated 是 true)。

⚠️ 命令分步执行,别串成一条超长命令(本机沙箱会因命令过长拒绝解析,报 decisionRecord missing ...)。

12.3 版本号规则

项规则
真相源plugins/<目录名>/plugin.json 的 version(不带 V;带 V 的是核心 version.php)
默认口径次版本 +1(a.b.c → a.(b+1).0)—— 与既有 50 个插件的 x.Y.0 谱系一致
例外站长可指定 patch 位(如纯修复走 1.3.0 → 1.3.1)—— 由站长拍板,agent 不擅自决定
必须同步plugin.json + 说明.md 版本行 + 代码文件头注释(三处一起改)
⚠️ 别用大写预发布标记version_compare('1.1.0-B2','1.1.0-beta1') = -1(同核心,见 §5.1)

12.4 ★★ 包内 plugin.json.activated 必须是 false(插件专属红线)

为什么:市场安装页有「安装后自动启用」勾选框(app/Views/admin/_market_action.php:22,name="activate",默认 checked)

→ 启用与否由站长安装时决定,包里的值只能是中性默认。

⚠️ PluginUpdater::PRESERVE_KEYS = ['activated','sort_order'] 只在「升级」时回填本地值;

全新安装时 localPreserve() 因 localConfig() 为 null 返回 [] → 包里的值被直接采用。

包内带 true ⇒ 新站装完直接启用,且那个勾选框失效(isActivated() 已 true → 跳过 activate 分支)。

⚠️ 别被现状误导:仓库里已发布的包多数带 true —— 那只是构建机的本地启用态被原样打进去的潜在缺陷,

不是设计口径(依据:docs/插件开发规范.md:256「activated 由系统在激活/禁用时自动写回」)。

做法:用 fh_flip_activated.php <目录名> flip|restore —— 快照 两个文件

(plugin.json + plugins/plugins_cache.json),还原顺序 json 先、缓存后。

⚠️ 为什么必须连缓存一起快照:Plugin::cacheFresh()(Plugin.php:388-408)只要发现**任一 plugin.json

比 plugins_cache.json 新,就判缓存过期 → 全量重扫 + saveCache() 重写缓存文件**。

只还原 plugin.json 会留下一个被重写过的缓存;还原顺序反了则缓存 mtime 反而更旧 → 下次请求又触发重扫。

12.5 产物三件套与交付形态

产物路径是否上传
包data/market/packages/flinthub-plugin-<name>-<ver>.zip✅ 上传(增量新增)
清单data/market/manifests/manifest-<name>-<ver>.json✅ 上传(已签名;增量新增)
公开索引release/plugins/index.json❌ 不上传(见下方 ⚠️⚠️)
镜像data/market/mirrors/flinthub-plugin-<name>-<ver>/❌ 不上传(本地审计凭证)

交付 = 这 2 个文件(包 + 清单),按站点根相对路径增量新增到 Hub 站点根

—— ⚠️ 只加这两个文件,不要整目录覆盖 data/market/manifests/。

⚠️⚠️ release/plugins/index.json 不上传(1.1.7「下架」功能起)

它是 data/market/manifests/ 的确定性函数(Hub 自己会重建)。上传它会:

① 复活已下架插件(归档在 manifests-off/ 的那些);

② 把只在 Hub 上存在的社区投稿从清单里抹掉(本地 index 不含它们)。

正确做法:包 + 清单传完后,到 Hub 后台 市场 Hub → 设置 Tab → 点「重建清单」

(POST /admin/market-hub/rebuild)。Hub 从自己的 manifests/ 重算清单,

按「同一插件只留最高版」→ 新版本自动生效,已下架 / 社区投稿状态原样保留。

⚠️ F-1 硬边界:清单公开(release/plugins/index.json 对公网可读);

包体受控(data/ 有三级防护,公网不可读,经 /api/market/download 鉴权流式输出)

→ 插件包体绝不进 release/。

⚠️ 旧版本包不要删(同 §5.2 核心策略)—— 客户端有 6h 清单缓存(Market::INDEX_TTL),

未同步的站仍按旧清单请求旧版本 → 旧包删了会 404(窗口 ≤6h)。打包器也不会自动清理正式版。

12.6 门禁 9 项(比核心包多一项)

#门禁插件包特有说明
#1~#8同 §7(i18n / 语法双版本 / 编码 / 调试残留 / 卫生 / 包结构 / 路径安全 / 签名链路自检)#1 的 i18n 校验**含 plugins/*/lang/**;#7 校验「包内路径 ⊆ plugins/<name>/ + 根级 改动说明.md」
#9命名空间一致性独立子进程实测 class_exists('\Plugin\<目录名>\Plugin');命名空间须被 Autoloader 三条候选之一命中(详见 docs/插件开发规范.md §5.1)

12.7 发布后验收

"$PHP" .workbuddy-ai/tmp/fh_plugin_verify.php <目录名>    # 单包 20 断言(只读)
python .workbuddy-ai/tmp/verify_market_release.py              # 市场全量(期望「全部通过 ✓」)

fh_plugin_verify.php 覆盖:清单可解析 → Updater::verifyManifest() 验签 → 包 sha256/size 对声明 →

manifest.files ↔ 站点根逐字节 → zip 条目 ↔ 站点根逐字节 → 镜像集合 == zip 集合 →

release/plugins/index.json 条目(version/sha256/size/manifest.sha256/signed/icon/url)→

plugin.json 版本已同步 → 本机 activated===true(还原态) → 包内 activated===false → 旧版本包仍在。

⚠️⚠️ 两条「逐字节一致」断言必须对 plugins/<name>/plugin.json 豁免 —— 包内 activated=false、站点根 true,

不豁免会必然报 2 条假 FAIL(2026-10-04 出 floor_reply 1.3.1 实测)。

正解 = 该文件改用「除 activated 外 JSON 等价」比较,其余文件照旧逐字节。

⚠️ 别断言「zip 文件集合 == manifest.files 集合」 —— 根级 改动说明.md 是打包器有意写进 zip 与镜像、

但不进 manifest.files 的文档(升级器按 manifest 逐条取文件 → 它永不被安装)。

正确断言 = 差集恰好 ['改动说明.md']。

12.8 插件发布红线

#红线原因
1包内 activated 必须 false否则新站装完直接启用,站长勾选框失效(§12.4)
2本机 activated 必须还原成原样构建机就是真实运行站,忘了还原 = 站点插件被静默停用
3镜像目录不上传本地审计凭证,无分发价值
4插件包体绝不进 release/F-1 边界:release/ 公开,包体须经鉴权下载
5旧版本包不删客户端 6h 清单缓存期内仍可能请求旧版(§12.5)
6必须 --sign站点端 Updater::verifyManifest() 默认拒签
7改静态资源必须 bump 版本号?v= 是唯一破缓存手段(同核心,见 §9 红线 12)
8release/plugins/index.json 不上传上传会复活已下架插件 + 抹掉只在 Hub 的社区投稿(§12.5)
9不整目录覆盖 data/market/manifests/同上;且会物理删掉 Hub 上独有的社区投稿 manifest

12.9 已知坑(都踩过)

症状真因处置
验收报 2 条「逐字节不一致」,都指向 plugin.json探针没豁免 activated(包内 false / 站点根 true)用 §12.7 的「除 activated 外 JSON 等价」口径
验收报「文件集合不等」断言写成「zip == manifest.files」差集应恰好 ['改动说明.md'](§12.7)
出包命令整条被沙箱拒绝命令过长 / 引号复杂拆成多条分步执行
商店里没有卡片新清单没重建 / 包+清单没到位① 传完包+清单后忘了点「重建清单」(§12.5)→ 先点它;② 同机 Hub 每次现读、不走 TTL,不是缓存问题
下架过的插件又出现在商店上传了本地 index.json 或整目录覆盖了 manifests/见 §12.5 ⚠️⚠️;把 Hub 上 manifests-off/ 的 manifest 移回 manifests/ 可重新下架
卡片只有「详情」、没有「安装」核心 bug(视图数据),非插件问题1.1.6 已修(见 skill §0.5b ⑥);旧站先升级核心
卡片在,按钮是「升级到 x.y.z」已装且远端更高正常升级,非故障

12.10 本节用到的脚本

脚本作用
.workbuddy-ai/tmp/fh_key_env.sh发布密钥环境(私钥路径 / key_id / 口令)
`.workbuddy-ai/tmp/fh_flip_activated.php <目录名> flip\restore`
.workbuddy-ai/tmp/fh_plugin_verify.php <目录名> [版本]单包发布验收(20 断言,只读)
.workbuddy-ai/tmp/verify_market_release.py市场全量端到端验收(50 插件 / 图标表 / CRC / 路径)
.workbuddy-ai/tmp/build_market_release.py汇总「可上传目录」到 .workbuddy-ai/output/market-release/


附录 A · 命令清单

A.1 环境变量(可 source 复用)

# .workbuddy-ai/tmp/fh_key_env.sh 已封装(本机专用)
. /d/phpstudy_pro/WWW/.workbuddy-ai/tmp/fh_key_env.sh
# 提供:FH_RELEASE_KEY / FH_RELEASE_KEY_ID / FH_RELEASE_KEY_PASS_FILE

A.2 构建

cd /d/phpstudy_pro/WWW
PHP="D:/phpstudy_pro/Extensions/php/php8.2.9nts/php.exe"
PASS=$(tr -d '\r\n' < /d/keys/flinthub/.fh_passphrase)
RUN() { FH_RELEASE_KEY=D:/keys/flinthub/flinthub-2026.key FH_RELEASE_KEY_PASS="$PASS" "$PHP" cli/release_build.php "$@"; }

RUN --type=core --version=1.1.1 --dry-run          # 预演
RUN --type=core --version=1.1.1 --dry-run --list   # 预演 + 文件清单
RUN --type=core --version=1.1.1 --sign --key-id=flinthub-2026   # 正式
RUN --type=plugin --name=<目录名> --version=1.1.1 --sign --key-id=flinthub-2026  # 插件包

A.3 构建后

"$PHP" -r '$f="data/runtime/update_check.json"; if(is_file($f)){unlink($f);echo "已清\n";}else{echo "无缓存\n";}'
"$PHP" .workbuddy-ai/tmp/fh_pkg_vs_site.php

A.4 验签 + 取包校验

"$PHP" -r '
require_once "app/Core/Autoloader.php"; use app\Helpers\Updater; ini_set("display_errors","0");
$mf = Updater::readJson("release/core/manifest-.json");
$s  = Updater::verifyManifest($mf, false);
echo "验签: ok=",var_export($s["ok"]??null,true)," signed=",var_export($s["signed"]??null,true)," key_id=",($s["key_id"]??"-"),"\n";
echo "取包: ",json_encode(Updater::verifyPackage("release/core/flinthub-core-.zip",$mf),JSON_UNESCAPED_UNICODE),"\n";'

A.5 回归套件一览

脚本覆盖基线结果
.workbuddy-ai/tmp/fh_ui_smoke.sh后台升级页 HTTP(页面 200 / 无原始 i18n 键 / 未登录 302 / 缺 CSRF 403 / check 正例 / 已删路由 404 / error.log 零新增)24/24
.workbuddy-ai/tmp/fh_theme_smoke.sh首页 + 后台主题页 + 主题 CSS 可达性9/9
.workbuddy-ai/tmp/fh_pkg_vs_site.php发布包镜像 ↔ 站点逐文件 sha256316/316
.workbuddy-ai/tmp/zip_vs_site.php <ver>★ 直读 zip 每个条目 ↔ 站点逐字节(剔除目录条目 + 改动说明.md;version.php 按「预期差异」断言包内为目标版本)317/317
.workbuddy-ai/tmp/fh_manifest_mirror_check.php镜像 ↔ manifest ↔ latest.json + 站点端验签(无参自动探测最新版本)9/9
.workbuddy-ai/tmp/fh_p4_sign_verify.php签名链路(含篡改 4 类 / 未签名默认拒 / 键序无关)14/14
cli/update_fetch_probe.php取包 sha256 + 预检 10 项 + 增量守卫19/19
.workbuddy-ai/tmp/fh_u99_diff2.phpupdate/99 子集差异—
.workbuddy-ai/tmp/fh_u99_sync.phpupdate/99 同步—
.workbuddy-ai/tmp/fh_u99_manifest_check.php改动说明.md SHA256 块逐条校验 + 漏项反查全 PASS / 漏项 0
.workbuddy-ai/tmp/fh_plugin_verify.php <目录名> [版本]插件包发布验收(20 断言,只读)20/20(floor_reply 1.3.1)
`.workbuddy-ai/tmp/fh_flip_activated.php <目录名> flip\restore`插件发布 activated 口径翻转 / 按字节还原
.workbuddy-ai/tmp/verify_market_release.py插件市场全量端到端验收(50 插件 / 图标表 / CRC / 路径)全部通过 ✓
.workbuddy-ai/tmp/build_market_release.py汇总「可上传目录」到 .workbuddy-ai/output/market-release/—
.workbuddy-ai/tmp/mksess.php造管理员会话夹具(php mksess.php 1 admin / php mksess.php del <sid>)—
.workbuddy-ai/tools/fh_i18n_check.php三语键集合 + 键序 + 代码引用键53/53
.workbuddy-ai/tools/php_var_cjk_check.php全角标点紧跟 $var 的静默空值风险0 命中
.workbuddy-ai/tmp/fh_p4_sandbox_test.php升级内核沙箱(备份/应用/回滚)32/32
.workbuddy-ai/tmp/fh_p4_rehearsal*.sh站点完整副本实弹演练41/41


附录 B · 给 agent 的执行契约

如果你是一个被指派「发布 FlintHub 新版本」的 agent,请遵守:

必做

  1. 读完本文件再动手。特别是 §9 红线、§10.1(--base 未实现)。
  2. 先预演(--dry-run),8 项门禁全 PASS 才正式构建。
  3. 正式构建必须带 --sign --key-id=flinthub-2026。
  4. 构建后必做两条:清 update_check.json + 跑 fh_pkg_vs_site.php。
  5. 把产物 sha256(zip / manifest)原文抄进回复,方便人工核对。
  6. 报告要包含:版本号、包大小、文件数、两项 sha256、门禁结果、验证套件结果。
  7. 出插件包(--type=plugin)走 §12,别套用核心包步骤 —— 多一步 activated 翻转/还原;

验收用 fh_plugin_verify.php;不需要清 update_check.json(那是核心更新检查的缓存)。

禁止

  1. 不要改 config.php、不要把它或任何站点私有文件放进任何分发目录。
  2. 不要动 D:/keys/flinthub/ 里的私钥;不要把口令写进任何文件或命令历史。
  3. 不要删除 release/core/ 里正式版(版本号不含 -)的包 —— 全量包永久保留。
  4. 不要在生产站上真跑 apply / rollback(要演练就造副本,见 fh_p4_rehearsal*.sh)。
  5. 不要清页面缓存(站长可能正开着页面自测)。
  6. 不要用 rm 通配删文件;删文件一律用 PHP unlink() / rmdir(),且只按精确文件名删自己刚建的。
  7. 改过 改动说明.md 的 SHA256 清单后,必须跑 fh_u99_manifest_check.php,别信肉眼。
  8. 出插件包时不要忘记还原本机 activated —— 构建机就是真实运行站(§12.4 / §12.8)。

判停条件

  • 门禁任一项 FAIL → 停下来修,不要继续。
  • fh_pkg_vs_site.php 报不一致 → 停下来重新构建,不要上线。
  • 不确定某个文件该不该进包 → 停下来问,不要猜。
  • 插件包验收报「plugin.json 逐字节不一致」→ 先怀疑断言没豁免 activated,别急着改包(§12.7)。
轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 瀏覽 6 次 | 回覆 0 次

全部回覆 (0)

暫無回覆

請 登入