继承自 v1 规划,基于最新讨论更新:Creative 命名、zip 素材包、模板下沉、manifest 扩展性设计。
| 维度 | v1 | v2 |
|---|---|---|
| 业务概念 | Project(项目) | Creative(创意)——广告行业标准术语 |
| 素材上传 | 单文件逐个上传 | zip 包上传(config.json + page.png + map.png ± special.jpeg) |
| 模板代码位置 | 留在项目根 src/ |
下沉到 templates/coloring/ |
| CSS 参数化 | 全面 var() 化 |
非重点,聚焦主流程 |
| storeUrls | 外部注入 | 不参数化,保持 adapter 内硬编码 |
| 模板元信息 | 无 | manifest.json,驱动前后端 UI + 校验 |
| optional 素材 | 未明确 | 构建时检查文件存在性,动态生成 import |
浏览器 (React SPA)
│
▼
Express API (:3001)
│ ┌── 静态文件服务 (React build)
│ ├── REST API (/api/v1/*)
│ └── Build Service (调用 Vite 程序化构建)
│
├── SQLite (creatives, builds, templates 元数据)
│
├── storage/creatives/<id>/
│ ├── assets/ # 用户上传的 zip 解压产物
│ └── builds/<uuid>/# 构建产物
│
└── templates/coloring/ # 模板 Vite 项目
│
▼
npx vite build --mode <platform>
playableads-platform/
│
├── templates/ # 模板根目录(扩展点)
│ └── coloring/ # 填色玩法模板
│ ├── manifest.json # 模板元信息 → 驱动前后端
│ ├── index.html # 模板入口 HTML
│ ├── vite.config.js # 构建配置(含 #ad-config alias)
│ ├── tsconfig.json
│ ├── package.json # 模板依赖(js-confetti, vite-plugin-singlefile…)
│ ├── src/
│ │ ├── base/ # 自研 WebGL 2 引擎(不变)
│ │ │ ├── Scene.ts
│ │ │ ├── Gesture.ts
│ │ │ ├── Animator.ts
│ │ │ ├── m4.ts
│ │ │ ├── BgLayer.ts / BoxLayer.ts / BorderLayer.ts / FrameLayer.ts
│ │ │ ├── ImageLayer.ts / TextureLayer.ts
│ │ │ ├── ImageShaders.ts
│ │ │ ├── Triangle.ts / 2d.ts / utils.ts
│ │ │ └── glsl/ # GLSL shader 文件
│ │ ├── filler/ # 填色玩法业务逻辑
│ │ │ ├── index.ts # ★ 重构:从 #ad-config 导入素材+主题
│ │ │ ├── ad-config.ts # NEW:默认配置(dev / CLI 构建用,提交到 repo)
│ │ │ ├── _ad_config_.ts # .gitignore,平台构建时动态生成
│ │ │ ├── FillerData.ts # 状态核心
│ │ │ ├── FillerScene.ts # WebGL 场景
│ │ │ ├── WorkLayer.ts
│ │ │ ├── AnimatableMask.ts
│ │ │ ├── LineArtLayer.ts
│ │ │ ├── NumberLayer.ts
│ │ │ ├── HintLayer.ts
│ │ │ ├── Mask.ts
│ │ │ ├── Audio.ts
│ │ │ ├── cta.ts
│ │ │ ├── FingerHint.ts
│ │ │ ├── explosion.ts
│ │ │ ├── common.ts
│ │ │ ├── createColored.ts
│ │ │ ├── LoadingController.ts
│ │ │ ├── play.ts # share.html 回放逻辑
│ │ │ ├── mraid.ts
│ │ │ └── ad-platform/
│ │ │ ├── types.ts # AdPlatformAdapter 接口
│ │ │ ├── current.ts # 构建时 Vite alias 替换
│ │ │ └── adapters/
│ │ │ ├── google.ts
│ │ │ ├── applovin.ts
│ │ │ ├── unity.ts
│ │ │ ├── playturbo.ts
│ │ │ ├── helpers.ts
│ │ │ └── storeUrls.ts
│ │ └── utils/
│ │ └── random.ts
│ ├── assets/
│ │ ├── res/ # 默认填色素材(dev 用)
│ │ │ └── 6a18f7d9957ac783bad75479/
│ │ │ ├── config.json
│ │ │ ├── page.png
│ │ │ ├── map.png
│ │ │ └── special.jpeg
│ │ ├── user/ # .gitignore,平台构建时 symlink → storage
│ │ ├── css/
│ │ │ ├── tools.css # 主样式
│ │ │ └── loading.css # 加载动画
│ │ ├── img/
│ │ │ ├── logo.png / logo-txt.png
│ │ │ ├── coloring-pages.png / slogon.png
│ │ │ ├── finger.png / finger2.png
│ │ │ └── icon/
│ │ ├── fonts/
│ │ │ └── numbers_roboto_500.png
│ │ └── sound/
│ │ ├── color_done_02.mp3
│ │ ├── section_done.mp3
│ │ └── sound_hint.mp3
│ ├── typings/ # TypeScript 声明
│ └── dist/ # .gitignore
│
├── platform/ # 二期平台代码
│ ├── server/
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── src/
│ │ ├── index.ts # Express 入口
│ │ ├── routes/
│ │ │ ├── templates.ts # GET /api/v1/templates
│ │ │ ├── creatives.ts # CRUD /api/v1/creatives
│ │ │ ├── assets.ts # POST/DELETE /api/v1/creatives/:id/assets
│ │ │ └── builds.ts # POST/GET /api/v1/creatives/:id/builds
│ │ ├── services/
│ │ │ ├── storageService.ts # 素材存取、zip 解压、文件扫描
│ │ │ ├── buildService.ts # 构建编排(队列 + mutex)
│ │ │ └── configGenerator.ts # 生成 _ad_config_.ts + symlink
│ │ ├── db/
│ │ │ ├── database.ts # SQLite 初始化 + 迁移
│ │ │ └── seed.ts # 种子数据(coloring 模板注册)
│ │ └── middleware/
│ │ └── errorHandler.ts
│ │
│ └── client/
│ ├── package.json
│ ├── vite.config.ts
│ ├── index.html
│ └── src/
│ ├── main.tsx # React 入口
│ ├── App.tsx # 路由
│ ├── api/
│ │ └── client.ts # API 请求封装
│ ├── pages/
│ │ ├── Dashboard.tsx # 创意列表 + 新建入口
│ │ ├── NewCreative.tsx # 选模板 → 填名称
│ │ └── CreativeDetail.tsx # 核心工作页(上传+配置+构建)
│ ├── components/
│ │ ├── Layout.tsx
│ │ ├── AssetUploader.tsx # zip 上传 + 已上传文件列表
│ │ ├── ThemeEditor.tsx # 动态表单(由 manifest.theme 驱动)
│ │ ├── PlatformSelector.tsx# 平台勾选
│ │ ├── BuildPanel.tsx # 构建按钮 + spinner + 产物下载
│ │ ├── BuildHistory.tsx # 历史构建记录
│ │ └── PreviewModal.tsx # HTML 预览 iframe
│ └── types/
│ └── index.ts # Template, Creative, Build 类型
│
├── storage/ # gitignored
│ ├── data.db # SQLite 数据库文件
│ └── creatives/<uuid>/
│ ├── assets/ # 用户上传 zip 解压产物
│ │ ├── config.json
│ │ ├── page.png
│ │ ├── map.png
│ │ └── special.jpeg # 可选
│ └── builds/<build_uuid>/
│ ├── google/
│ │ └── index.html
│ ├── applovin/
│ │ └── index.html
│ ├── unity/
│ │ └── index.html
│ ├── playturbo/
│ │ └── index.html
│ ├── mintegral/
│ │ └── index.html
│ └── all.zip # 全部产物打包
│
├── docs/
│ └── GIT_GUIDELINES.md
│
├── share.html # 分享落地页(现有,保留)
├── agent.md # Agent 工作文档
├── README.md # 项目说明
├── package.json # Root workspace orchestration
├── .gitignore
└── .claude/
├── settings.local.json
└── plans/
# 平台构建时动态生成
templates/*/src/filler/_ad_config_.ts
templates/*/assets/user/
# 运行时数据
storage/
platform/server/dist/
platform/client/dist/
dist/
node_modules/
每个模板目录根部的 manifest.json 是平台了解模板的唯一元数据来源。新增模板 = 新增目录 + 写一个 manifest,无需改平台代码。
templates/coloring/manifest.json:
{
"id": "coloring",
"name": "填色互动广告",
"description": "WebGL 填色核心玩法,点击区域上色,完成后展示宣传界面",
"version": "1.0.0",
"assets": {
"uploadFormat": "zip",
"required": [
{ "key": "config", "file": "config.json", "label": "区域配置文件", "accept": ".json" },
{ "key": "page", "file": "page.png", "label": "线稿图", "accept": ".png,.jpg,.jpeg" },
{ "key": "map", "file": "map.png", "label": "映射图", "accept": ".png" }
],
"optional": [
{ "key": "special", "file": "special.jpeg", "label": "着色参考图(推荐)", "accept": ".jpeg,.jpg,.png" }
]
},
"theme": {
"properties": [
{ "key": "bgGradient", "label": "背景渐变", "type": "css-gradient", "default": "linear-gradient(160deg, #fff9f2, #fed)" },
{ "key": "ctaGradient", "label": "CTA 按钮渐变", "type": "css-gradient", "default": "linear-gradient(135deg, #ff6b6b, #ee5a24)" },
{ "key": "ctaText", "label": "CTA 按钮文案", "type": "text", "default": "PLAY NOW", "maxLength": 30 },
{ "key": "progressColor", "label": "进度条颜色", "type": "color", "default": "#07ce07" }
]
},
"platforms": {
"available": ["google", "applovin", "unity", "playturbo", "mintegral"],
"defaults": ["google", "applovin"]
},
"build": {
"command": "npx vite build",
"cwd": "templates/coloring",
"outputPatterns": [
{ "platform": "google", "path": "dist/google/index.html" },
{ "platform": "applovin", "path": "dist/applovin/index.html" },
{ "platform": "unity", "path": "dist/unity/index.html" },
{ "platform": "playturbo", "path": "dist/playturbo/index.html" },
{ "platform": "mintegral", "path": "dist/mintegral/index.html" }
],
"adConfigAlias": "#ad-config",
"adConfigPath": "src/filler/_ad_config_.ts",
"assetsSymlink": {
"source": "assets/user",
"target": "../../storage/creatives/{creativeId}/assets"
}
}
}
| 字段 | 用途 |
|---|---|
id |
模板唯一标识,用于路由和数据库关联 |
assets.uploadFormat |
"zip" 表示素材通过 zip 上传;未来可扩展 "files" 多文件 |
assets.required[*] |
必填素材清单——前端据此渲染上传区,后端据此校验 |
assets.optional[*] |
选填素材——前端标注"(选填)",后端检查存在性来生成条件 import |
theme.properties[*] |
主题参数——前端据此动态渲染表单控件 |
platforms.available |
此模板支持的目标平台 |
platforms.defaults |
默认勾选的平台 |
build.outputPatterns |
告诉平台构建产物在哪里,复制到哪里 |
模板 index.ts 的唯一数据入口:
// 类型定义(模板内)
interface AdAssets {
configRaw: string; // config.json 原始文本
pageUrl: string; // 线稿图 URL
mapUrl: string; // 映射图 URL
specialUrl?: string; // 着色参考图(可选)
numberFontUrl: string; // 数字字体图
fingerUrl: string; // 手指引导图
logoUrl: string; // Logo 图标
logoTxtUrl: string; // Logo 文字
coloringPagesUrl: string; // 宣传图1
slogonUrl: string; // 宣传图2
}
interface AdTheme {
bgGradient: string;
ctaGradient: string;
ctaText: string;
progressColor: string;
}
// templates/coloring/src/filler/ad-config.ts
import configRaw from "/assets/res/6a18f7d9957ac783bad75479/config.json?raw";
import pageUrl from "/assets/res/6a18f7d9957ac783bad75479/page.png?url";
import mapUrl from "/assets/res/6a18f7d9957ac783bad75479/map.png?url";
import specialUrl from "/assets/res/6a18f7d9957ac783bad75479/special.jpeg?url";
import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";
import fingerUrl from "/assets/img/finger.png?url";
import logoUrl from "/assets/img/logo.png?url";
import logoTxtUrl from "/assets/img/logo-txt.png?url";
import coloringPagesUrl from "/assets/img/coloring-pages.png?url";
import slogonUrl from "/assets/img/slogon.png?url";
export const adAssets: AdAssets = {
configRaw, pageUrl, mapUrl, specialUrl,
numberFontUrl, fingerUrl, logoUrl, logoTxtUrl,
coloringPagesUrl, slogonUrl,
};
export const adTheme: AdTheme = {
bgGradient: "linear-gradient(160deg, #fff9f2, #fed)",
ctaGradient: "linear-gradient(135deg, #ff6b6b, #ee5a24)",
ctaText: "PLAY NOW",
progressColor: "#07ce07",
};
平台 configGenerator.ts 根据"用户上传了哪些文件" + "用户选择的主题参数"来生成:
// ↓ 以下为 configGenerator.ts 拼接生成 ↓
// ==== 用户素材(从 storage/creatives/<id>/assets/ → symlink → assets/user/) ====
import configRaw from "/assets/user/config.json?raw";
import pageUrl from "/assets/user/page.png?url";
import mapUrl from "/assets/user/map.png?url";
// 仅当 special.jpeg 存在于用户上传包中时,才生成此行:
import specialUrl from "/assets/user/special.jpeg?url";
// ==== 模板自有素材 ====
import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";
import fingerUrl from "/assets/img/finger.png?url";
import logoUrl from "/assets/img/logo.png?url";
import logoTxtUrl from "/assets/img/logo-txt.png?url";
import coloringPagesUrl from "/assets/img/coloring-pages.png?url";
import slogonUrl from "/assets/img/slogon.png?url";
export const adAssets = {
configRaw, pageUrl, mapUrl,
// 仅当 special.jpeg 存在时才有此字段:
specialUrl,
numberFontUrl, fingerUrl, logoUrl, logoTxtUrl,
coloringPagesUrl, slogonUrl,
};
export const adTheme = {
bgGradient: "linear-gradient(160deg, #fff9f2, #fed)",
ctaGradient: "linear-gradient(135deg, #ff6b6b, #ee5a24)",
ctaText: "PLAY NOW",
progressColor: "#07ce07",
};
当前 loadResource() (index.ts:251-275):11 行分散 import,hardcode 素材路径。
重构后:
// templates/coloring/src/filler/index.ts
import { adAssets, adTheme } from "#ad-config";
async function loadResource(): Promise<FillerResource> {
const config = JSON.parse(adAssets.configRaw) as AreaGroups;
const [page, map, numberImage] = await Promise.all([
loadImage(adAssets.pageUrl),
loadImage(adAssets.mapUrl),
loadImage(adAssets.numberFontUrl),
]);
// optional: 仅当用户上传了 special 时才尝试加载
let special: HTMLImageElement | undefined;
if ("specialUrl" in adAssets && adAssets.specialUrl) {
try {
special = await loadImage(adAssets.specialUrl);
} catch { /* fallback to createColored */ }
}
return new FillerResource(config, page, map, numberImage, [], special);
}
// templates/coloring/vite.config.js
const { defineConfig } = require("vite");
const { viteSingleFile } = require("vite-plugin-singlefile");
const path = require("path");
module.exports = defineConfig(({ mode }) => {
const platformBuild = platformBuilds[mode];
const adapter = platformBuild?.adapter || "google";
const output = platformBuild?.output;
const outDir = output ? `dist/${output}` : "dist";
// ★ 新增:#ad-config alias,通过环境变量切换
const adConfigPath = process.env.AD_CONFIG_PATH
? path.resolve(__dirname, process.env.AD_CONFIG_PATH)
: path.resolve(__dirname, "src/filler/ad-config.ts");
return {
plugins: [viteSingleFile(), finalizeHtmlPlugin(outDir, adapter)],
resolve: {
alias: {
"#ad-config": adConfigPath, // ★ 新增
"./ad-platform/current": path.resolve(
__dirname,
`src/filler/ad-platform/adapters/${adapter}.ts`,
),
},
},
build: {
assetsInlineLimit: 100 * 1024 * 1024,
target: "es2017",
outDir,
emptyOutDir: true,
rollupOptions: {
input: { main: "./index.html" },
output: {
entryFileNames: "assets/[name].js",
chunkFileNames: "assets/[name].js",
assetFileNames: "assets/[name][extname]",
},
},
},
};
});
改动极小:只加了一个 #ad-config alias,其他不变。
SQLite,五张表。
templates 1──N creatives 1──N builds
│ │
└── manifest (JSON) └── results (JSON)
creatives.theme (JSON)
creatives 1──N creative_assets (上传文件清单)
-- 模板注册表
CREATE TABLE templates (
id TEXT PRIMARY KEY, -- 'coloring'
name TEXT NOT NULL,
manifest TEXT NOT NULL, -- manifest.json 全文
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 创意表
CREATE TABLE creatives (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL, -- 运营人员命名的名称
template_id TEXT NOT NULL REFERENCES templates(id),
theme TEXT NOT NULL DEFAULT '{}',-- JSON: 用户配置的主题参数
status TEXT NOT NULL DEFAULT 'draft', -- draft | assets_ready | building | built | failed
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 素材文件记录(从 zip 解压出来的文件清单)
CREATE TABLE creative_assets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creative_id TEXT NOT NULL REFERENCES creatives(id),
file_key TEXT NOT NULL, -- manifest 中定义的 key: 'config' | 'page' | 'map' | 'special'
file_name TEXT NOT NULL, -- 原始文件名
file_path TEXT NOT NULL, -- 相对 storage 的路径
file_size INTEGER, -- 字节
is_required INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 构建记录表
CREATE TABLE builds (
id TEXT PRIMARY KEY, -- UUID
creative_id TEXT NOT NULL REFERENCES creatives(id),
status TEXT NOT NULL DEFAULT 'pending', -- pending | building | completed | failed
platforms TEXT NOT NULL, -- JSON: ["google","applovin"]
theme_snapshot TEXT NOT NULL, -- JSON: 构建时的 theme 快照
results TEXT, -- JSON: [{platform, filePath, fileSize}]
error_log TEXT,
started_at TEXT,
finished_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 索引
CREATE INDEX idx_creatives_template ON creatives(template_id);
CREATE INDEX idx_builds_creative ON builds(creative_id);
CREATE INDEX idx_creative_assets_creative ON creative_assets(creative_id);
creatives:
draft ──→ assets_ready ──→ building ──→ built
│ │
└──→ (re-upload) └──→ failed (可重试)
builds:
pending ──→ building ──→ completed
│
└──→ failed
Base: /api/v1
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/templates |
模板列表(含 manifest 中的 assets/theme/platforms 摘要) |
GET |
/templates/:id |
单个模板详情(含完整 manifest) |
GET /templates 响应:
{
"data": [{
"id": "coloring",
"name": "填色互动广告",
"description": "WebGL 填色核心玩法",
"version": "1.0.0",
"platforms": ["google","applovin","unity","playturbo","mintegral"],
"assetCount": { "required": 3, "optional": 1 }
}]
}
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/creatives |
创意列表(支持 ?status= 筛选,?page=&limit= 分页) |
POST |
/creatives |
新建创意 |
GET |
/creatives/:id |
创意详情(含 assets 列表、builds 摘要) |
PATCH |
/creatives/:id |
更新创意名称或 theme |
DELETE |
/creatives/:id |
删除创意及其所有 assets 和 builds |
POST /creatives 请求:
{
"name": "萌宠填色-谷歌渠道",
"templateId": "coloring"
}
GET /creatives/:id 响应:
{
"data": {
"id": "uuid-1",
"name": "萌宠填色-谷歌渠道",
"templateId": "coloring",
"template": { "id": "coloring", "name": "填色互动广告", "version": "1.0.0" },
"status": "assets_ready",
"theme": { "bgGradient": "...", "ctaText": "PLAY NOW" },
"assets": [
{ "key": "config", "fileName": "config.json", "fileSize": 2048, "isRequired": true },
{ "key": "page", "fileName": "page.png", "fileSize": 302000, "isRequired": true },
{ "key": "map", "fileName": "map.png", "fileSize": 46000, "isRequired": true },
{ "key": "special", "fileName": "special.jpeg","fileSize": 150000, "isRequired": false }
],
"recentBuilds": [
{ "id": "uuid-b1", "status": "completed", "platforms": ["google"], "createdAt": "..." }
],
"createdAt": "2026-06-03T10:00:00Z",
"updatedAt": "2026-06-03T10:30:00Z"
}
}
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/creatives/:id/assets/upload |
上传素材 zip(multipart/form-data) |
DELETE |
/creatives/:id/assets |
清除当前创意所有素材 |
POST /creatives/:id/assets/upload:
multipart/form-datafile → .zip 文件storage/creatives/:id/assets/assets_ready响应:
{
"data": {
"files": [
{ "key": "config", "fileName": "config.json", "fileSize": 2048, "valid": true },
{ "key": "page", "fileName": "page.png", "fileSize": 302000, "valid": true },
{ "key": "map", "fileName": "map.png", "fileSize": 46000, "valid": true }
],
"warnings": []
}
}
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/creatives/:id/builds |
触发构建 |
GET |
/creatives/:id/builds |
构建历史 |
GET |
/builds/:id/status |
构建状态(轮询) |
GET |
/builds/:id/download/:platform |
下载单个平台 HTML |
GET |
/builds/:id/download/all |
下载全部产物 ZIP |
POST /creatives/:id/builds 请求:
{
"platforms": ["google", "applovin", "unity"],
"theme": {
"bgGradient": "linear-gradient(160deg, #fff9f2, #fed)",
"ctaGradient": "linear-gradient(135deg, #ff6b6b, #ee5a24)",
"ctaText": "PLAY NOW",
"progressColor": "#07ce07"
}
}
响应:
{
"data": {
"id": "uuid-b1",
"status": "pending",
"platforms": ["google", "applovin", "unity"]
}
}
GET /builds/:id/status 响应(轮询用):
{
"data": {
"id": "uuid-b1",
"status": "completed",
"results": [
{ "platform": "google", "fileSize": 1024000 },
{ "platform": "applovin", "fileSize": 1025000 },
{ "platform": "unity", "fileSize": 1023000 }
]
}
}
buildService.build(creativeId, buildId, platforms, theme)
│
▼
┌─ 1. 预检查 ─────────────────────────────────────┐
│ - 读取 manifest.json │
│ - 校验必填素材文件均存在 │
│ - 校验 creative.status ∈ {assets_ready, built} │
└──────────────────────────────────────────────────┘
│
▼
┌─ 2. 准备构建环境 ────────────────────────────────┐
│ - 创建/更新 symlink: │
│ templates/coloring/assets/user/ │
│ → storage/creatives/<creativeId>/assets/ │
│ - configGenerator.generate(creativeId, theme): │
│ 扫描 assets/ 目录,生成 _ad_config_.ts │
│ 包含条件 import(special 存在则导入) │
└──────────────────────────────────────────────────┘
│
▼
┌─ 3. 构建循环(串行)─────────────────────────────┐
│ for each platform in platforms: │
│ $ cd templates/coloring │
│ $ AD_CONFIG_PATH=src/filler/_ad_config_.ts \ │
│ npx vite build --mode <platform> │
│ → 生成 dist/<platform>/index.html │
│ → 复制到 storage/creatives/<id>/builds/<bid>/ │
└──────────────────────────────────────────────────┘
│
▼
┌─ 4. 收尾 ───────────────────────────────────────┐
│ - 打包 all.zip(archiver) │
│ - 更新 builds 表 (status=completed, results) │
│ - 更新 creatives 表 (status=built) │
│ - 删除 _ad_config_.ts(可选保留 symlink) │
└──────────────────────────────────────────────────┘
// platform/server/src/services/configGenerator.ts
import fs from "fs";
import path from "path";
interface GenerateConfigInput {
creativeId: string;
theme: Record<string, string>;
manifest: Manifest;
}
function generate(input: GenerateConfigInput): string {
const userAssetsDir = path.resolve(
__dirname, `../../../storage/creatives/${input.creativeId}/assets`
);
const files = fs.readdirSync(userAssetsDir);
const hasSpecial = files.includes("special.jpeg") || files.includes("special.jpg");
let imports = "";
let assetFields = "";
// Required assets (always present after validation)
imports += `import configRaw from "/assets/user/config.json?raw";\n`;
imports += `import pageUrl from "/assets/user/page.png?url";\n`;
imports += `import mapUrl from "/assets/user/map.png?url";\n`;
assetFields += " configRaw, pageUrl, mapUrl,\n";
// Optional: special image
if (hasSpecial) {
const ext = files.find(f => f.startsWith("special."))!.split(".").pop();
imports += `import specialUrl from "/assets/user/special.${ext}?url";\n`;
assetFields += " specialUrl,\n";
}
// Template-provided assets
imports += `import numberFontUrl from "/assets/fonts/numbers_roboto_500.png?url";\n`;
imports += `import fingerUrl from "/assets/img/finger.png?url";\n`;
imports += `import logoUrl from "/assets/img/logo.png?url";\n`;
// ... etc
return `${imports}
export const adAssets = {
${assetFields} numberFontUrl, fingerUrl, logoUrl,
logoTxtUrl, coloringPagesUrl, slogonUrl,
};
export const adTheme = ${JSON.stringify(input.theme, null, 2)};
`;
}
// platform/server/src/services/buildService.ts
class BuildService {
private queue: Array<() => Promise<void>> = [];
private running = false;
async enqueue(fn: () => Promise<void>) {
this.queue.push(fn);
if (!this.running) this.processQueue();
}
private async processQueue() {
this.running = true;
while (this.queue.length > 0) {
const task = this.queue.shift()!;
await task();
}
this.running = false;
}
}
Vite build 自身就是单线程的,队列只需保证不并发写 _ad_config_.ts。
vite build 超时:120s(正常构建 < 30s)failed,不清除 symlink(方便排查)/ Dashboard 创意列表卡片 + 新建入口
/creatives/new NewCreative 选模板 → 填名称
/creatives/:id CreativeDetail 上传素材 + 主题配置 + 构建
Dashboard(/):
┌─────────────────────────────────────────┐
│ Playable Ads Platform [+新建创意] │
├─────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 创意1 │ │ 创意2 │ │ 创意3 │ │
│ │ 模板:填色 │ │ 模板:填色 │ │ 模板:填色 │ │
│ │ 状态:已就绪│ │ 状态:已构建│ │ 状态:草稿 │ │
│ │ 3 files │ │ 4 files │ │ 0 files │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ (empty state: "暂无创意,点击新建") │
└─────────────────────────────────────────┘
NewCreative(/creatives/new):
┌─────────────────────────────────────────┐
│ ← 返回 │
│ 新建创意 │
│ │
│ 选择模板: │
│ ┌──────────────────────────┐ (选中✓) │
│ │ 🎨 填色互动广告 │ │
│ │ WebGL 填色核心玩法 │ │
│ └──────────────────────────┘ │
│ │
│ 创意名称:[萌宠填色-谷歌渠道____] │
│ │
│ [取消] [创建] │
└─────────────────────────────────────────┘
CreativeDetail(/creatives/:id)—— 核心工作页:
┌──────────────────────────────────────────────────────────┐
│ ← 返回 萌宠填色-谷歌渠道 状态: ● 素材已就绪 │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌─ 左栏:素材上传 ──────────┐ ┌─ 右栏:主题配置 ───────┐ │
│ │ │ │ │ │
│ │ 📦 上传素材 zip │ │ 背景渐变 │ │
│ │ [拖拽或点击上传] │ │ [████████░░░░] │ │
│ │ │ │ │ │
│ │ 已上传文件 (3/3 必填): │ │ CTA 渐变 │ │
│ │ ✅ config.json 2 KB │ │ [████████░░░░] │ │
│ │ ✅ page.png 302 KB │ │ │ │
│ │ ✅ map.png 46 KB │ │ CTA 文案 │ │
│ │ ⬜ special.jpeg (选填) │ │ [PLAY NOW ] │ │
│ │ │ │ │ │
│ │ [清除素材] │ │ 进度条颜色 │ │
│ │ │ │ [■] #07ce07 │ │
│ └───────────────────────────┘ └────────────────────────┘ │
│ │
│ ┌─ 构建 ───────────────────────────────────────────────┐ │
│ │ 目标平台: │ │
│ │ ☑ Google ☑ Applovin ☑ Unity ☐ Playturbo ☐ Mintegral │
│ │ │ │
│ │ [🚀 开始构建] (素材未上传时 disabled) │ │
│ │ │ │
│ │ ┌─ 构建历史 ───────────────────────────────────┐ │ │
│ │ │ #3 2026-06-03 14:30 ✅ 完成 │ │ │
│ │ │ [Google ↓] [Applovin ↓] [Unity ↓] [全部ZIP ↓] │ │ │
│ │ │ │ │ │
│ │ │ #2 2026-06-03 11:00 ❌ 失败 │ │ │
│ │ │ 错误: Required file 'map.png' missing │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
| 组件 | Loading | Empty | Error | Success |
|---|---|---|---|---|
| Dashboard | Skeleton cards | "暂无创意,点击新建" | "加载失败,点击重试" | 卡片列表 |
| AssetUploader | 上传进度条 | "拖拽 zip 到此处" | "解压失败:缺少 config.json" | 文件清单 |
| ThemeEditor | — | (manifest 无 theme 时隐藏整栏) | — | 表单控件 |
| BuildPanel | 按钮 disabled+spinner | — | "构建失败:xxx" + 重试按钮 | 下载按钮 |
| BuildHistory | Spinner 占位 | "暂无构建记录" | — | 构建列表 |
前端状态简单,不需要 Redux 等状态库。使用 React hooks + 页面级 state:
Dashboard 自身管理创意列表CreativeDetail 用 useReducer 管理一个页面的复杂状态:
API 请求封装为 hooks:
useCreatives() // → { data, loading, error, refetch }
useCreative(id) // → { data, loading, error }
useBuilds(creativeId) // → { data, loading, error, trigger }
useBuildStatus(buildId) // → { status, results } 轮询
目标:Express 启动、React 可访问、DB 初始化。
| # | 任务 | 产出 |
|---|---|---|
| 1.1 | 重构目录结构:src/ → templates/coloring/,创建 platform/ |
目录就位 |
| 1.2 | 创建 templates/coloring/manifest.json |
manifest 文件 |
| 1.3 | platform/server/:Express + SQLite + better-sqlite3 初始化 |
server 可启动 |
| 1.4 | 建表 + 种子数据(coloring 模板注册) | DB 就绪 |
| 1.5 | platform/client/:Vite React + react-router-dom 脚手架 |
dev server 可访问 |
| 1.6 | 清理:删除 setting.css 引用(index.html)、标记 Loader.ts 为待移除 |
代码整洁 |
验收:
cd platform/server && npm run dev → :3001 响应cd platform/client && npm run dev → :5173 React 页面可访问GET /api/v1/templates 返回 coloring 模板目标:index.ts 改为从 #ad-config 导入,dev/CLI 构建行为不变。
| # | 任务 | 产出 |
|---|---|---|
| 2.1 | 创建 ad-config.ts(默认配置,内容 = 当前 11 行 import) |
默认配置就位 |
| 2.2 | 重构 index.ts:分散 import → import { adAssets, adTheme } from "#ad-config" |
index.ts 重构完成 |
| 2.3 | loadResource() 适配:检查 "specialUrl" in adAssets |
optional 处理 |
| 2.4 | vite.config.js 加 #ad-config alias (+ 环境变量切换) |
alias 就位 |
| 2.5 | 验证:npm run dev(模板目录)行为不变 |
dev 正常 |
| 2.6 | 验证:npm run build:all(模板目录)产物不变 |
构建正常 |
| 2.7 | 验证:AD_CONFIG_PATH=src/filler/ad-config.ts npx vite build 产物一致 |
环境变量切换正常 |
| 2.8 | 移除 Loader.ts(未被任何代码引用) |
死代码清理 |
验收:日常开发和 CLI 构建行为与重构前完全一致。dist/ 下所有平台产物大小、功能无差异。
目标:curl 可触发完整"上传 zip → 构建 → 下载"流程。
| # | 任务 | 产出 |
|---|---|---|
| 3.1 | storageService:zip 解压(adm-zip)、文件扫描、校验(对照 manifest) |
素材存取 |
| 3.2 | configGenerator:根据文件列表 + theme 生成 _ad_config_.ts + 创建 symlink |
配置生成 |
| 3.3 | buildService:构建队列 + mutex + npx vite build 调用 + 产物复制 + ZIP 打包 |
构建服务 |
| 3.4 | GET /templates / GET /templates/:id |
模板 API |
| 3.5 | GET/POST /creatives / GET/PATCH/DELETE /creatives/:id |
创意 CRUD |
| 3.6 | POST /creatives/:id/assets/upload / DELETE /creatives/:id/assets |
素材 API |
| 3.7 | POST/GET /creatives/:id/builds / GET /builds/:id/status / GET /builds/:id/download/:platform |
构建 API |
| 3.8 | 错误处理 + 日志 | 稳定性 |
验收:
# 1. 创建创意
curl -X POST http://localhost:3001/api/v1/creatives \
-H "Content-Type: application/json" \
-d '{"name":"测试创意","templateId":"coloring"}'
# 2. 上传素材
curl -X POST http://localhost:3001/api/v1/creatives/<id>/assets/upload \
-F "file=@test-assets.zip"
# 3. 触发构建
curl -X POST http://localhost:3001/api/v1/creatives/<id>/builds \
-H "Content-Type: application/json" \
-d '{"platforms":["google","applovin"],"theme":{...}}'
# 4. 轮询状态
curl http://localhost:3001/api/v1/builds/<bid>/status
# 5. 下载
curl -O http://localhost:3001/api/v1/builds/<bid>/download/all
目标:浏览器端可完整体验"新建创意 → 上传素材 → 配置 → 构建 → 下载"。
| # | 任务 | 产出 |
|---|---|---|
| 4.1 | API client 封装 + React Query hooks | 数据层 |
| 4.2 | Layout + Dashboard(列表 + 空态 + 新建入口) |
首页 |
| 4.3 | NewCreative(模板选择 + 名称输入) |
新建页 |
| 4.4 | CreativeDetail — 左栏 AssetUploader(拖拽上传 zip + 文件列表 + 清除) |
素材区 |
| 4.5 | CreativeDetail — 右栏 ThemeEditor(由 manifest.theme 驱动的动态表单) |
配置区 |
| 4.6 | CreativeDetail — 底部 PlatformSelector + 构建按钮 + BuildHistory |
构建区 |
| 4.7 | BuildPanel:触发构建 → 轮询状态(spinner)→ 完成显示下载按钮 |
构建交互 |
| 4.8 | 各组件 loading / empty / error 状态覆盖 | 完整性 |
| 4.9 | 响应式适配(Desktop 双栏,Mobile 上下堆叠) | 移动端可用 |
验收:浏览器全流程可操作,各状态覆盖无白屏。
目标:ECS 可访问完整平台。
| # | 任务 | 产出 |
|---|---|---|
| 5.1 | Client 生产构建(vite build)→ platform/client/dist/ |
静态文件 |
| 5.2 | Server 编译(tsc)→ Express 同时服务 API + 静态文件 |
单体部署 |
| 5.3 | Dockerfile(Node 20 + 模板依赖预装) | Docker 化 |
| 5.4 | ECS 部署 + 端口映射(3001) | 公网可访问 |
| 5.5 | 端到端手动测试(上传 zip → 构建 → 下载 → 真机验证产物) | 质量确认 |
验收:http://42.193.231.145:3001 可访问,完整流程无报错。
| 风险 | 概率 | 影响 | 缓解措施 |
|---|---|---|---|
| 用户上传的 zip 文件命名不规范(大小写、扩展名) | 高 | 低 | 文件匹配时忽略大小写和扩展名变体(.jpeg/.jpg、.png/.PNG) |
| 大素材包导致 Vite build 超时(>120s) | 中 | 中 | 设置合理超时,前端提示"构建时间较长请耐心等待" |
并发构建写 _ad_config_.ts 冲突 |
低 | 高 | Mutex 队列串行化,每 creative 同一时间只有 1 个构建 |
storage/ 磁盘积累 |
中 | 低 | 保留最近 10 次构建产物;后续可加自动清理 cron |
| WebGL 2 在低端 Android WebView 不支持 | 中 | 高 | 模板层面检测 + 静态降级画面(一期已有基础) |
| Playturbo 扫描拒绝动态生成的 HTML | 中 | 中 | 构建后 finalize plugin 已处理 type="module" 移除(一期已验证) |
| 模板目录结构变动后 vite alias 失效 | 低 | 中 | vite.config.js 使用 path.resolve(__dirname, ...) 绝对路径 |
| optional 文件判断逻辑错误导致构建失败 | 中 | 中 | configGenerator 单元测试覆盖"有/无 special"两种场景 |
当前只实现 coloring 模板,但架构已预留多模板支持:
新增模板步骤:
templates/<new-id>/ 下放置完整 Vite 项目manifest.jsontemplates 表中插入一行(或 seed 脚本中添加)manifest 未来可扩展字段:
previewImage:模板缩略图,Dashboard 展示build.env:构建时需要注入的额外环境变量assets.previewHint:上传区的示例图theme.groups:主题参数分组(基础/高级)平台功能后续可加: