From 7eaaa1a499a500a49cbabb30508f20b4c3840ed6 Mon Sep 17 00:00:00 2001 From: kdletters <61648117+kdletters@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:24:50 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=A8=A1=E6=9D=BF=E5=BA=93?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=BA=90=E3=80=81=E7=A9=BA=E7=99=BD=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E4=B8=8E=E7=A1=AE=E5=AE=9A=E6=80=A7=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增仓库模板源 apps/ai-game-creator-shell/template-library/:空白网页、空白二维画布、空白三维场景三个空白模板,以及 Phaser 2D、Three.js 3D 两个起步工程模板 - 发布脚本改为按 project/ 现场打包确定性 zip(条目排序 + 固定时间戳),支持 svg 封面、--prune 清理旧对象与 --index-out - 模板库新增真连检查:读取线上清单、下载并安装线上模板包后校验文件落盘 - 刷新线上清单 fixture,并同步技术方案与实施计划的封面格式、模板源与发布方式 --- .../src-tauri/src/template_library.rs | 51 +- .../fixtures/agc-template-library-index.json | 97 +- .../v1/blank-2d-canvas/cover.svg | 13 + .../v1/blank-2d-canvas/meta.json | 18 + .../blank-2d-canvas/project/game/index.html | 10 + .../v1/blank-2d-canvas/project/game/main.js | 30 + .../blank-2d-canvas/project/game/package.json | 12 + .../v1/blank-2d-canvas/project/game/style.css | 3 + .../project/game/vite.config.js | 7 + .../v1/blank-3d-scene/cover.svg | 13 + .../v1/blank-3d-scene/meta.json | 18 + .../v1/blank-3d-scene/project/game/index.html | 10 + .../v1/blank-3d-scene/project/game/main.js | 38 + .../blank-3d-scene/project/game/package.json | 15 + .../v1/blank-3d-scene/project/game/style.css | 3 + .../project/game/vite.config.js | 7 + .../template-library/v1/blank-web/cover.svg | 13 + .../template-library/v1/blank-web/meta.json | 18 + .../v1/blank-web/project/game/index.html | 10 + .../v1/blank-web/project/game/main.js | 2 + .../v1/blank-web/project/game/package.json | 12 + .../v1/blank-web/project/game/style.css | 2 + .../v1/blank-web/project/game/vite.config.js | 7 + .../v1/phaser-2d-starter/cover.svg | 13 + .../v1/phaser-2d-starter/meta.json | 18 + .../v1/phaser-2d-starter/project/game/game.js | 21 + .../phaser-2d-starter/project/game/index.html | 9 + .../project/game/package-lock.json | 1138 +++++++++++++++++ .../project/game/package.json | 15 + .../phaser-2d-starter/project/game/style.css | 2 + .../project/game/vite.config.js | 7 + .../v1/threejs-3d-starter/cover.svg | 13 + .../v1/threejs-3d-starter/meta.json | 18 + .../project/game/index.html | 10 + .../threejs-3d-starter/project/game/main.js | 43 + .../project/game/package.json | 15 + .../threejs-3d-starter/project/game/style.css | 3 + .../project/game/vite.config.js | 7 + ...实施计划】AGC模板库客户端接入-2026-09-17.md | 5 +- ...技术方案】AGC模板库与模板建项-2026-09-17.md | 6 +- scripts/agc-template-library-publish.mjs | 296 ++++- 41 files changed, 1962 insertions(+), 86 deletions(-) create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/cover.svg create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/meta.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/index.html create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/main.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/package.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/style.css create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/vite.config.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/cover.svg create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/meta.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/index.html create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/package.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/style.css create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/vite.config.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-web/cover.svg create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-web/meta.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/index.html create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/main.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/package.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/style.css create mode 100644 apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/vite.config.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/cover.svg create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/meta.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/game.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/index.html create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package-lock.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/style.css create mode 100644 apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/vite.config.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/cover.svg create mode 100644 apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/meta.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/index.html create mode 100644 apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js create mode 100644 apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/package.json create mode 100644 apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/style.css create mode 100644 apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/vite.config.js diff --git a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs index 00e0b9c96..56e76c3af 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs @@ -874,9 +874,18 @@ mod tests { .iter() .map(|entry| entry.id.as_str()) .collect::>(), - vec!["phaser-2d-starter", "threejs-3d-starter"] + vec![ + "blank-2d-canvas", + "blank-3d-scene", + "blank-web", + "phaser-2d-starter", + "threejs-3d-starter", + ] ); - let first = templates.first().expect("first template"); + let first = templates + .iter() + .find(|entry| entry.id == "phaser-2d-starter") + .expect("phaser template"); assert_eq!(first.runtime, "html"); assert_eq!(first.tags.first().map(String::as_str), Some("起步工程")); assert!(first.cover_width > 0 && first.cover_height > 0); @@ -889,7 +898,7 @@ mod tests { ); assert_eq!( entry.cover_url, - format!("{base}/templates/v1/phaser-2d-starter/cover.png") + format!("{base}/templates/v1/phaser-2d-starter/cover.svg") ); assert!(!entry.installed); } @@ -1028,4 +1037,40 @@ mod tests { assert!(zip_url.starts_with(&format!("{base}/"))); } } + + /// 可选的真连检查:下载线上模板包并安装到临时目录,证明"清单 → 下载 → 摘要校验 → 解压"整条链路可用。 + #[tokio::test] + #[ignore = "需要网络:下载并安装线上模板包"] + async fn downloads_and_installs_a_live_template() { + let cache_root = tempfile::tempdir().expect("temp dir"); + let base = template_library_base().expect("trusted base"); + let client = build_template_library_client(); + let body = fetch_limited_bytes( + &client, + &format!("{base}/{TEMPLATE_LIBRARY_INDEX_KEY}"), + TEMPLATE_LIBRARY_MAX_INDEX_BYTES, + ) + .await + .expect("fetch live index"); + let body = String::from_utf8(body).expect("utf-8 index"); + let (_, templates) = parse_game_template_library_index(&body).expect("parse live index"); + let entry = templates + .iter() + .find(|template| template.id == "blank-web") + .expect("线上应存在 blank-web 模板"); + let bytes = fetch_limited_bytes( + &client, + &template_object_url(&entry.zip_key).expect("trusted zip url"), + TEMPLATE_ARCHIVE_MAX_BYTES, + ) + .await + .expect("download live template"); + let record = install_template_archive(cache_root.path(), entry, &bytes) + .expect("install live template"); + assert!(record.file_count >= 5, "模板文件数 {}", record.file_count); + let project_root = Path::new(&record.project_dir); + assert!(project_root.join("game/index.html").is_file()); + assert!(project_root.join("game/main.js").is_file()); + assert!(project_root.join(TEMPLATE_INSTALLED_MARKER_FILE).is_file()); + } } diff --git a/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json b/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json index 256670fe0..7cf1b85a7 100644 --- a/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json +++ b/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json @@ -2,8 +2,83 @@ "schemaVersion": "agc-template-library.v1", "library": "agc-game-templates", "libraryVersion": 1, - "updatedAt": "2026-09-17T03:06:41Z", + "updatedAt": "2026-09-17T03:22:43Z", "templates": [ + { + "id": "blank-2d-canvas", + "title": "空白二维画布工程", + "summary": "原生 Canvas 二维空白工程:自适应画布、按设备像素比缩放与 requestAnimationFrame 主循环已就绪。", + "tags": [ + "空白", + "起步工程", + "2d", + "canvas" + ], + "runtime": "html", + "engine": "canvas", + "engineVersion": "", + "templateVersion": "0.1.0", + "updatedAt": "2026-09-17T03:22:43Z", + "entry": "game/index.html", + "zipKey": "templates/v1/blank-2d-canvas/template.zip", + "zipSizeBytes": 1534, + "zipSha256": "ff8f84e4793941acaf161738c2795f65c5d5390de8614f51aa9e3a5771767134", + "coverKey": "templates/v1/blank-2d-canvas/cover.svg", + "coverWidth": 960, + "coverHeight": 540, + "coverSha256": "afb753dc6d3de0f9fb6e03ec94f2be7221dd04c9d4ab3cf92311879f0af25192", + "metadataKey": "templates/v1/blank-2d-canvas/template.json" + }, + { + "id": "blank-3d-scene", + "title": "空白三维场景工程", + "summary": "Three.js 空白场景:空场景、透视相机、网格地面与自适应视口已就绪,适合从零搭三维玩法。", + "tags": [ + "空白", + "起步工程", + "3d", + "three.js" + ], + "runtime": "html", + "engine": "three.js", + "engineVersion": "0.180.0", + "templateVersion": "0.1.0", + "updatedAt": "2026-09-17T03:22:43Z", + "entry": "game/index.html", + "zipKey": "templates/v1/blank-3d-scene/template.zip", + "zipSizeBytes": 1644, + "zipSha256": "f3f295f4e5adcf1445d75229dc1b583376a9bc96d3f27a257d69ee3b7cace892", + "coverKey": "templates/v1/blank-3d-scene/cover.svg", + "coverWidth": 960, + "coverHeight": 540, + "coverSha256": "1429232adaf6df4457e45b4fc8d7ee9fab2e6bffce9f3f0016e91b81bb66c6a7", + "metadataKey": "templates/v1/blank-3d-scene/template.json" + }, + { + "id": "blank-web", + "title": "空白网页工程", + "summary": "最小网页工程(HTML + CSS + 原生 JS + Vite),没有任何引擎依赖,适合从零写玩法。", + "tags": [ + "空白", + "起步工程", + "网页", + "原生" + ], + "runtime": "html", + "engine": "none", + "engineVersion": "", + "templateVersion": "0.1.0", + "updatedAt": "2026-09-17T03:22:43Z", + "entry": "game/index.html", + "zipKey": "templates/v1/blank-web/template.zip", + "zipSizeBytes": 1212, + "zipSha256": "6fa4391f30342e8dcbdcf735f990d2534ea50405f119e4fa5879b83e8f00119e", + "coverKey": "templates/v1/blank-web/cover.svg", + "coverWidth": 960, + "coverHeight": 540, + "coverSha256": "326a2753386618971b1311043effa46c2e74bc1cfb116862c0ee4097dcd5086a", + "metadataKey": "templates/v1/blank-web/template.json" + }, { "id": "phaser-2d-starter", "title": "Phaser 2D 起步工程", @@ -18,15 +93,15 @@ "engine": "phaser", "engineVersion": "4.2.1", "templateVersion": "0.1.0", - "updatedAt": "2026-09-17T03:06:41Z", + "updatedAt": "2026-09-17T03:22:43Z", "entry": "game/index.html", "zipKey": "templates/v1/phaser-2d-starter/template.zip", - "zipSizeBytes": 8823, - "zipSha256": "6d3decc9f770a3e5225c387fb6c907561823a72f2f40da932e71308e6288cc97", - "coverKey": "templates/v1/phaser-2d-starter/cover.png", + "zipSizeBytes": 8770, + "zipSha256": "9026856c3c0b3a42401172e36ce8b450a65e9f11eb9096624d8990d51449d8ce", + "coverKey": "templates/v1/phaser-2d-starter/cover.svg", "coverWidth": 960, "coverHeight": 540, - "coverSha256": "5fcf952aa89c302258fcf462344d7d799a8bd2a79070d5412c94ac269e313376", + "coverSha256": "fdb422027bf54bf755b2b91cd21fa10fdd7ce3f5c7e3ccd9ac3ffba602b12b96", "metadataKey": "templates/v1/phaser-2d-starter/template.json" }, { @@ -43,15 +118,15 @@ "engine": "three.js", "engineVersion": "0.180.0", "templateVersion": "0.1.0", - "updatedAt": "2026-09-17T03:06:41Z", + "updatedAt": "2026-09-17T03:22:43Z", "entry": "game/index.html", "zipKey": "templates/v1/threejs-3d-starter/template.zip", - "zipSizeBytes": 1700, - "zipSha256": "dd353052fd61990ace3aa4bafeb8b48a7121133f33c976dd4865fff9761fc44b", - "coverKey": "templates/v1/threejs-3d-starter/cover.png", + "zipSizeBytes": 1697, + "zipSha256": "03096152b17cd6d55e7f6ccd518485136133cb54fd2a5a5d0ac8e0974149155c", + "coverKey": "templates/v1/threejs-3d-starter/cover.svg", "coverWidth": 960, "coverHeight": 540, - "coverSha256": "3a4d3080853737663d970a21a63e9d6b5cb523b31c5463d2c2dd94fddf66f187", + "coverSha256": "7ba013e8a8b515d7aff7146fe401afe5ba3416b9c69db181179705e1bce01beb", "metadataKey": "templates/v1/threejs-3d-starter/template.json" } ] diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/cover.svg b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/cover.svg new file mode 100644 index 000000000..48439503b --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/cover.svg @@ -0,0 +1,13 @@ + + + + + + + + + + 空白二维画布工程 + AGC 空白模板 · Canvas 2D 主循环 + 自适应画布与渲染循环已接好,直接画自己的东西 + diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/meta.json b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/meta.json new file mode 100644 index 000000000..5c3bdbb8e --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/meta.json @@ -0,0 +1,18 @@ +{ + "id": "blank-2d-canvas", + "title": "空白二维画布工程", + "summary": "原生 Canvas 二维空白工程:自适应画布、按设备像素比缩放与 requestAnimationFrame 主循环已就绪。", + "tags": [ + "空白", + "起步工程", + "2d", + "canvas" + ], + "runtime": "html", + "engine": "canvas", + "engineVersion": "", + "templateVersion": "0.1.0", + "entry": "game/index.html", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/index.html b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/index.html new file mode 100644 index 000000000..0aa1ef189 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/index.html @@ -0,0 +1,10 @@ + + + + + + Genarrative Game Draft + + +
+ diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/main.js b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/main.js new file mode 100644 index 000000000..bfdce3f80 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/main.js @@ -0,0 +1,30 @@ +const canvas = document.querySelector('#stage'); +const context = canvas.getContext('2d'); +let viewport = { width: 0, height: 0 }; + +function resize() { + const ratio = window.devicePixelRatio || 1; + viewport = { width: window.innerWidth, height: window.innerHeight }; + canvas.width = Math.floor(viewport.width * ratio); + canvas.height = Math.floor(viewport.height * ratio); + context.setTransform(ratio, 0, 0, ratio, 0, 0); +} + +function update(_deltaSeconds) {} + +function render() { + context.clearRect(0, 0, viewport.width, viewport.height); +} + +let previous = performance.now(); +function frame(now) { + const deltaSeconds = Math.min((now - previous) / 1000, 0.1); + previous = now; + update(deltaSeconds); + render(); + requestAnimationFrame(frame); +} + +window.addEventListener('resize', resize); +resize(); +requestAnimationFrame(frame); diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/package.json b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/package.json new file mode 100644 index 000000000..6352b75bc --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/package.json @@ -0,0 +1,12 @@ +{ + "name": "agc-game", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "vite": "^6.2.0" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/style.css b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/style.css new file mode 100644 index 000000000..f1256b925 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/style.css @@ -0,0 +1,3 @@ +body { margin: 0; overflow: hidden; background: #0b1512; } +main { display: block; width: 100vw; height: 100vh; } +canvas { display: block; width: 100%; height: 100%; } diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/vite.config.js b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/vite.config.js new file mode 100644 index 000000000..d8e631e07 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-2d-canvas/project/game/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: './', + build: { outDir: 'dist', emptyOutDir: true }, +}); diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/cover.svg b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/cover.svg new file mode 100644 index 000000000..8da38262d --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/cover.svg @@ -0,0 +1,13 @@ + + + + + + + + + + 空白三维场景工程 + AGC 空白模板 · Three.js 空场景 + 空场景 + 相机 + 网格地面,直接摆自己的三维内容 + 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 new file mode 100644 index 000000000..f099612e4 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/meta.json @@ -0,0 +1,18 @@ +{ + "id": "blank-3d-scene", + "title": "空白三维场景工程", + "summary": "Three.js 空白场景:空场景、透视相机、网格地面与自适应视口已就绪,适合从零搭三维玩法。", + "tags": [ + "空白", + "起步工程", + "3d", + "three.js" + ], + "runtime": "html", + "engine": "three.js", + "engineVersion": "0.180.0", + "templateVersion": "0.1.0", + "entry": "game/index.html", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/index.html b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/index.html new file mode 100644 index 000000000..d01aa860a --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/index.html @@ -0,0 +1,10 @@ + + + + + + Genarrative Game Draft + + +
+ diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js new file mode 100644 index 000000000..73c37324b --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/main.js @@ -0,0 +1,38 @@ +import * as THREE from 'three'; + +const container = document.querySelector('#game'); +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x070b14); + +const camera = new THREE.PerspectiveCamera( + 60, + window.innerWidth / window.innerHeight, + 0.1, + 200, +); +camera.position.set(0, 3, 6); +camera.lookAt(0, 0, 0); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +container.append(renderer.domElement); + +const light = new THREE.DirectionalLight(0xffffff, 1.2); +light.position.set(4, 8, 6); +scene.add(light, new THREE.AmbientLight(0x8899ff, 0.5)); + +const grid = new THREE.GridHelper(20, 20, 0x3b4a6b, 0x1d2739); +scene.add(grid); + +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); + +function render() { + renderer.render(scene, camera); + requestAnimationFrame(render); +} +render(); diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/package.json b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/package.json new file mode 100644 index 000000000..496deca74 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/package.json @@ -0,0 +1,15 @@ +{ + "name": "agc-game", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "three": "^0.180.0" + }, + "devDependencies": { + "vite": "^6.2.0" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/style.css b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/style.css new file mode 100644 index 000000000..a3c5a9c8c --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/style.css @@ -0,0 +1,3 @@ +body { margin: 0; overflow: hidden; background: #05070d; } +main { display: block; width: 100vw; height: 100vh; } +canvas { display: block; } diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/vite.config.js b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/vite.config.js new file mode 100644 index 000000000..d8e631e07 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-3d-scene/project/game/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: './', + build: { outDir: 'dist', emptyOutDir: true }, +}); diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-web/cover.svg b/apps/ai-game-creator-shell/template-library/v1/blank-web/cover.svg new file mode 100644 index 000000000..ac7c02d1b --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-web/cover.svg @@ -0,0 +1,13 @@ + + + + + + + + + + 空白网页工程 + AGC 空白模板 · HTML / CSS / 原生 JS + 零依赖最小工程,从 main.js 开始写自己的玩法 + diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-web/meta.json b/apps/ai-game-creator-shell/template-library/v1/blank-web/meta.json new file mode 100644 index 000000000..a92652137 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-web/meta.json @@ -0,0 +1,18 @@ +{ + "id": "blank-web", + "title": "空白网页工程", + "summary": "最小网页工程(HTML + CSS + 原生 JS + Vite),没有任何引擎依赖,适合从零写玩法。", + "tags": [ + "空白", + "起步工程", + "网页", + "原生" + ], + "runtime": "html", + "engine": "none", + "engineVersion": "", + "templateVersion": "0.1.0", + "entry": "game/index.html", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/index.html b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/index.html new file mode 100644 index 000000000..d01aa860a --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/index.html @@ -0,0 +1,10 @@ + + + + + + Genarrative Game Draft + + +
+ diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/main.js b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/main.js new file mode 100644 index 000000000..f2307680d --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/main.js @@ -0,0 +1,2 @@ +const root = document.querySelector('#game'); +root.textContent = ''; diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/package.json b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/package.json new file mode 100644 index 000000000..6352b75bc --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/package.json @@ -0,0 +1,12 @@ +{ + "name": "agc-game", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "vite": "^6.2.0" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/style.css b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/style.css new file mode 100644 index 000000000..e40ed7036 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/style.css @@ -0,0 +1,2 @@ +body { margin: 0; background: #0f1218; color: #e8eefc; font: 16px system-ui, sans-serif; } +main { display: grid; min-height: 100vh; place-items: center; } diff --git a/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/vite.config.js b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/vite.config.js new file mode 100644 index 000000000..d8e631e07 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/blank-web/project/game/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: './', + build: { outDir: 'dist', emptyOutDir: true }, +}); diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/cover.svg b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/cover.svg new file mode 100644 index 000000000..0d00d4b79 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/cover.svg @@ -0,0 +1,13 @@ + + + + + + + + + + Phaser 2D 起步工程 + AGC 起步工程模板 · 网页 / Phaser + Vite + 新建项目即得可运行脚手架,可直接进入对话开始创作 + 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 new file mode 100644 index 000000000..71f52ba90 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/meta.json @@ -0,0 +1,18 @@ +{ + "id": "phaser-2d-starter", + "title": "Phaser 2D 起步工程", + "summary": "AGC 新建项目使用的默认二维起步工程(Phaser 4 + Vite),解压后即为可运行项目根。", + "tags": [ + "起步工程", + "2d", + "phaser", + "像素" + ], + "runtime": "html", + "engine": "phaser", + "engineVersion": "4.2.1", + "templateVersion": "0.1.0", + "entry": "game/index.html", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/game.js b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/game.js new file mode 100644 index 000000000..a77ba6be9 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/game.js @@ -0,0 +1,21 @@ +import './style.css'; + +import Phaser from 'phaser'; + +class PlaceholderScene extends Phaser.Scene { + create() { + this.add.text( + 24, + 24, + '还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。', + ); + } +} + +new Phaser.Game({ + type: Phaser.AUTO, + width: 720, + height: 420, + parent: 'game', + scene: PlaceholderScene, +}); diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/index.html b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/index.html new file mode 100644 index 000000000..3447d5909 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/index.html @@ -0,0 +1,9 @@ + + + + + + Genarrative Game Draft + +
+ diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package-lock.json b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package-lock.json new file mode 100644 index 000000000..45ece2543 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package-lock.json @@ -0,0 +1,1138 @@ +{ + "name": "agc-game", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agc-game", + "dependencies": { + "phaser": "4.2.1" + }, + "devDependencies": { + "vite": "^6.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/phaser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-4.2.1.tgz", + "integrity": "sha512-WUNwCPJpdjvZiuT6SgCfYVW8Qw/3j0jJ4ws7P2QkhFLFu74sbGuyHJcbFueGkY/AYO4Pi47bNQXn1OCJeLX//w==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package.json b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package.json new file mode 100644 index 000000000..6504e9aee --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/package.json @@ -0,0 +1,15 @@ +{ + "name": "agc-game", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "phaser": "4.2.1" + }, + "devDependencies": { + "vite": "^6.2.0" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/style.css b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/style.css new file mode 100644 index 000000000..8b3c907f4 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/style.css @@ -0,0 +1,2 @@ +body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; } +main { width: min(720px, calc(100vw - 32px)); } diff --git a/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/vite.config.js b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/vite.config.js new file mode 100644 index 000000000..d8e631e07 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/phaser-2d-starter/project/game/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: './', + build: { outDir: 'dist', emptyOutDir: true }, +}); diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/cover.svg b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/cover.svg new file mode 100644 index 000000000..f08503a3f --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/cover.svg @@ -0,0 +1,13 @@ + + + + + + + + + + Three.js 3D 起步工程 + AGC 起步工程模板 · 网页 / Three.js + Vite + 自带可旋转立方体场景与光照,适合作为三维玩法的起点 + 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 new file mode 100644 index 000000000..2b492a71d --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/meta.json @@ -0,0 +1,18 @@ +{ + "id": "threejs-3d-starter", + "title": "Three.js 3D 起步工程", + "summary": "网页三维起步工程(Three.js + Vite),自带可旋转立方体场景、方向光与自适应视口。", + "tags": [ + "起步工程", + "3d", + "three.js", + "网页" + ], + "runtime": "html", + "engine": "three.js", + "engineVersion": "0.180.0", + "templateVersion": "0.1.0", + "entry": "game/index.html", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/index.html b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/index.html new file mode 100644 index 000000000..d01aa860a --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/index.html @@ -0,0 +1,10 @@ + + + + + + Genarrative Game Draft + + +
+ diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js new file mode 100644 index 000000000..211f285ef --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/main.js @@ -0,0 +1,43 @@ +import * as THREE from 'three'; + +const container = document.querySelector('#game'); +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x0b1120); + +const camera = new THREE.PerspectiveCamera( + 60, + window.innerWidth / window.innerHeight, + 0.1, + 100, +); +camera.position.set(2.4, 1.8, 3.2); +camera.lookAt(0, 0, 0); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +container.append(renderer.domElement); + +const light = new THREE.DirectionalLight(0xffffff, 1.4); +light.position.set(3, 5, 4); +scene.add(light, new THREE.AmbientLight(0x8899ff, 0.6)); + +const cube = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + new THREE.MeshStandardMaterial({ color: 0xe0a060 }), +); +scene.add(cube); + +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); + +function tick() { + cube.rotation.y += 0.01; + cube.rotation.x += 0.004; + renderer.render(scene, camera); + requestAnimationFrame(tick); +} +tick(); diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/package.json b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/package.json new file mode 100644 index 000000000..496deca74 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/package.json @@ -0,0 +1,15 @@ +{ + "name": "agc-game", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "three": "^0.180.0" + }, + "devDependencies": { + "vite": "^6.2.0" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/style.css b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/style.css new file mode 100644 index 000000000..b25e3e5ae --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/style.css @@ -0,0 +1,3 @@ +body { margin: 0; overflow: hidden; background: #06080f; } +main { display: block; width: 100vw; height: 100vh; } +canvas { display: block; } diff --git a/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/vite.config.js b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/vite.config.js new file mode 100644 index 000000000..d8e631e07 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/threejs-3d-starter/project/game/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + base: './', + build: { outDir: 'dist', emptyOutDir: true }, +}); diff --git a/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md b/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md index c5559f198..97e12c415 100644 --- a/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md +++ b/docs/project-memory/plans/【实施计划】AGC模板库客户端接入-2026-09-17.md @@ -8,9 +8,10 @@ Milestone Spec: `docs/project-memory/plans/【里程碑】AGC模板库客户端 ## 步骤 1. **OSS 库布局与契约** - - 在 `agc-dev` 落地 `templates/` 前缀:`index.json`、`v1//{template.json,template.zip,cover.png}`。 + - 在 `agc-dev` 落地 `templates/` 前缀:`index.json`、`v1//{template.json,template.zip,cover.*}`。 - 清单补齐 `tags`、`coverKey/coverWidth/coverHeight/coverSha256`,正文改为 zip(zip 根 == 项目根)。 - - 交付:`scripts/agc-template-library-publish.mjs`(校验 + 上传 + 回读校验)、`templates/README.md`。 + - 模板源落在 `apps/ai-game-creator-shell/template-library/v1//{meta.json,project/**,cover.*}`;zip 由 `scripts/agc-template-library-publish.mjs` 现场打包(不落仓库)。 + - 交付:发布脚本(校验 + 打包 + 上传 + 回读校验,支持 `--dry-run` / `--prune`)、`templates/README.md`,以及 5 个模板(3 个空白 + 2 个起步工程)。 - 验收:匿名 `GET templates/index.json` 可读,每个 `zipKey` 回读 SHA-256 与清单一致。 2. **Rust 模板库模块** diff --git a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md index 00b071ab6..3956604f8 100644 --- a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md +++ b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md @@ -16,7 +16,7 @@ templates/ v1// template.json # 单模板元数据(含文件级摘要) template.zip # 模板正文,zip 根 == AGC 项目根(如 game/index.html) - cover.png # 封面图(卡片展示,客户端 直接取) + cover.(png|jpg|webp|svg) # 封面图(卡片展示,客户端 直接取) ``` `index.json`(schema `agc-template-library.v1`): @@ -39,7 +39,9 @@ templates/ - 所有对象键必须落在 `templates/` 前缀内;客户端只用「受信任 OSS 主机 + 对象键」自行拼 URL,**不直接信任清单里的地址**。 - 任何一项校验失败(schema、标识符、sha256、尺寸、键前缀)都让整次清单读取失败,前端拿到的是全有或全无的清单。 -- 上传与校验由仓库脚本 [`scripts/agc-template-library-publish.mjs`](../../scripts/agc-template-library-publish.mjs) 完成:源目录放 `v1//{meta.json,template.zip,cover.png,project/}`,脚本生成 `template.json` 与 `index.json`、上传并回读校验。 +- 模板源在仓库 `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] [--prune]`,脚本生成 `template.json` 与 `index.json`、上传后回读 zip 摘要;`--prune` 清理该模板前缀下本次没有产出的旧对象(例如换封面扩展名后的残留)。 +- 当前模板:`blank-web`(空白网页)、`blank-2d-canvas`(空白二维画布)、`blank-3d-scene`(空白三维场景)、`phaser-2d-starter`(Phaser 2D 起步工程)、`threejs-3d-starter`(Three.js 3D 起步工程)。 - 客户端可用 `AGC_TEMPLATE_LIBRARY_BASE_URL` 覆盖库地址;只接受 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com`(拒绝其他主机、路径、http)。 ## 客户端实现 diff --git a/scripts/agc-template-library-publish.mjs b/scripts/agc-template-library-publish.mjs index 405da60de..aa0281caa 100644 --- a/scripts/agc-template-library-publish.mjs +++ b/scripts/agc-template-library-publish.mjs @@ -1,36 +1,47 @@ #!/usr/bin/env node /** - * 发布 AGC 模板库到 OSS。 + * 打包并发布 AGC 模板库到 OSS。 * * 用法: - * node scripts/agc-template-library-publish.mjs --source [--dry-run] + * node scripts/agc-template-library-publish.mjs --source [--dry-run] [--prune] * [--bucket agc-dev] [--endpoint oss-rg-china-mainland.aliyuncs.com] [--prefix templates] + * [--index-out ] * - * 源目录结构(模板正文是 zip,zip 根 == AGC 项目根,例如 game/index.html): + * 源目录结构(仓库内为 `apps/ai-game-creator-shell/template-library/`): * /v1//meta.json 模板元数据(title/summary/tags/runtime/engine/…) - * /v1//template.zip 模板包 - * /v1//cover.png 封面图 + * /v1//cover.(png|jpg|jpeg|webp|svg) 封面图 + * /v1//project/** 模板正文(就是解压后的项目根内容) * - * 脚本会按包内容生成 `v1//template.json` 与库清单 `index.json`, - * 然后上传 zip、封面、模板元数据与清单。上传前会完成全部校验,任何一项不合法都不发请求。 + * 脚本按 `project/` 现场打包 `template.zip`(zip 根 == AGC 项目根),再上传 + * `v1//{template.zip,cover.*,template.json}` 与库清单 `index.json`; + * `--prune` 会删除该模板前缀下本次没有产出的旧对象(例如换了封面扩展名)。 + * 上传前完成全部校验,任何一项不合法都不发请求。 */ import { createHash, createHmac } from 'node:crypto'; import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; +import { deflateRawSync } from 'node:zlib'; const SCHEMA_VERSION = 'agc-template-library.v1'; const TEMPLATE_SCHEMA_VERSION = 'agc-template.v1'; 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']); -const COVER_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']); +const COVER_CONTENT_TYPES = new Map([ + ['.png', 'image/png'], + ['.jpg', 'image/jpeg'], + ['.jpeg', 'image/jpeg'], + ['.webp', 'image/webp'], + ['.svg', 'image/svg+xml'], +]); function usage() { console.log( [ - '用法: node scripts/agc-template-library-publish.mjs --source [--dry-run]', + '用法: node scripts/agc-template-library-publish.mjs --source [--dry-run] [--prune]', ' [--bucket agc-dev] [--endpoint oss-rg-china-mainland.aliyuncs.com] [--prefix templates]', + ' [--index-out ]', ].join('\n'), ); } @@ -43,7 +54,9 @@ function parseArgs(argv) { process.env.AGC_TEMPLATE_LIBRARY_ENDPOINT?.trim() || 'oss-rg-china-mainland.aliyuncs.com', prefix: 'templates', + indexOut: '', dryRun: false, + prune: false, }; for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; @@ -51,7 +64,9 @@ function parseArgs(argv) { else if (value === '--bucket') args.bucket = argv[(index += 1)] ?? ''; else if (value === '--endpoint') args.endpoint = argv[(index += 1)] ?? ''; else if (value === '--prefix') args.prefix = argv[(index += 1)] ?? ''; + else if (value === '--index-out') args.indexOut = argv[(index += 1)] ?? ''; else if (value === '--dry-run') args.dryRun = true; + else if (value === '--prune') args.prune = true; else if (value === '--help' || value === '-h') { usage(); process.exit(0); @@ -97,25 +112,102 @@ function loadAccessKeys() { const sha256 = (buffer) => createHash('sha256').update(buffer).digest('hex'); -function walkFiles(directory, base = directory) { - const files = []; - for (const entry of readdirSync(directory, { withFileTypes: true }).sort( - (left, right) => left.name.localeCompare(right.name), - )) { - const full = join(directory, entry.name); - if (entry.isDirectory()) files.push(...walkFiles(full, base)); - else { - const bytes = readFileSync(full); - files.push({ - path: full - .slice(base.length + 1) - .split('\\') - .join('/'), - sizeBytes: bytes.length, - sha256: sha256(bytes), - }); +const CRC32_TABLE = (() => { + const table = new Uint32Array(256); + for (let index = 0; index < 256; index += 1) { + let value = index; + for (let bit = 0; bit < 8; bit += 1) { + value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; } + table[index] = value >>> 0; } + return table; +})(); + +function crc32(buffer) { + let crc = 0xffffffff; + for (const byte of buffer) { + crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +/** + * 生成确定性的 ZIP:条目按路径排序、固定 DOS 时间戳、UTF-8 名称位标记。 + * 同一份 `project/` 内容重复打包得到相同摘要,便于比对发布结果。 + */ +function buildZip(entries) { + const chunks = []; + const central = []; + let offset = 0; + for (const entry of entries) { + const nameBytes = Buffer.from(entry.path, 'utf8'); + const content = entry.bytes; + const compressed = deflateRawSync(content, { level: 9 }); + const checksum = crc32(content); + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0x0800, 6); + local.writeUInt16LE(8, 8); + local.writeUInt16LE(0, 10); + local.writeUInt16LE(0x0021, 12); + local.writeUInt32LE(checksum, 14); + local.writeUInt32LE(compressed.length, 18); + local.writeUInt32LE(content.length, 22); + local.writeUInt16LE(nameBytes.length, 26); + local.writeUInt16LE(0, 28); + chunks.push(local, nameBytes, compressed); + const directory = Buffer.alloc(46); + directory.writeUInt32LE(0x02014b50, 0); + directory.writeUInt16LE(20, 4); + directory.writeUInt16LE(20, 6); + directory.writeUInt16LE(0x0800, 8); + directory.writeUInt16LE(8, 10); + directory.writeUInt16LE(0, 12); + directory.writeUInt16LE(0x0021, 14); + directory.writeUInt32LE(checksum, 16); + directory.writeUInt32LE(compressed.length, 20); + directory.writeUInt32LE(content.length, 24); + directory.writeUInt16LE(nameBytes.length, 28); + directory.writeUInt16LE(0, 30); + directory.writeUInt16LE(0, 32); + directory.writeUInt16LE(0, 34); + directory.writeUInt16LE(0, 36); + directory.writeUInt32LE(0, 38); + directory.writeUInt32LE(offset, 42); + central.push(directory, nameBytes); + offset += local.length + nameBytes.length + compressed.length; + } + const centralBytes = Buffer.concat(central); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50, 0); + end.writeUInt16LE(0, 4); + end.writeUInt16LE(0, 6); + end.writeUInt16LE(entries.length, 8); + end.writeUInt16LE(entries.length, 10); + end.writeUInt32LE(centralBytes.length, 12); + end.writeUInt32LE(offset, 16); + end.writeUInt16LE(0, 20); + return Buffer.concat([...chunks, centralBytes, end]); +} + +function readProjectFiles(projectRoot) { + const files = []; + const walk = (directory, prefix) => { + for (const entry of readdirSync(directory, { withFileTypes: true }).sort( + (left, right) => left.name.localeCompare(right.name), + )) { + const relative = prefix ? `${prefix}/${entry.name}` : entry.name; + const full = join(directory, entry.name); + if (entry.isDirectory()) walk(full, relative); + else if (entry.isFile()) + files.push({ path: relative, bytes: readFileSync(full) }); + else throw new Error(`模板正文包含不支持的条目:${full}`); + } + }; + walk(projectRoot, ''); + if (files.length === 0) throw new Error(`模板正文为空:${projectRoot}`); return files; } @@ -123,10 +215,12 @@ function readMeta(templateRoot, templateId) { const metaPath = join(templateRoot, 'meta.json'); if (!existsSync(metaPath)) throw new Error(`${templateId} 缺少 meta.json`); const meta = JSON.parse(readFileSync(metaPath, 'utf8')); - if (!TEMPLATE_ID_PATTERN.test(meta.id ?? '')) + if (!TEMPLATE_ID_PATTERN.test(meta.id ?? '')) { throw new Error(`${templateId} 的 meta.id 非法`); - if (meta.id !== templateId) + } + if (meta.id !== templateId) { throw new Error(`${templateId} 目录名与 meta.id 不一致:${meta.id}`); + } if (typeof meta.title !== 'string' || !meta.title.trim()) { throw new Error(`${templateId} 缺少 title`); } @@ -140,12 +234,14 @@ function readMeta(templateRoot, templateId) { } if ( !Array.isArray(meta.tags) || + meta.tags.length === 0 || meta.tags.some((tag) => typeof tag !== 'string' || !tag.trim()) ) { throw new Error(`${templateId} 的 tags 必须是非空字符串数组`); } if ( typeof meta.entry !== 'string' || + !meta.entry.trim() || meta.entry.includes('..') || meta.entry.startsWith('/') ) { @@ -167,25 +263,23 @@ function buildLibrary(source, prefix) { const updatedAt = new Date().toISOString().replace(/\.\d{3}Z$/u, 'Z'); const templates = []; const objects = []; + const managedPrefixes = []; for (const templateId of templateIds) { const templateRoot = join(versionRoot, templateId); const meta = readMeta(templateRoot, templateId); - const zipPath = join(templateRoot, 'template.zip'); - if (!existsSync(zipPath)) - throw new Error(`${templateId} 缺少 template.zip`); - const zipBytes = readFileSync(zipPath); - const coverEntries = readdirSync(templateRoot).filter((name) => - COVER_EXTENSIONS.has(name.slice(name.lastIndexOf('.')).toLowerCase()), + const projectFiles = readProjectFiles(join(templateRoot, 'project')); + const zipBytes = buildZip(projectFiles); + const coverNames = readdirSync(templateRoot).filter((name) => + COVER_CONTENT_TYPES.has(name.slice(name.lastIndexOf('.')).toLowerCase()), ); - if (coverEntries.length !== 1) { - throw new Error(`${templateId} 必须且只能有一张封面图(png/jpg/webp)`); + if (coverNames.length !== 1) { + throw new Error( + `${templateId} 必须且只能有一张封面图(png/jpg/webp/svg)`, + ); } - const coverName = coverEntries[0]; + const coverName = coverNames[0]; const coverBytes = readFileSync(join(templateRoot, coverName)); - const projectFiles = walkFiles(join(templateRoot, 'project')); - if (projectFiles.length === 0) - throw new Error(`${templateId} 的 project/ 为空`); const zipKey = `${prefix}/v1/${templateId}/template.zip`; const coverKey = `${prefix}/v1/${templateId}/${coverName}`; @@ -214,7 +308,11 @@ function buildLibrary(source, prefix) { height: Number.isInteger(meta.coverHeight) ? meta.coverHeight : 0, sha256: sha256(coverBytes), }, - files: projectFiles, + files: projectFiles.map((file) => ({ + path: file.path, + sizeBytes: file.bytes.length, + sha256: sha256(file.bytes), + })), }; templates.push({ @@ -238,16 +336,15 @@ function buildLibrary(source, prefix) { metadataKey, }); + managedPrefixes.push(`${prefix}/v1/${templateId}/`); objects.push( { key: zipKey, body: zipBytes, contentType: 'application/zip' }, { key: coverKey, body: coverBytes, - contentType: coverName.endsWith('.webp') - ? 'image/webp' - : coverName.endsWith('.png') - ? 'image/png' - : 'image/jpeg', + contentType: COVER_CONTENT_TYPES.get( + coverName.slice(coverName.lastIndexOf('.')).toLowerCase(), + ), }, { key: metadataKey, @@ -272,37 +369,72 @@ function buildLibrary(source, prefix) { body: Buffer.from(`${JSON.stringify(indexJson, null, 2)}\n`, 'utf8'), contentType: 'application/json', }); - writeFileSync( - join(source, 'index.json'), - `${JSON.stringify(indexJson, null, 2)}\n`, - 'utf8', - ); - return { objects, indexJson }; + return { objects, indexJson, managedPrefixes }; } function createClient({ bucket, endpoint, accessKeyId, accessKeySecret }) { - async function call( - method, - key, - { body = Buffer.alloc(0), contentType = '' } = {}, - ) { + function authorize(method, resourcePath, contentType) { const date = new Date().toUTCString(); - const stringToSign = `${method}\n\n${contentType}\n${date}\n/${bucket}/${key}`; + const stringToSign = `${method}\n\n${contentType}\n${date}\n${resourcePath}`; const signature = createHmac('sha1', accessKeySecret) .update(stringToSign, 'utf8') .digest('base64'); - const response = await fetch(`https://${bucket}.${endpoint}/${key}`, { - method, + return { date, authorization: `OSS ${accessKeyId}:${signature}` }; + } + + async function put(key, body, contentType) { + const { date, authorization } = authorize( + 'PUT', + `/${bucket}/${key}`, + contentType, + ); + return fetch(`https://${bucket}.${endpoint}/${key}`, { + method: 'PUT', headers: { Date: date, - Authorization: `OSS ${accessKeyId}:${signature}`, + Authorization: authorization, ...(contentType ? { 'Content-Type': contentType } : {}), }, - ...(method === 'PUT' ? { body } : {}), + body, }); - return response; } - return { call }; + + async function get(key) { + const { date, authorization } = authorize('GET', `/${bucket}/${key}`, ''); + return fetch(`https://${bucket}.${endpoint}/${key}`, { + headers: { Date: date, Authorization: authorization }, + }); + } + + async function remove(key) { + const { date, authorization } = authorize( + 'DELETE', + `/${bucket}/${key}`, + '', + ); + return fetch(`https://${bucket}.${endpoint}/${key}`, { + method: 'DELETE', + headers: { Date: date, Authorization: authorization }, + }); + } + + async function listKeys(prefix) { + const { date, authorization } = authorize('GET', `/${bucket}/`, ''); + const response = await fetch( + `https://${bucket}.${endpoint}/?prefix=${encodeURIComponent(prefix)}&max-keys=1000`, + { headers: { Date: date, Authorization: authorization } }, + ); + const body = await response.text(); + if (!response.ok) + throw new Error( + `列举对象失败:HTTP ${response.status} ${body.slice(0, 200)}`, + ); + return [...body.matchAll(/([\s\S]*?)<\/Key>/gu)].map( + (match) => match[1], + ); + } + + return { put, get, remove, listKeys }; } async function main() { @@ -312,7 +444,17 @@ async function main() { throw new Error('必须提供 --source'); } const source = resolve(args.source); - const { objects, indexJson } = buildLibrary(source, args.prefix); + const { objects, indexJson, managedPrefixes } = buildLibrary( + source, + args.prefix, + ); + if (args.indexOut) { + writeFileSync( + resolve(args.indexOut), + `${JSON.stringify(indexJson, null, 2)}\n`, + 'utf8', + ); + } console.log( `模板库:${indexJson.templates.length} 个模板 -> oss://${args.bucket}/${args.prefix}/`, ); @@ -327,10 +469,15 @@ async function main() { console.log(` PUT ${object.key} (${object.body.length} B)`); return; } + const credentials = loadAccessKeys(); const client = createClient({ ...args, ...credentials }); for (const object of objects) { - const response = await client.call('PUT', object.key, object); + const response = await client.put( + object.key, + object.body, + object.contentType, + ); if (!response.ok) { throw new Error( `上传失败 ${object.key}:HTTP ${response.status} ${await response.text()}`, @@ -340,10 +487,23 @@ async function main() { `PUT ${object.key} (${object.body.length} B) -> ${response.status}`, ); } - const verify = await client.call('GET', `${args.prefix}/index.json`); + + if (args.prune) { + const uploaded = new Set(objects.map((object) => object.key)); + for (const prefix of managedPrefixes) { + for (const key of await client.listKeys(prefix)) { + if (uploaded.has(key) || key === prefix) continue; + const response = await client.remove(key); + console.log(`DELETE ${key} -> ${response.status}`); + } + } + } + + const verify = await client.get(`${args.prefix}/index.json`); + if (!verify.ok) throw new Error(`回读清单失败:HTTP ${verify.status}`); const liveIndex = JSON.parse(await verify.text()); for (const template of liveIndex.templates) { - const zipResponse = await client.call('GET', template.zipKey); + const zipResponse = await client.get(template.zipKey); const zipBytes = Buffer.from(await zipResponse.arrayBuffer()); const digest = sha256(zipBytes); if (digest !== template.zipSha256) {