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

👑Lv.11 元老 🌞 骨干会员
2026-10-23 22:15:37
> 本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:WWW/待办事宜/FlintHub_发布流程手册.md。 # FlintHub 发布流程手册(发布端) > **适用**:FlintHub 1.1.0 起的内置升级体系 > **读者**:站长本人 / 任何代跑发布流程的 agent > **本文件位置**:`待办事宜/` —— ⚠️ 该目录**不在发布包内**(打包器白名单只有 `app/ assets/ lang/ cli/ docs/` + 18 个根级文件), > 可自由编辑、不会被分发给用户。 > **最后更新**:2026-10-04(新增 §12 插件发布) --- ## 0. 30 秒速查 ```bash 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 PHP | `D:/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-.zip` | 站点端下载的升级包;解压到站点根即到位 | `Updater::fetch()` | | `manifest-.json` | 逐文件 sha256 + 差异依据 + 签名 | `Updater::fetchManifest()` | | `latest.json` | 「当前发行版是哪个」;站点端先读它再决定要不要拉包 | `Updater::fetchLatest()` | | `flinthub/` | 镜像(审计凭证);⚠️ 内含 `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 生成签名密钥 ```bash FH_RELEASE_KEY_PASS='<你的口令>' "$PHP" cli/release_keygen.php --key-id=flinthub-2026 ``` - 私钥写到 `D:/keys/flinthub/.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' < <文件>)` 读入环境变量: ```bash 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=`** —— 不给的话: - `latest.json.core.notes` 为空 → **后台「升级」页远端版本下方那行「更新内容」什么都不显示**,站长升级前看不到这次改了什么; - 包内 `改动说明.md` 的「四、本版新增」只会写一句模板话(「本版为**增量包**…只含与基线有差异的文件」),等于没写。 > 插件流程(§8)一直带 `--notes`,核心流程这里以前**漏了**,别再有样学样地省掉。 准备一个 markdown 片段(建议放 `.workbuddy-ai/tmp/notes-.md`),要求: - 3~6 条**面向站长**的祈使句,说清「这次多了什么 / 修了什么」,不写内部术语、不写文件名; - **首行写 `## <一句话主题>`** —— 它会**自动**成为 `latest.json.core.notes`(后台升级页展示的那行,硬截断 600 字); - 想单独控制那行摘要,另用 `--notes-brief=`(优先级高于 `## ` 首标题;见打包器 `renderBrief()`)。 示例(1.1.22 实况): ```markdown ## 插件商店新增「授权前置自检」,一键定位无法授权的原因 - 后台「插件商店 → 授权」新增「授权前置自检」,逐项检测回跳地址 / 主社区地址 / 出网与证书 / 会话与二次验证 - 检测出网与证书时会区分 TLS 证书失败与网络不可达,并原样显示报错,便于站长自查 - 自检为只读,不修改任何配置 ``` ### 步骤 3 · 预演(必跑) ```bash 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 · 正式构建 ```bash 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`;但普通检查会踩。 2. **跑「包 ↔ 站点逐字节一致」** ```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 ` | 逐条目 ↔ 站点逐字节一致(`version.php` 按预期差异放行) | | 镜像一致性 | `"$PHP" .workbuddy-ai/tmp/fh_manifest_mirror_check.php` | **9 PASS**(无参自动探测最新版本) | | 验签 | `"$PHP" .workbuddy-ai/tmp/fh_p4_sign_verify.php` | **14 PASS** | | 验签 + 取包 | 见附录 A.4 | `ok=true signed=true key_id=flinthub-2026` | | 后台升级页冒烟 | `bash .workbuddy-ai/tmp/fh_ui_smoke.sh` | **24 PASS / 0 FAIL** | | 首页 / 主题页冒烟 | `bash .workbuddy-ai/tmp/fh_theme_smoke.sh` | **9 PASS / 0 FAIL** | | i18n 三语比对 | `"$PHP" .workbuddy-ai/tools/fh_i18n_check.php` | **53 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/`(镜像目录,内含 `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=`(不加 `--base`) | `full=true`。含站点根白名单内**全部**文件。任意版本可直升 | | 增量包 | `--version= --base=` | ⚠️ **当前是半成品,见 §10.1,不要用于生产** | **保留策略**(`pruneBeta()` 自动执行,只清**预发布包**): - 版本号**含 `-`** 的(如 `1.1.0-beta2`)→ 只保留最新 **3** 个(`--keep-beta=N` 可调),同步删 zip + `manifest-.json` + 镜像目录。 - 版本号**不含 `-`** 的(正式版)→ **永不自动删除**。 ### 5.3 `latest.json` 结构(★ 2026-10-02 起**扁平**) ```json { "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 起不再随版本更新**。 **如果仍要同步它**: ```bash "$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` / 密钥都不同,整份覆盖 = **毁站**。 2. **`改动说明.md` 的文件清单必须与目录实际内容一致**。 ★ **清单没提、目录里却有 = 高危信号**,一律先当遗留物处理。 --- ## 7. 8 项发布门禁(打包器自动跑) | # | 门禁 | 内容 | |:--|:--|:--| | #1 | i18n 键集合 | 三语(`zh` / `zh_tw` / `en`)键集合零差异 + **键序一致** | | #2 | PHP 语法 | **双版本**(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` 的常量是唯一真相源: ```php 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. 红线清单(务必遵守) | # | 红线 | 原因 | |:--|:--|:--| | 1 | **`config.php` 永不进包** | 站点私有,覆盖 = 毁站。改它要同步改 `install.php::configTemplate()` | | 2 | **私钥永不进站点根 / 仓库 / 发布包** | 泄露 = 签名体系失效 | | 3 | **口令只走环境变量**,不落盘、不进命令行参数 | 避免 shell 历史 / `ps` 列表泄露 | | 4 | **`/release/` 必须对公网可读** | 它是**下游所有安装的更新源**。⚠️ **禁止一把 `deny all`** | | 5 | **不上传镜像目录 `release/core/flinthub/`** | 内含 `install.php`,有执行面 | | 6 | **包必须与站点代码逐字节一致** | 否则升级 = 降级 | | 7 | **构建后必清 `data/runtime/update_check.json`** | 6h 缓存存着旧包 sha256 → 取包校验失败 | | 8 | **`latest.json` 保持扁平** | 站点端已不认 `stable`/`beta` 嵌套 | | 9 | **改过 `改动说明.md` 的 SHA256 清单后必须逐条校验** | 手改极易出错(改错对象 / 重复行 / 错哈希) | | 10 | **升级包不写数据库、不删数据** | 回滚 = 覆盖备份即可 | | 11 | **Windows 上别拿生产站当靶子跑 apply** | `rename()` 无法覆盖「正在执行的入口脚本」→ 拒绝访问(5);Linux 无此问题 | | 12 | **改插件静态资源必须 bump 版本号** | `?v=` 是唯一的破缓存手段 | --- ## 10. 已知未实现 / 待办 ### 10.1 ⚠️ 打包器 `--base=` 是**半成品,不要用于生产** 现状:`--base` 只影响**元数据**(`manifest.base_version`、`manifest.full=false`、改动说明文案), **完全不做文件过滤** —— 产出的包声称是「增量包」,实际仍是全量内容。 而且改动说明里会自动写一句「本版为**增量包**(基线 `V` → `V`),**只含与基线有差异的文件**」 —— **这句话目前是假的**。 **要落地还需**(用户已批准的方案): 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` | ✅ 允许根级 `改动说明.md` | `data/market/{packages,mirrors,manifests}/` | | 前台投稿(用户投稿) | `/plughub/submit` | ❌ 禁止任何根级文件(每文件须 `plugins//` 开头) | 由 Hub 侧接收 | ⚠️ 前台投稿包的硬约束(单根 `plugins//`、表单字段约束表)见 `docs/插件开发规范.md` §11.35。 ### 12.2 四步流程 ```bash 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--.zip` | ✅ 上传(**增量新增**) | | 清单 | `data/market/manifests/manifest--.json` | ✅ 上传(**已签名**;**增量新增**) | | 公开索引 | `release/plugins/index.json` | ❌ **不上传**(见下方 ⚠️⚠️) | | 镜像 | `data/market/mirrors/flinthub-plugin--/` | ❌ **不上传**(本地审计凭证) | **交付 = 这 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//` + 根级 `改动说明.md`」 | | **#9** | **命名空间一致性** | **独立子进程**实测 `class_exists('\Plugin\<目录名>\Plugin')`;命名空间须被 `Autoloader` 三条候选之一命中(详见 `docs/插件开发规范.md` §5.1) | ### 12.7 发布后验收 ```bash "$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//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) | | 8 | **`release/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` | `activated` 口径翻转 / 按字节还原(快照 json + 缓存) | | `.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 复用) ```bash # .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 构建 ```bash 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 构建后 ```bash "$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 验签 + 取包校验 ```bash "$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` | 发布包镜像 ↔ 站点逐文件 sha256 | 316/316 | | `.workbuddy-ai/tmp/zip_vs_site.php ` | ★ **直读 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.php` | `update/99` 子集差异 | — | | `.workbuddy-ai/tmp/fh_u99_sync.php` | `update/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 `) | — | | `.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社区系统。
| 浏览 2 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录