背景
上一篇文章整理了 OpenGame 的基本概念:它可以把自然语言提示词转成一个可运行的 Web 游戏工程。但如果只在命令行里执行 opengame -p "..." --yolo,很快会遇到几个工程问题:
- 生成任务耗时长,需要队列和状态追踪。
- Agent 输出很多日志,需要实时可视化。
- 游戏不是“一次生成就结束”,需要 GDD 审核、分阶段实现、失败修复、版本迭代。
- 生成结果需要可预览、可试玩、可换皮,而不是只留在本地目录。
- 失败原因需要结构化,否则很难知道是 API 中断、构建失败、资源缺失,还是画面质量不过关。
所以我设计了一个 opengame-web-console:它不是简单包一层按钮,而是把 OpenGame SDK 放进一个任务化、可观察、可修复、可迭代的控制中心。
结论
这个控制中心的核心设计可以概括为一句话:
前端只负责提交需求和观察状态,后端把每一次游戏生成封装成任务,任务运行时负责规划、调用 SDK、执行门禁、自动修复、沉淀版本。
整体链路如下:
用户提示词
↓
Web Console
↓
POST /api/tasks
↓
TaskService 创建任务记录与 workspace
↓
TaskQueue 串行调度
↓
TaskRunner 调用 @opengame/sdk
↓
OpenGame Agent 在 workspace 中生成游戏
↓
Playable / Visual Gate
↓
成功:保存 previewEntry,可预览、试玩、迭代、换皮
失败:记录 failureReason,生成修复 prompt,可重试
为什么不是直接调用 CLI
直接调用 CLI 适合本地实验,但不适合做“控制中心”。控制中心需要解决的是长期运行问题:
| 问题 | CLI 模式 | 控制中心模式 |
|---|---|---|
| 多任务 | 手动开多个终端 | 任务队列统一调度 |
| 状态 | 看终端输出 | SQLite 持久化任务状态 |
| 日志 | 滚动文本易丢 | JSONL 持久化 + SSE 实时推送 |
| 审核 | 生成时无法插入人工关卡 | 半自动模式先审 GDD |
| 失败 | 人工读日志判断 | failureReason 结构化 |
| 结果 | 本地目录 | Web 预览、试玩、版本链 |
| 复用 | 复制目录手动改 | 基于版本继续迭代 |
因此控制中心的第一原则是:不要把 OpenGame 当成一个一次性命令,而要当成一个异步任务运行时。
技术栈
控制中心使用的技术栈比较克制:
| 模块 | 技术 |
|---|---|
| Web 框架 | Next.js |
| UI | React |
| 数据库 | SQLite / better-sqlite3 |
| Agent 调用 | @opengame/sdk |
| 实时事件 | Server-Sent Events |
| 可玩性检查 | Playwright |
| 图片处理 | sharp |
| 资源生成 | OpenGame SDK MCP tool |
| 任务产物 | 本地 workspace 目录 |
运行时数据默认放在:
data/
├── app.db
└── runs/
└── <taskId>/
├── workspace/
└── events.jsonl
这里的 workspace/ 是每个任务的独立生成目录,events.jsonl 是任务运行日志。
组件设计
前端组件
前端围绕一个 Workbench 展开:
Workbench
├── TaskCreateForm # 创建任务:标题、提示词、自动/半自动、full/mvp
├── TaskList # 任务队列
├── StatusBadge # 状态展示
├── GddReviewPanel # 半自动模式下审核 GDD
├── TaskPipelineReport # M1-M4 与 Gate 报告
├── TaskQualityReport # 可玩性与视觉质量报告
├── TaskPreviewPanel # iframe 预览
├── TaskPlayLink # 打开沉浸式试玩页
├── VersionIterateForm # 基于当前版本继续迭代
├── VersionList # 版本链
├── SkinManager # 导出占位资源包、上传换皮包
└── TaskLogPanel # 实时日志
前端不直接碰 OpenGame SDK。它只调用控制中心 API,并通过 SSE 或定时刷新同步任务状态。
后端组件
后端核心组件如下:
Container
├── env # 数据目录配置
├── db # SQLite
├── TaskRepository # tasks 表读写
├── GameRepository # games 与版本链
├── WorkspaceManager # 为 task 创建 workspace
├── TaskQueue # 串行队列
├── SdkClient # @opengame/sdk 封装
├── TaskRunner # SDK 流式消息转 TaskEvent
├── TaskService # 任务生命周期编排
├── PlayableChecker # 浏览器可玩性检查
├── VisualQualityChecker # 视觉与资源质量检查
└── SkinService # 换皮与资源包
其中 TaskService 是最重要的编排层。它不负责具体写游戏代码,而是负责决定什么时候规划、什么时候实现、什么时候检查、什么时候修复、什么时候把任务标记为成功或失败。
API 设计
控制中心 API 可以分为五类。
任务 API
GET /api/tasks
POST /api/tasks
GET /api/tasks/:taskId
POST /api/tasks/:taskId/retry
GET /api/tasks/events
GET /api/tasks/:taskId/events
创建任务的请求体:
{
"title": "太空塔防",
"prompt": "创建一个太空主题塔防游戏...",
"automationMode": "auto",
"pipelineMode": "full"
}
automationMode 有两种:
| 模式 | 行为 |
|---|---|
auto | 规划 GDD 后自动进入实现 |
semi_auto | 规划 GDD 后暂停,等待人工审核 |
pipelineMode 也有两种:
| 模式 | 行为 |
|---|---|
mvp | 只跑 M1 playable core |
full | 跑 M1-M4,再跑 final gate |
GDD API
GET /api/tasks/:taskId/gdd
PUT /api/tasks/:taskId/gdd
POST /api/tasks/:taskId/gdd/approve
半自动模式下,任务会先进入 awaiting_gdd_review。用户可以在页面里编辑 GDD,再点击确认,后端才继续进入实现阶段。
预览与试玩 API
GET /api/tasks/:taskId/preview
GET /api/tasks/:taskId/preview/assets/*
GET /api/tasks/:taskId/play
GET /tasks/:taskId/play
preview 是嵌在控制台里的预览,play 是更沉浸的试玩页。控制台不会要求用户进入服务器目录找 index.html,而是把生成产物代理成 Web 路由。
版本 API
GET /api/tasks/:taskId/versions
POST /api/tasks/:taskId/iterate
POST /api/tasks/:taskId/versions/:versionId/promote
每次迭代不是覆盖原任务,而是复制旧 workspace,生成一个新版本任务:
V1 workspace
↓ clone
V2 workspace
↓ 修改 prompt:基于 V1 做增量需求
V2 进入任务队列
这样可以保留历史版本,也可以随时把某个版本提升为当前版本。
换皮 API
GET /api/tasks/:taskId/skin/placeholders
POST /api/tasks/:taskId/skin
GET /api/tasks/:taskId/skin/active/*
生成游戏时,Agent 会被要求创建 skin-manifest.json,声明哪些 PNG 资源可以替换。控制中心可以导出占位资源包,用户替换后上传 zip,后端会创建一个新的成功版本。
任务状态机
任务状态设计如下:
queued
↓
running
├─ planning
│ ├─ auto → m1_mvp
│ └─ semi_auto → awaiting_gdd_review
│ ↓ approve
│ queued → running
├─ m1_mvp
├─ m2_complete_loop
├─ m3_visual_audio
├─ m4_polish
├─ final_gate
└─ repair
↓
succeeded / failed
状态字段分两层:
status表示任务总体状态,例如queued、running、awaiting_gdd_review、succeeded、failed。currentStage表示当前流水线阶段,例如planning、m1_mvp、repair。
这样 UI 可以同时展示“任务还在 running”和“现在跑到 M3 visual/audio”。
生成流水线
控制中心把一次生成拆成规划和实现两大段。
1. Planning:先写 GDD
Planning 阶段只做一件事:根据用户 prompt 生成 docs/opengame-gdd.md。
用户 prompt
↓
buildOpenGamePlanningPrompt()
↓
@opengame/sdk 在 planning-workspace 运行
↓
输出 docs/opengame-gdd.md
↓
复制到主 workspace
这里有一个小设计:规划阶段运行在 planning-workspace,不是直接在主 workspace 里写。这样可以避免规划 Agent 误生成实现文件。规划完成后,只把 GDD 文档复制回主 workspace。
如果文件没写成功,系统还会尝试从 Agent 输出中的标记恢复:
BEGIN_OPENGAME_GDD
...
END_OPENGAME_GDD
这能降低“Agent 说它写了文件,但文件实际不存在”的失败率。
2. M1-M4:分阶段实现
完整模式会跑四个实现阶段:
| 阶段 | 目标 |
|---|---|
| M1 playable core | 最小可玩核心:输入、规则、胜负、重开 |
| M2 complete loop | 菜单、结算、三关或难度递进 |
| M3 visual and audio | PNG 资源、UI 状态、动画反馈、音效 |
| M4 polish | 移动端、性能、加载、引导、最终观感 |
每个阶段都会把 GDD、用户原始需求、上一阶段门禁证据一起喂给 Agent:
GDD + userPrompt + stageGoals + previousEvidence
↓
buildOpenGameStagePrompt()
↓
TaskRunner 调用 @opengame/sdk
↓
Agent 修改 workspace
↓
Gate 检查
↓
通过则进入下一阶段
失败则进入 repair
这个设计的好处是让 Agent 不必一次性完成所有要求。M1 先保可玩,M2 补完整循环,M3 做表现,M4 做收尾。
SDK 调用设计
控制中心通过 SdkClient 封装 @opengame/sdk:
TaskRunner
↓
SdkClient.runTask({ prompt, cwd })
↓
query({
prompt,
options: {
cwd,
permissionMode: "yolo",
debug: true,
mcpServers: {
"opengame-assets": assetServer
}
}
})
关键点有三个:
cwd指向任务 workspace,Agent 的所有文件操作都限制在这个任务目录里。permissionMode: "yolo"允许 Agent 在任务目录内执行必要命令,适合受控的服务器环境。- 注入
opengame-assetsMCP Server,让 Agent 可以调用generate_game_asset生成 PNG 游戏资源。
TaskRunner 会把 SDK 的流式消息转成统一的 TaskEvent:
SDK stream message
↓
mapSdkMessageToTaskEvents()
↓
task.log / task.completed / task.failed
↓
写入 events.jsonl
↓
通过 SSE 推送到前端
为了避免任务卡死,TaskRunner 还做了两层超时:
- idle timeout:一段时间没有新消息,认为 SDK 卡住。
- wall timeout:单次尝试总运行时间超限。
对 API 中断和 timeout 这类可重试错误,最多尝试 3 次。
实时日志设计
日志有两条路径:
TaskEvent
├── append events.jsonl # 持久化
└── emitTaskEvent() # 内存事件总线
↓
/api/tasks/events # 全局 SSE
/api/tasks/:id/events # 单任务 SSE
↓
TaskLogPanel / Workbench
为什么既要 JSONL,又要 SSE?
- SSE 负责实时体验。
- JSONL 负责刷新后还能看到历史日志。
- 如果 SSE 推送失败,不应该影响任务执行,所以日志写入也是 best effort。
质量门禁
一次 Agent 生成结束后,控制中心不会立刻标记成功,而是跑 gate。
Static Preview Gate
先检查是否能找到可预览入口:
npm run build
↓
resolvePreviewEntry()
↓
index.html / dist/index.html / 其他可预览入口
如果找不到入口,任务失败,失败原因会归类为 build_failed 或相关类型。
Playable Gate
Playable Gate 会启动一个临时预览服务器,再用 Playwright 打开页面:
startPreviewServer()
↓
Playwright chromium
↓
监听页面错误、console error、资源 404
↓
检查 Loading 是否消失
↓
检查 canvas 是否空白
这一步解决的是“能构建但不能玩”的问题。
Visual Gate
Visual Gate 更偏游戏观感:
打开预览页
↓
截图
↓
采样 canvas 像素
↓
检查 skin-manifest.json
↓
检查 PNG 是否存在、尺寸是否足够、是否被实际引用
它会拒绝几类结果:
- 没有 canvas。
- canvas 全空白。
- canvas 颜色过于单一,看起来像占位图。
- 没有
skin-manifest.json。 - manifest 里的资源缺失。
- active asset 不是 PNG。
- 资源太小或未被游戏引用。
自动修复逻辑
如果 Gate 失败,控制中心不会马上结束,而是进入 repair:
Gate failed
↓
buildOpenGameRepairPrompt(errorMessage, gddDraft, stage)
↓
Agent 在原 workspace 中修复
↓
再次运行 Gate
↓
最多修复 2 次
修复 prompt 的核心要求是:
- 不要从零重写。
- 在现有 workspace 中做最小修复。
- 保证 preview entry 存在。
- 保证所有本地 script、style、image、skin asset 都存在。
- 如果是视觉质量失败,就补 PNG、改布局、修按钮反馈。
- 如果是缺资源,就优先创建缺失路径。
对“预览 HTML 引用了不存在的本地资源”这种问题,还有专门的 targeted repair prompt,要求 Agent 先创建这些精确路径,避免越修越偏。
资源生成 MCP
控制中心为 OpenGame SDK 注入了一个 generate_game_asset 工具:
Agent
↓ call generate_game_asset
↓
Asset MCP Server
↓
Image Provider
├── tongyi
├── doubao
├── openai-compat
└── n1n
↓
写入 workspace 内 PNG 文件
工具入参包括:
{
"prompt": "space tower turret",
"outputPath": "public/skins/active/turret.png",
"size": "1024*1024",
"kind": "sprite",
"transparentBackground": true,
"styleGuide": "dark sci-fi pixel art",
"negativePrompt": "text, logo, watermark"
}
如果图片 provider 超时、限流或临时失败,系统会用 sharp 生成一个 fallback PNG。这样任务不会因为图片服务偶发不可用而完全中断。
版本链设计
游戏生成成功后,它不只是一个 task,也是一个 game version。
games
└── gameId
├── V1 taskId
├── V2 taskId
└── V3 taskId
当用户基于某个版本继续迭代时:
选择 V1
↓
输入 changeNote
↓
复制 V1 workspace 到 version-2
↓
构造迭代 prompt
↓
创建 V2 task
↓
进入同一套队列与 Gate
这个设计比“覆盖式修改”稳很多。每次迭代都有完整产物、日志、质量报告和回退路径。
换皮设计
换皮不是让用户直接上传任意图片覆盖目录,而是依赖 skin-manifest.json。
manifest 结构大致是:
{
"version": 1,
"assets": [
{
"key": "player",
"label": "玩家角色",
"path": "public/skins/active/player.png",
"placeholderPath": "public/skins/placeholders/player.png",
"mimeType": "image/png"
}
]
}
控制中心可以:
- 根据 manifest 导出占位资源 zip。
- 用户替换 zip 内同路径 PNG。
- 上传 zip。
- 后端复制当前 workspace。
- 只替换 manifest 中声明的资源。
- 重新
npm run build。 - 创建一个新的成功版本。
换皮本质上也是版本迭代,只是它不调用大模型。
失败分类
失败不能只存一段错误文本。控制中心会把失败归类为 failureReason,例如:
| 类型 | 含义 |
|---|---|
api_terminated | 上游 API 中断 |
timeout | SDK 或任务超时 |
build_failed | 构建失败 |
playable_check_failed | 浏览器可玩性检查失败 |
visual_quality_failed | 视觉质量检查失败 |
cancelled | 被取消 |
recovered | 进程重启后恢复失败状态 |
结构化失败原因有两个作用:
- UI 可以显示更友好的失败说明。
- retry 可以判断从哪个阶段恢复,例如只重跑 gate,还是回到 planning。
进程重启恢复
服务启动时会扫描所有 running 任务:
server boot
↓
recoverRunningTasks()
↓
running → failed
↓
failureReason = recovered
↓
写入 task.failed 日志
这样做很朴素,但非常重要。因为 Node 进程重启后,内存里的队列已经没了,继续显示 running 会误导用户。把它们标记为可解释的失败状态,再允许用户 retry,是更可靠的选择。
总体逻辑图
完整控制中心可以这样理解:
┌────────────────────┐
│ React UI │
│ create/list/log/play│
└─────────┬──────────┘
│ HTTP / SSE
▼
┌────────────────────┐
│ Next.js API │
│ route handlers │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ TaskService │
│ lifecycle director │
└──────┬───────┬──────┘
│ │
│ ├──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌───────────────┐
│ SQLite │ │ JSONL Log│ │ Workspace FS │
└──────────┘ └──────────┘ └───────────────┘
│
▼
┌────────────────────┐
│ TaskQueue │
└─────────┬──────────┘
▼
┌────────────────────┐
│ TaskRunner │
└─────────┬──────────┘
▼
┌────────────────────┐
│ @opengame/sdk │
│ + asset MCP │
└─────────┬──────────┘
▼
┌────────────────────┐
│ Generated Web Game │
└─────────┬──────────┘
▼
┌────────────────────┐
│ Playable/Visual Gate│
└──────┬────────┬─────┘
│ │
▼ ▼
succeeded repair / failed
设计取舍
1. 串行队列,而不是并发队列
当前实现使用单队列串行执行。这样吞吐低一些,但更稳:
- 避免多个 Agent 同时抢 CPU、网络和图片 provider。
- 避免 Playwright 与构建进程把服务器打满。
- 更容易定位日志和失败原因。
等单任务链路稳定后,再考虑按资源配额做并发。
2. 本地 SQLite,而不是一开始上 PostgreSQL
控制中心首先是单机工具,SQLite 足够简单可靠。任务元数据放 SQLite,产物放文件系统,日志放 JSONL,正好符合静态产物生成场景。
如果后续要多人使用、分布式队列、多机执行,再迁到 PostgreSQL + Redis Queue 会更合适。
3. Gate 失败自动修复,但限制次数
自动修复有价值,但不能无限循环。当前最多修复 2 次。这样可以覆盖资源缺失、构建小错误、空白画面等常见问题,同时避免 Agent 在错误方向上无限消耗。
4. 半自动 GDD 审核是必要入口
全自动适合快速试验,但复杂游戏最好先审 GDD。GDD 是后续 M1-M4 的源头,如果需求理解错了,后面生成得越完整,返工越重。
小结
OpenGame Web 控制中心真正解决的不是“给命令行加一个网页按钮”,而是把 AI 生成游戏这件事产品化:
- 用任务队列承接长耗时生成。
- 用 GDD 把模糊需求变成工程计划。
- 用 M1-M4 把一次性生成拆成可控阶段。
- 用 Playwright 和视觉检查把“可玩”变成门禁。
- 用 repair prompt 把失败转成下一轮修复输入。
- 用版本链和换皮把一次生成变成可持续迭代。
这套设计也可以迁移到其他 Agent 应用:只要任务耗时长、结果需要验收、失败需要自动修复,就应该把 Agent 调用包进类似的控制中心,而不是停留在一次性脚本调用。