背景

上一篇文章整理了 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
UIReact
数据库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 表示任务总体状态,例如 queuedrunningawaiting_gdd_reviewsucceededfailed
  • currentStage 表示当前流水线阶段,例如 planningm1_mvprepair

这样 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 audioPNG 资源、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
    }
  }
})

关键点有三个:

  1. cwd 指向任务 workspace,Agent 的所有文件操作都限制在这个任务目录里。
  2. permissionMode: "yolo" 允许 Agent 在任务目录内执行必要命令,适合受控的服务器环境。
  3. 注入 opengame-assets MCP 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"
    }
  ]
}

控制中心可以:

  1. 根据 manifest 导出占位资源 zip。
  2. 用户替换 zip 内同路径 PNG。
  3. 上传 zip。
  4. 后端复制当前 workspace。
  5. 只替换 manifest 中声明的资源。
  6. 重新 npm run build
  7. 创建一个新的成功版本。

换皮本质上也是版本迭代,只是它不调用大模型。

失败分类

失败不能只存一段错误文本。控制中心会把失败归类为 failureReason,例如:

类型含义
api_terminated上游 API 中断
timeoutSDK 或任务超时
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 调用包进类似的控制中心,而不是停留在一次性脚本调用。