补齐模板库模板源、空白模板与确定性打包
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m45s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m42s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m58s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m0s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m24s
Project CI / Repository checks (pull_request) Failing after 22s
Project CI / Frontend tests (pull_request) Failing after 4m47s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m28s
Project CI / Native shell tests (pull_request) Successful in 8m51s

- 新增仓库模板源 apps/ai-game-creator-shell/template-library/:空白网页、空白二维画布、空白三维场景三个空白模板,以及 Phaser 2D、Three.js 3D 两个起步工程模板
- 发布脚本改为按 project/ 现场打包确定性 zip(条目排序 + 固定时间戳),支持 svg 封面、--prune 清理旧对象与 --index-out
- 模板库新增真连检查:读取线上清单、下载并安装线上模板包后校验文件落盘
- 刷新线上清单 fixture,并同步技术方案与实施计划的封面格式、模板源与发布方式
This commit is contained in:
kdletters
2026-09-17 11:24:50 +08:00
parent 8ee50e8b94
commit 7eaaa1a499
41 changed files with 1962 additions and 86 deletions
@@ -874,9 +874,18 @@ mod tests {
.iter()
.map(|entry| entry.id.as_str())
.collect::<Vec<_>>(),
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());
}
}
@@ -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"
}
]
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-label="空白二维画布工程">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1" gradientTransform="rotate(75 0.5 0.5)">
<stop offset="0" stop-color="#0d1a16" />
<stop offset="1" stop-color="#2f7f63" />
</linearGradient>
</defs>
<rect width="960" height="540" fill="url(#bg)" />
<rect x="72" y="132" width="6" height="196" rx="3" fill="#ffffff" fill-opacity="0.35" />
<text x="104" y="200" fill="#ffffff" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="46" font-weight="700">空白二维画布工程</text>
<text x="108" y="252" fill="#e2ecff" fill-opacity="0.86" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="20">AGC 空白模板 · Canvas 2D 主循环</text>
<text x="108" y="302" fill="#d6e2f5" fill-opacity="0.68" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="15">自适应画布与渲染循环已接好,直接画自己的东西</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -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
}
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Genarrative Game Draft</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body><main id="game"><canvas id="stage"></canvas></main><script type="module" src="./main.js"></script></body>
</html>
@@ -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);
@@ -0,0 +1,12 @@
{
"name": "agc-game",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"vite": "^6.2.0"
}
}
@@ -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%; }
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
export default defineConfig({
root: '.',
base: './',
build: { outDir: 'dist', emptyOutDir: true },
});
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-label="空白三维场景工程">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1" gradientTransform="rotate(40 0.5 0.5)">
<stop offset="0" stop-color="#0a0f1e" />
<stop offset="1" stop-color="#1f3b8c" />
</linearGradient>
</defs>
<rect width="960" height="540" fill="url(#bg)" />
<rect x="72" y="132" width="6" height="196" rx="3" fill="#ffffff" fill-opacity="0.35" />
<text x="104" y="200" fill="#ffffff" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="46" font-weight="700">空白三维场景工程</text>
<text x="108" y="252" fill="#e2ecff" fill-opacity="0.86" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="20">AGC 空白模板 · Three.js 空场景</text>
<text x="108" y="302" fill="#d6e2f5" fill-opacity="0.68" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="15">空场景 + 相机 + 网格地面,直接摆自己的三维内容</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -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
}
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Genarrative Game Draft</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body><main id="game"></main><script type="module" src="./main.js"></script></body>
</html>
@@ -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();
@@ -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"
}
}
@@ -0,0 +1,3 @@
body { margin: 0; overflow: hidden; background: #05070d; }
main { display: block; width: 100vw; height: 100vh; }
canvas { display: block; }
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
export default defineConfig({
root: '.',
base: './',
build: { outDir: 'dist', emptyOutDir: true },
});
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-label="空白网页工程">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1" gradientTransform="rotate(20 0.5 0.5)">
<stop offset="0" stop-color="#12161f" />
<stop offset="1" stop-color="#4a5568" />
</linearGradient>
</defs>
<rect width="960" height="540" fill="url(#bg)" />
<rect x="72" y="132" width="6" height="196" rx="3" fill="#ffffff" fill-opacity="0.35" />
<text x="104" y="200" fill="#ffffff" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="46" font-weight="700">空白网页工程</text>
<text x="108" y="252" fill="#e2ecff" fill-opacity="0.86" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="20">AGC 空白模板 · HTML / CSS / 原生 JS</text>
<text x="108" y="302" fill="#d6e2f5" fill-opacity="0.68" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="15">零依赖最小工程,从 main.js 开始写自己的玩法</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -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
}
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Genarrative Game Draft</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body><main id="game"></main><script type="module" src="./main.js"></script></body>
</html>
@@ -0,0 +1,2 @@
const root = document.querySelector('#game');
root.textContent = '';
@@ -0,0 +1,12 @@
{
"name": "agc-game",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"vite": "^6.2.0"
}
}
@@ -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; }
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
export default defineConfig({
root: '.',
base: './',
build: { outDir: 'dist', emptyOutDir: true },
});
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-label="Phaser 2D 起步工程">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1" gradientTransform="rotate(30 0.5 0.5)">
<stop offset="0" stop-color="#101827" />
<stop offset="1" stop-color="#3a60b2" />
</linearGradient>
</defs>
<rect width="960" height="540" fill="url(#bg)" />
<rect x="72" y="132" width="6" height="196" rx="3" fill="#ffffff" fill-opacity="0.35" />
<text x="104" y="200" fill="#ffffff" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="46" font-weight="700">Phaser 2D 起步工程</text>
<text x="108" y="252" fill="#e2ecff" fill-opacity="0.86" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="20">AGC 起步工程模板 · 网页 / Phaser + Vite</text>
<text x="108" y="302" fill="#d6e2f5" fill-opacity="0.68" font-family="Microsoft YaHei, Noto Sans SC, sans-serif" font-size="15">新建项目即得可运行脚手架,可直接进入对话开始创作</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -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
}

Some files were not shown because too many files have changed in this diff Show More