From f4ebc4561207b7cc86cbac2760ff76eb0198a6bf Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:51:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E5=BA=93=E7=BA=BF=E4=B8=8A?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E4=BB=93=E5=BA=93=E6=BA=90=EF=BC=9A=E9=80=92?= =?UTF-8?q?=E5=A2=9E=E7=89=88=E6=9C=AC=E9=87=8D=E5=8F=91=E5=B9=B6=E6=8A=8A?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3=E7=BA=B3=E5=85=A5=E5=8F=91?= =?UTF-8?q?=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 7 个内容已变的模板 templateVersion 升到 0.1.1,blank-2d-canvas 与 blank-web 保持 0.1.0 - 发布脚本把仓库 README.md 覆盖写到 templates/README.md(上限 64 KiB,写在清单之前并回读校验),线上说明文档不再是手工副本 - 修复守卫拒绝时直接 process.exit(1) 触发 libuv 断言崩溃、看不到拒绝原因的问题 - 新增发布用例:说明文档随发布覆盖写且写在清单之前、源目录没有 README 时不写线上文档 - 模板库新增仓库侧 README.md 源文件(发布后与线上逐字节一致) - 技术方案补说明文档契约与 2026-09-21 重发布记录;决策记录与踩坑记录各补一条 - 验证:node --test 27 项、真实发布 28 个对象回读通过、线上清单与 README 核对一致、AGC 壳 3 项线上用例重跑通过、check:doc-index、check:encoding、eslint、git diff --check --- .../template-library/README.md | 10 +++ .../v1/blank-3d-scene/meta.json | 2 +- .../v1/cocos-empty-2d/meta.json | 2 +- .../v1/cocos-empty-3d-hq/meta.json | 2 +- .../v1/cocos-empty-3d/meta.json | 2 +- .../v1/cocos-hello-world/meta.json | 2 +- .../v1/phaser-2d-starter/meta.json | 2 +- .../v1/threejs-3d-starter/meta.json | 2 +- .../shared-memory/decision-log.md | 9 +++ docs/project-memory/shared-memory/pitfalls.md | 8 ++ ...技术方案】AGC模板库与模板建项-2026-09-17.md | 3 + scripts/agc-template-library-publish.mjs | 75 ++++++++++++++----- scripts/agc-template-library-publish.test.mjs | 36 +++++++++ 13 files changed, 128 insertions(+), 27 deletions(-) create mode 100644 apps/ai-game-creator-shell/template-library/README.md diff --git a/apps/ai-game-creator-shell/template-library/README.md b/apps/ai-game-creator-shell/template-library/README.md new file mode 100644 index 000000000..692abfbfc --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/README.md @@ -0,0 +1,10 @@ +# AGC 游戏模板库 + +- 用途:AGC 客户端读取的**游戏模板库**,正文是可直接解压到项目根的 zip(zip 根 == AGC 项目根,例如 `game/index.html`)。 +- 位置:bucket `agc-dev`(endpoint `oss-rg-china-mainland.aliyuncs.com`)的 `templates/` 前缀,随 bucket 公共读。客户端只读 `templates/index.json`(schema `agc-template-library.v1`)。 +- 目录结构:`templates/index.json` + `templates/v1//sha256/<内容摘要>/{template.zip,template.json,cover.*}`;条目元数据含 `tags`、封面键与尺寸/摘要、包大小与 SHA-256、`templateVersion`。所有正文按自身字节摘要寻址,重复发布复用同一对象。 +- 封面支持 `png / jpg / webp / svg`。 +- 客户端只信任「受信任 OSS 主机 + 对象键」,清单里的地址字段不参与请求;下载后强校验包大小与 SHA-256,安装完成的唯一判据是模板目录里的 `installed.json`。 +- 这份文件就是线上 `templates/README.md` 的源:内容随每次发布覆盖写,所以改契约要改这里,不要只改线上对象。 +- 契约与验收以仓库文档 `docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md` 为准;模板源在 `apps/ai-game-creator-shell/template-library/`,发布用 `node scripts/agc-template-library-publish.mjs --source apps/ai-game-creator-shell/template-library [--dry-run] [--only ]`(脚本现场打包 zip、按内容地址上传、逐个回读校验、持锁提交清单)。 +- 同一个 `templateVersion` 的 ZIP 字节发生变化时发布会失败关闭,必须递增该模板的 `templateVersion`;发布不删除历史对象,历史版本仍可按旧键下载。 diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/meta.json b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/meta.json index f099612e4..fe50a2870 100644 --- a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/meta.json +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/meta.json @@ -11,7 +11,7 @@ "runtime": "html", "engine": "three.js", "engineVersion": "0.180.0", - "templateVersion": "0.1.0", + "templateVersion": "0.1.1", "entry": "game/index.html", "coverWidth": 960, "coverHeight": 540 diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/meta.json index 5aa321e36..47569d748 100644 --- a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/meta.json +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/meta.json @@ -11,7 +11,7 @@ "runtime": "cocos", "engine": "cocos-creator", "engineVersion": "3.8.8", - "templateVersion": "0.1.0", + "templateVersion": "0.1.1", "entry": "package.json", "coverWidth": 960, "coverHeight": 540 diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/meta.json index a586e3366..d1b5df3b8 100644 --- a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/meta.json +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/meta.json @@ -11,7 +11,7 @@ "runtime": "cocos", "engine": "cocos-creator", "engineVersion": "3.8.8", - "templateVersion": "0.1.0", + "templateVersion": "0.1.1", "entry": "package.json", "coverWidth": 960, "coverHeight": 540 diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/meta.json index 493f35be0..257389c1c 100644 --- a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/meta.json +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/meta.json @@ -11,7 +11,7 @@ "runtime": "cocos", "engine": "cocos-creator", "engineVersion": "3.8.8", - "templateVersion": "0.1.0", + "templateVersion": "0.1.1", "entry": "package.json", "coverWidth": 960, "coverHeight": 540 diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/meta.json index 8ff875247..fc9079559 100644 --- a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/meta.json +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/meta.json @@ -11,7 +11,7 @@ "runtime": "cocos", "engine": "cocos-creator", "engineVersion": "3.8.8", - "templateVersion": "0.1.0", + "templateVersion": "0.1.1", "entry": "package.json", "coverWidth": 960, "coverHeight": 540 diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/meta.json b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/meta.json index 71f52ba90..9fac2fe2b 100644 --- a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/meta.json +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/meta.json @@ -11,7 +11,7 @@ "runtime": "html", "engine": "phaser", "engineVersion": "4.2.1", - "templateVersion": "0.1.0", + "templateVersion": "0.1.1", "entry": "game/index.html", "coverWidth": 960, "coverHeight": 540 diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/meta.json b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/meta.json index 2b492a71d..4d8910066 100644 --- a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/meta.json +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/meta.json @@ -11,7 +11,7 @@ "runtime": "html", "engine": "three.js", "engineVersion": "0.180.0", - "templateVersion": "0.1.0", + "templateVersion": "0.1.1", "entry": "game/index.html", "coverWidth": 960, "coverHeight": 540 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e708d7af2..447c7f88a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,14 @@ # 决策记录 +## 2026-09-21 模板库线上产物对齐仓库源:递增版本重发 + 说明文档纳入发布 + +- 背景:`agc-dev` 上的 `templates/` 产物停在 2026-09-17 发布的那一版,仓库源在那之后改过(`5e4ff54a9` 删掉模板内嵌 `package-lock.json`、`game.js` / `main.js` 等内容调整),9 个模板里 7 个的 ZIP 与线上不一致;dry-run 被「同一 `templateVersion` 的 ZIP 不得变」门禁拒绝,发布器因此无法把仓库状态发上去。 +- 决策:按门禁要求为内容已变的 7 个模板递增 `templateVersion` 到 `0.1.1`(`blank-2d-canvas`、`blank-web` 内容未变,保持 `0.1.0`),并完成一次真实发布;对象使用内容寻址键 `v1//sha256/<摘要>/…`,`index.json` 与说明文档在发布锁内最后提交,历史对象不删除。 +- 决策(说明文档):`templates/README.md` 不再是手工副本——它由发布脚本从仓库源 `apps/ai-game-creator-shell/template-library/README.md`(≤64 KiB)覆盖写入并回读校验,写在清单之前;改契约只改仓库源。 +- 决策(CLI 行为):发布器遇到门禁拒绝时用 `process.exitCode = 1` 正常退出,不再 `process.exit(1)` 让 Node 在 fetch 句柄未关闭时抛 libuv 断言(此前现场只剩 `Assertion failed`,看不到拒绝原因)。 +- 影响范围:`apps/ai-game-creator-shell/template-library/v1/*/meta.json`、`apps/ai-game-creator-shell/template-library/README.md`、`scripts/agc-template-library-publish.mjs` 与其测试、本文件与模板库技术方案。 +- 验证方式:`node --test scripts/agc-template-library-publish.test.mjs`(27 项,含新增「说明文档随发布覆盖写且写在清单之前」「源目录没有 README 时不写线上文档」);真实发布 28 个对象回读通过;匿名读取线上清单(9 个模板、7 个 `0.1.1`、全部内容寻址键)与 `templates/README.md`(与仓库源逐字节一致);AGC 壳 3 项线上用例(读清单、下载安装、下载并原生建项 Cocos)重跑通过。 + ## 2026-09-21 macOS 发布改为只出 arm64 单架构(Intel 暂不支持) - 背景:Mac 发布管线按 `universal-apple-darwin` 构建,但随包 Node 便携运行时只有**单架构官方发行版**(`stage-node-runtime.mjs` 从 `process.execPath` 取材),于是 macOS Job #7~#13 连续失败在「Node 运行时不支持发布目标:universal-apple-darwin」。期间出现过一版「按宿主架构放行」的过渡实现,它能骗过通用包自检(`check-macos-bundle.mjs` 按 `process.arch` 校验),但 Intel 上那份 arm64 侧车不可执行,并且已发布的 dev-mac 0.1.86 就带着这个缺陷。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 14b72578c..bd9198a62 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,13 @@ # 踩坑与排障记录 +## 发布器守卫拒绝时不要把 process.exit 用在 fetch 句柄未关闭处 + +- 现象:`agc-template-library-publish.mjs --dry-run` 撞上「同一 `templateVersion` 的 ZIP 不得变」门禁时,终端只剩一句 `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c`,看不到任何拒绝原因,看起来像脚本崩溃而不是被拒绝。 +- 原因:`main().catch(...)` 里直接 `process.exit(1)`;此时 dry-run 刚用 fetch 读过公共清单,句柄仍在关闭流程中,Node/libuv 在 Windows 上先抛断言,把真实错误信息挤掉。 +- 处理:`catch` 里只设 `process.exitCode = 1`,让事件循环自然退出;同批把说明文档纳入受管发布(见决策记录 2026-09-21 条目)。 +- 验证:把模板源复制到临时目录、把某个模板的 `templateVersion` 改回与线上同版本并改动一个字节,`--dry-run` 应打印 `同版本 ZIP 内容或尺寸变化,请递增 templateVersion` 且退出码为 1,不再出现 `Assertion failed`。 +- 关联:`scripts/agc-template-library-publish.mjs`。 + ## macOS 只出 arm64 单架构,universal 必须失败关闭 `stage-node-runtime.mjs` 只把**构建宿主的 Node**打成便携运行时(官方发行版是单架构,没有 universal 发行版),而 2026-09-21 之前 `build-macos-ci.mjs` 构建的是 `universal-apple-darwin`:macOS Job #7~#13 因此在 `stageNodeRuntime` 直接抛「Node 运行时不支持发布目标:universal-apple-darwin」。期间出现过一版「按宿主架构放行」的过渡实现(`targetRuntime` 对 universal 返回宿主架构),它能骗过通用包自检(`check-macos-bundle.mjs` 按 `process.arch` 校验),但**Intel Mac 上这份 arm64 侧车不可执行**,等于把坏包发出去。当前决策:macOS 固定只构建 `aarch64-apple-darwin`,清单只登记 `darwin-aarch64`,`targetRuntime('universal-apple-darwin')` 保持失败关闭。恢复 Intel 的正确路径是先在 staging 支持按架构各带一份**同版本**运行时(另下载另一架构官方发行版)并让通用包自检按架构分别校验,再切回 universal 目标、把 `darwin-x86_64` 键登记回去;不得用「只带宿主架构」充数,也不得把 arm64 产物登记成 x86_64 键。 diff --git a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md index 5cee7f513..a70a94d6f 100644 --- a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md +++ b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md @@ -68,6 +68,7 @@ templates/ - 模板源在仓库 `apps/ai-game-creator-shell/template-library/`:`v1//{meta.json, project/**, cover.(png|jpg|webp|svg)}`,`template.zip` **不落仓库**,由脚本按 `project/` 现场打包(条目排序、固定时间戳,同内容重复打包摘要一致)。 - 上传与校验由 [`scripts/agc-template-library-publish.mjs`](../../scripts/agc-template-library-publish.mjs) 完成:`--source apps/ai-game-creator-shell/template-library [--dry-run] [--only ]`。ZIP、封面和元数据分别以自身字节的 SHA-256 定位,只创建新对象或复用逐字节校验一致的已有对象;全部对象回读一致后才更新 `index.json`。失败不回收已上传对象,旧清单及其引用始终可读。 - 只更新指定模板时使用 `--only `,在发布锁内读取最新清单,只替换指定 ID,其余条目和未知扩展字段保留。首次清单 404 可由本次选择初始化;读取异常或清单非法时停止。全量发布也遵守相同锁与版本门禁。 +- `templates/README.md` 与上述正文不同:它不是内容寻址对象,而是**覆盖写的说明文档**,源在仓库 `apps/ai-game-creator-shell/template-library/README.md`(上限 64 KiB),由同一次发布在清单之前写入并回读校验。客户端从不读它,改契约只改仓库源即可,不要再手工维护线上副本。 - 正式发布先通过 `GetBucketVersioning` 确认 Bucket 从未开启版本控制,再用 `x-oss-forbid-overwrite: true` 原子创建 `.publish-lock.json`;版本控制 Enabled、Suspended、检查无权限或无法判定时均在写入前停止。所有写同一清单的发布进程必须使用此锁,发布期间不得改变 Bucket 版本控制配置。锁没有自动过期或抢占机制,已被占用时直接失败,重新执行须重新获取锁并读取最新清单。 - 只释放本任务已明确获取且 owner 标识仍一致的锁。获取结果不明时不猜测删除。正文对象不可变,其写入失败可安全释放本任务的锁;清单 PUT 已发起后若遇到断连、超时或服务端 5xx 等不确定结果,必须保留锁并报错,防止旧在途请求晚于下一发布者写入。清单收到确定成功或确定拒绝响应后才进入正常解锁路径;不自动重发清单写入,也不凭一次 GET 猜测在途 PUT 已结束。遗留锁须在确认原请求及进程已终止或完成后由运维处理;释放失败必须报告,不伪装为发布成功。 - `--dry-run` 仅构造和读取合并计划,不读取凭据、不获取锁、不 PUT/DELETE。发布不删除历史对象,也不提供随发布清理的选项;旧客户端缓存和未完成下载可能仍引用旧键。 @@ -117,6 +118,8 @@ Cocos 回归分别覆盖仓库模板和线上真实 ZIP 的安装、连续建项 `2026-09-21` 客户端接入复核:Rust 定向 24 项通过(新增清单来源判定 5 项:合法远端正文优先并标 `network`、远端失败回退缓存并标 `cache`、无缓存时暴露远端错误、远端正文非法时不用缓存掩盖、缓存损坏时失败关闭;新增建项目失败清理 1 项;并在大小/摘要不一致、越界归档两条用例上补「拒绝后不留安装目录、不产生已下载判据」断言)。3 项线上用例(读线上清单、下载安装线上模板、下载并原生建项 Cocos 模板)本轮全部真实执行通过;匿名 `GET https://agc-dev.oss-rg-china-mainland.aliyuncs.com/templates/index.json` 返回 200、`schemaVersion=agc-template-library.v1`、9 个模板。前端 38 项通过(模型 9、网格 6、页面 12、控制器 11),`appSurface` 首页模板用例 2 项通过(灰度外隐藏入口与推荐位、点击进入模板库不建项),AGC `typecheck` 通过。仍未验证:Creator 内场景运行,以及真机 iOS / Android 观感。 +`2026-09-21` 模板库重发布:把线上库从 09-17 的旧产物对齐到仓库当前源。线上 9 个模板里 7 个(`blank-3d-scene`、`cocos-empty-2d/3d/3d-hq/hello-world`、`phaser-2d-starter`、`threejs-3d-starter`)的 ZIP 与仓库不一致(旧产物来自 PR 定稿前的源,例如模板内嵌 `package-lock.json` 已由 `5e4ff54a9` 删除、`game.js` / `main.js` 后来改过),其中 2 个原本一致(`blank-2d-canvas`、`blank-web`)。按「同版本 ZIP 不得变」门禁,为这 7 个模板递增 `templateVersion` 到 `0.1.1` 后重发:28 个内容寻址对象逐个回读校验通过,`index.json` 与 `templates/README.md` 在锁内提交;线上清单现在全部指向 `v1//sha256/<摘要>/` 键,`libraryVersion=1`、9 个模板、无下架条目。发布脚本本次同时补齐说明文档受管写入(源文件即 `apps/ai-game-creator-shell/template-library/README.md`)并修掉「守卫拒绝后直接 `process.exit(1)` 触发 libuv 断言崩溃、看不到原因」的问题;线上 README 与仓库源已逐字节一致。 + ## 本地压测假数据注入(feature 控制) 模板库的数据源在 Rust 侧(清单校验、安装状态、下载与建项目都在这里),TS 只消费快照做渲染,所以假数据注入也放在 Rust 侧,走与真实完全一致的链路。 diff --git a/scripts/agc-template-library-publish.mjs b/scripts/agc-template-library-publish.mjs index e74b37c3f..215c737aa 100644 --- a/scripts/agc-template-library-publish.mjs +++ b/scripts/agc-template-library-publish.mjs @@ -25,6 +25,8 @@ import { deflateRawSync } from 'node:zlib'; const SCHEMA_VERSION = 'agc-template-library.v1'; const TEMPLATE_SCHEMA_VERSION = 'agc-template.v1'; +/** 说明文档上限:它随每次发布覆盖写 `templates/README.md`,不做内容寻址。 */ +const README_MAX_BYTES = 64 * 1024; const TEMPLATE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/u; const TEMPLATE_VERSION_PATTERN = /^[a-z0-9][a-z0-9._-]{0,31}$/u; const RUNTIMES = new Set(['html', 'unity', 'godot', 'cocos']); @@ -380,7 +382,14 @@ export function buildLibrary(source, prefix, only = []) { body: Buffer.from(`${JSON.stringify(indexJson, null, 2)}\n`, 'utf8'), contentType: 'application/json', }); - return { objects, indexJson }; + // 说明文档不是内容寻址对象:它固定挂在 `templates/README.md`,随每次发布覆盖, + // 客户端从不读取它,但运维和后台都需要看到与当次契约一致的说明。 + const readmePath = join(source, 'README.md'); + const readmeBody = existsSync(readmePath) ? readFileSync(readmePath) : null; + if (readmeBody && readmeBody.length > README_MAX_BYTES) { + throw new Error(`README.md 超过 ${README_MAX_BYTES} 字节上限`); + } + return { objects, indexJson, readmeBody }; } export function mergeLibraryIndex(existing, selected) { @@ -769,25 +778,43 @@ export async function publishLibrary( `${JSON.stringify(indexJson, null, 2)}\n`, 'utf8', ); - // 指针写入结果不明时旧请求可能晚到,必须留锁,不能让后续发布者越过它。 - releaseAllowed = false; - const published = await checkedRequest( - () => client.put(indexKey, indexBytes, 'application/json'), - '发布清单', - ); - if (published.ok) { - releaseAllowed = true; - } else if ( - published.status >= 400 && - published.status < 500 && - published.status !== 408 - ) { - releaseAllowed = true; - throw new Error(`发布清单被拒绝:HTTP ${published.status}`); - } else { - throw new Error(`发布清单结果不明:HTTP ${published.status}`); + // 可变指针(说明文档、库清单)写入结果不明时旧请求可能晚到,必须留锁, + // 不能让后续发布者越过它。清单必须最后写,否则会出现「清单已指新对象、 + // 说明还是旧版」的可观测窗口。 + const pointerWrites = [ + ...(library.readmeBody + ? [ + { + key: `${prefix}/README.md`, + body: library.readmeBody, + contentType: 'text/markdown; charset=utf-8', + }, + ] + : []), + { key: indexKey, body: indexBytes, contentType: 'application/json' }, + ]; + for (const pointer of pointerWrites) { + releaseAllowed = false; + const published = await checkedRequest( + () => client.put(pointer.key, pointer.body, pointer.contentType), + `发布 ${pointer.key}`, + ); + if (published.ok) { + releaseAllowed = true; + } else if ( + published.status >= 400 && + published.status < 500 && + published.status !== 408 + ) { + releaseAllowed = true; + throw new Error(`发布 ${pointer.key} 被拒绝:HTTP ${published.status}`); + } else { + throw new Error( + `发布 ${pointer.key} 结果不明:HTTP ${published.status}`, + ); + } + await verifyObject(client, pointer.key, pointer.body); } - await verifyObject(client, indexKey, indexBytes); } catch (error) { failure = error; } finally { @@ -858,6 +885,11 @@ async function main() { for (const object of library.objects.slice(0, -1)) { console.log(` PUT ${object.key} (${object.body.length} B)`); } + if (library.readmeBody) { + console.log( + ` PUT ${args.prefix}/README.md (${library.readmeBody.length} B,覆盖写)`, + ); + } console.log( ` PUT ${args.prefix}/index.json (${Buffer.byteLength(`${JSON.stringify(indexJson, null, 2)}\n`)} B)`, ); @@ -871,6 +903,9 @@ if ( ) { main().catch((error) => { console.error(`[agc-template-library-publish] ${error.message}`); - process.exit(1); + // 直接 `process.exit(1)` 会在 fetch 句柄尚未关闭时触发 libuv 断言崩溃 + // (Windows 上实测只剩一句 Assertion failed,看不到拒绝原因); + // 交给事件循环自然退出,保留真实错误信息。 + process.exitCode = 1; }); } diff --git a/scripts/agc-template-library-publish.test.mjs b/scripts/agc-template-library-publish.test.mjs index 0bc802a13..4affd962a 100644 --- a/scripts/agc-template-library-publish.test.mjs +++ b/scripts/agc-template-library-publish.test.mjs @@ -190,6 +190,42 @@ const publish = (built, store) => log: () => {}, }); +test('说明文档随发布覆盖写线上 README,且写在清单之前', async () => { + const store = memoryOss(oldPublishedLibrary()); + const built = { + ...library(), + readmeBody: Buffer.from('# AGC 游戏模板库\n\n- 契约说明\n', 'utf8'), + }; + + await publish(built, store); + + assert.equal( + store.objects.get('templates/README.md').toString('utf8'), + built.readmeBody.toString('utf8'), + ); + const putKeys = store.calls + .filter((call) => call.method === 'PUT') + .map((call) => call.key); + assert.deepEqual(putKeys.slice(-2), [ + 'templates/README.md', + 'templates/index.json', + ]); +}); + +test('源目录没有 README 时不写线上说明文档', async () => { + const store = memoryOss(oldPublishedLibrary()); + + await publish({ ...library(), readmeBody: null }, store); + + assert.equal(store.objects.has('templates/README.md'), false); + assert.equal( + store.calls.some( + (call) => call.method === 'PUT' && call.key === 'templates/README.md', + ), + false, + ); +}); + test('所有发布正文均使用自身字节摘要定位,重复打包复用ZIP与封面地址', () => { const built = library(); for (const object of built.objects.filter(