<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>OpenGame on heyaohua's Blog</title><link>https://blog.heyaohua.com/tags/opengame/</link><description>Recent content in OpenGame on heyaohua's Blog</description><image><title>heyaohua's Blog</title><url>https://blog.heyaohua.com/og-image.png</url><link>https://blog.heyaohua.com/og-image.png</link></image><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Wed, 06 May 2026 15:45:00 +0800</lastBuildDate><atom:link href="https://blog.heyaohua.com/tags/opengame/index.xml" rel="self" type="application/rss+xml"/><item><title>OpenGame Web 控制中心设计：从提示词到可试玩游戏的任务化流水线</title><link>https://blog.heyaohua.com/posts/2026/05/opengame-web-console-design/</link><pubDate>Wed, 06 May 2026 15:45:00 +0800</pubDate><guid>https://blog.heyaohua.com/posts/2026/05/opengame-web-console-design/</guid><description>本文以 opengame-web-console 的实现为例，讲解如何设计一个 OpenGame Web 控制中心：任务队列、GDD 审核、SDK 调用、实时日志、M1-M4 生成流水线、质量门禁、自动修复、版本迭代与换皮。</description><content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p>上一篇文章整理了 <a href="/posts/2026/05/opengame-technical-guide/">OpenGame</a> 的基本概念：它可以把自然语言提示词转成一个可运行的 Web 游戏工程。但如果只在命令行里执行 <code>opengame -p &quot;...&quot; --yolo</code>，很快会遇到几个工程问题：</p>
<ul>
<li>生成任务耗时长，需要队列和状态追踪。</li>
<li>Agent 输出很多日志，需要实时可视化。</li>
<li>游戏不是“一次生成就结束”，需要 GDD 审核、分阶段实现、失败修复、版本迭代。</li>
<li>生成结果需要可预览、可试玩、可换皮，而不是只留在本地目录。</li>
<li>失败原因需要结构化，否则很难知道是 API 中断、构建失败、资源缺失，还是画面质量不过关。</li>
</ul>
<p>所以我设计了一个 <code>opengame-web-console</code>：它不是简单包一层按钮，而是把 OpenGame SDK 放进一个任务化、可观察、可修复、可迭代的控制中心。</p>
<h2 id="结论">结论</h2>
<p>这个控制中心的核心设计可以概括为一句话：</p>
<blockquote>
<p>前端只负责提交需求和观察状态，后端把每一次游戏生成封装成任务，任务运行时负责规划、调用 SDK、执行门禁、自动修复、沉淀版本。</p>
</blockquote>
<p>整体链路如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户提示词
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Web Console
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>POST /api/tasks
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>TaskService 创建任务记录与 workspace
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>TaskQueue 串行调度
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>TaskRunner 调用 @opengame/sdk
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>OpenGame Agent 在 workspace 中生成游戏
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Playable / Visual Gate
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>成功：保存 previewEntry，可预览、试玩、迭代、换皮
</span></span><span style="display:flex;"><span>失败：记录 failureReason，生成修复 prompt，可重试
</span></span></code></pre></div><h2 id="为什么不是直接调用-cli">为什么不是直接调用 CLI</h2>
<p>直接调用 CLI 适合本地实验，但不适合做“控制中心”。控制中心需要解决的是长期运行问题：</p>
<table>
  <thead>
      <tr>
          <th>问题</th>
          <th>CLI 模式</th>
          <th>控制中心模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多任务</td>
          <td>手动开多个终端</td>
          <td>任务队列统一调度</td>
      </tr>
      <tr>
          <td>状态</td>
          <td>看终端输出</td>
          <td>SQLite 持久化任务状态</td>
      </tr>
      <tr>
          <td>日志</td>
          <td>滚动文本易丢</td>
          <td>JSONL 持久化 + SSE 实时推送</td>
      </tr>
      <tr>
          <td>审核</td>
          <td>生成时无法插入人工关卡</td>
          <td>半自动模式先审 GDD</td>
      </tr>
      <tr>
          <td>失败</td>
          <td>人工读日志判断</td>
          <td>failureReason 结构化</td>
      </tr>
      <tr>
          <td>结果</td>
          <td>本地目录</td>
          <td>Web 预览、试玩、版本链</td>
      </tr>
      <tr>
          <td>复用</td>
          <td>复制目录手动改</td>
          <td>基于版本继续迭代</td>
      </tr>
  </tbody>
</table>
<p>因此控制中心的第一原则是：<strong>不要把 OpenGame 当成一个一次性命令，而要当成一个异步任务运行时</strong>。</p>
<h2 id="技术栈">技术栈</h2>
<p>控制中心使用的技术栈比较克制：</p>
<table>
  <thead>
      <tr>
          <th>模块</th>
          <th>技术</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Web 框架</td>
          <td>Next.js</td>
      </tr>
      <tr>
          <td>UI</td>
          <td>React</td>
      </tr>
      <tr>
          <td>数据库</td>
          <td>SQLite / better-sqlite3</td>
      </tr>
      <tr>
          <td>Agent 调用</td>
          <td><code>@opengame/sdk</code></td>
      </tr>
      <tr>
          <td>实时事件</td>
          <td>Server-Sent Events</td>
      </tr>
      <tr>
          <td>可玩性检查</td>
          <td>Playwright</td>
      </tr>
      <tr>
          <td>图片处理</td>
          <td>sharp</td>
      </tr>
      <tr>
          <td>资源生成</td>
          <td>OpenGame SDK MCP tool</td>
      </tr>
      <tr>
          <td>任务产物</td>
          <td>本地 workspace 目录</td>
      </tr>
  </tbody>
</table>
<p>运行时数据默认放在：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>data/
</span></span><span style="display:flex;"><span>├── app.db
</span></span><span style="display:flex;"><span>└── runs/
</span></span><span style="display:flex;"><span>    └── &lt;taskId&gt;/
</span></span><span style="display:flex;"><span>        ├── workspace/
</span></span><span style="display:flex;"><span>        └── events.jsonl
</span></span></code></pre></div><p>这里的 <code>workspace/</code> 是每个任务的独立生成目录，<code>events.jsonl</code> 是任务运行日志。</p>
<h2 id="组件设计">组件设计</h2>
<h3 id="前端组件">前端组件</h3>
<p>前端围绕一个 Workbench 展开：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Workbench
</span></span><span style="display:flex;"><span>├── TaskCreateForm        # 创建任务：标题、提示词、自动/半自动、full/mvp
</span></span><span style="display:flex;"><span>├── TaskList              # 任务队列
</span></span><span style="display:flex;"><span>├── StatusBadge           # 状态展示
</span></span><span style="display:flex;"><span>├── GddReviewPanel        # 半自动模式下审核 GDD
</span></span><span style="display:flex;"><span>├── TaskPipelineReport    # M1-M4 与 Gate 报告
</span></span><span style="display:flex;"><span>├── TaskQualityReport     # 可玩性与视觉质量报告
</span></span><span style="display:flex;"><span>├── TaskPreviewPanel      # iframe 预览
</span></span><span style="display:flex;"><span>├── TaskPlayLink          # 打开沉浸式试玩页
</span></span><span style="display:flex;"><span>├── VersionIterateForm    # 基于当前版本继续迭代
</span></span><span style="display:flex;"><span>├── VersionList           # 版本链
</span></span><span style="display:flex;"><span>├── SkinManager           # 导出占位资源包、上传换皮包
</span></span><span style="display:flex;"><span>└── TaskLogPanel          # 实时日志
</span></span></code></pre></div><p>前端不直接碰 OpenGame SDK。它只调用控制中心 API，并通过 SSE 或定时刷新同步任务状态。</p>
<h3 id="后端组件">后端组件</h3>
<p>后端核心组件如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Container
</span></span><span style="display:flex;"><span>├── env                  # 数据目录配置
</span></span><span style="display:flex;"><span>├── db                   # SQLite
</span></span><span style="display:flex;"><span>├── TaskRepository       # tasks 表读写
</span></span><span style="display:flex;"><span>├── GameRepository       # games 与版本链
</span></span><span style="display:flex;"><span>├── WorkspaceManager     # 为 task 创建 workspace
</span></span><span style="display:flex;"><span>├── TaskQueue            # 串行队列
</span></span><span style="display:flex;"><span>├── SdkClient            # @opengame/sdk 封装
</span></span><span style="display:flex;"><span>├── TaskRunner           # SDK 流式消息转 TaskEvent
</span></span><span style="display:flex;"><span>├── TaskService          # 任务生命周期编排
</span></span><span style="display:flex;"><span>├── PlayableChecker      # 浏览器可玩性检查
</span></span><span style="display:flex;"><span>├── VisualQualityChecker # 视觉与资源质量检查
</span></span><span style="display:flex;"><span>└── SkinService          # 换皮与资源包
</span></span></code></pre></div><p>其中 <code>TaskService</code> 是最重要的编排层。它不负责具体写游戏代码，而是负责决定什么时候规划、什么时候实现、什么时候检查、什么时候修复、什么时候把任务标记为成功或失败。</p>
<h2 id="api-设计">API 设计</h2>
<p>控制中心 API 可以分为五类。</p>
<h3 id="任务-api">任务 API</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GET  /api/tasks
</span></span><span style="display:flex;"><span>POST /api/tasks
</span></span><span style="display:flex;"><span>GET  /api/tasks/:taskId
</span></span><span style="display:flex;"><span>POST /api/tasks/:taskId/retry
</span></span><span style="display:flex;"><span>GET  /api/tasks/events
</span></span><span style="display:flex;"><span>GET  /api/tasks/:taskId/events
</span></span></code></pre></div><p>创建任务的请求体：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;title&#34;</span>: <span style="color:#f1fa8c">&#34;太空塔防&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;prompt&#34;</span>: <span style="color:#f1fa8c">&#34;创建一个太空主题塔防游戏...&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;automationMode&#34;</span>: <span style="color:#f1fa8c">&#34;auto&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;pipelineMode&#34;</span>: <span style="color:#f1fa8c">&#34;full&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>automationMode</code> 有两种：</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>行为</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>auto</code></td>
          <td>规划 GDD 后自动进入实现</td>
      </tr>
      <tr>
          <td><code>semi_auto</code></td>
          <td>规划 GDD 后暂停，等待人工审核</td>
      </tr>
  </tbody>
</table>
<p><code>pipelineMode</code> 也有两种：</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>行为</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>mvp</code></td>
          <td>只跑 M1 playable core</td>
      </tr>
      <tr>
          <td><code>full</code></td>
          <td>跑 M1-M4，再跑 final gate</td>
      </tr>
  </tbody>
</table>
<h3 id="gdd-api">GDD API</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GET  /api/tasks/:taskId/gdd
</span></span><span style="display:flex;"><span>PUT  /api/tasks/:taskId/gdd
</span></span><span style="display:flex;"><span>POST /api/tasks/:taskId/gdd/approve
</span></span></code></pre></div><p>半自动模式下，任务会先进入 <code>awaiting_gdd_review</code>。用户可以在页面里编辑 GDD，再点击确认，后端才继续进入实现阶段。</p>
<h3 id="预览与试玩-api">预览与试玩 API</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GET /api/tasks/:taskId/preview
</span></span><span style="display:flex;"><span>GET /api/tasks/:taskId/preview/assets/*
</span></span><span style="display:flex;"><span>GET /api/tasks/:taskId/play
</span></span><span style="display:flex;"><span>GET /tasks/:taskId/play
</span></span></code></pre></div><p><code>preview</code> 是嵌在控制台里的预览，<code>play</code> 是更沉浸的试玩页。控制台不会要求用户进入服务器目录找 <code>index.html</code>，而是把生成产物代理成 Web 路由。</p>
<h3 id="版本-api">版本 API</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GET  /api/tasks/:taskId/versions
</span></span><span style="display:flex;"><span>POST /api/tasks/:taskId/iterate
</span></span><span style="display:flex;"><span>POST /api/tasks/:taskId/versions/:versionId/promote
</span></span></code></pre></div><p>每次迭代不是覆盖原任务，而是复制旧 workspace，生成一个新版本任务：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>V1 workspace
</span></span><span style="display:flex;"><span>  ↓ clone
</span></span><span style="display:flex;"><span>V2 workspace
</span></span><span style="display:flex;"><span>  ↓ 修改 prompt：基于 V1 做增量需求
</span></span><span style="display:flex;"><span>V2 进入任务队列
</span></span></code></pre></div><p>这样可以保留历史版本，也可以随时把某个版本提升为当前版本。</p>
<h3 id="换皮-api">换皮 API</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GET  /api/tasks/:taskId/skin/placeholders
</span></span><span style="display:flex;"><span>POST /api/tasks/:taskId/skin
</span></span><span style="display:flex;"><span>GET  /api/tasks/:taskId/skin/active/*
</span></span></code></pre></div><p>生成游戏时，Agent 会被要求创建 <code>skin-manifest.json</code>，声明哪些 PNG 资源可以替换。控制中心可以导出占位资源包，用户替换后上传 zip，后端会创建一个新的成功版本。</p>
<h2 id="任务状态机">任务状态机</h2>
<p>任务状态设计如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>queued
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>running
</span></span><span style="display:flex;"><span>  ├─ planning
</span></span><span style="display:flex;"><span>  │   ├─ auto       → m1_mvp
</span></span><span style="display:flex;"><span>  │   └─ semi_auto  → awaiting_gdd_review
</span></span><span style="display:flex;"><span>  │                    ↓ approve
</span></span><span style="display:flex;"><span>  │                  queued → running
</span></span><span style="display:flex;"><span>  ├─ m1_mvp
</span></span><span style="display:flex;"><span>  ├─ m2_complete_loop
</span></span><span style="display:flex;"><span>  ├─ m3_visual_audio
</span></span><span style="display:flex;"><span>  ├─ m4_polish
</span></span><span style="display:flex;"><span>  ├─ final_gate
</span></span><span style="display:flex;"><span>  └─ repair
</span></span><span style="display:flex;"><span>      ↓
</span></span><span style="display:flex;"><span>succeeded / failed
</span></span></code></pre></div><p>状态字段分两层：</p>
<ul>
<li><code>status</code> 表示任务总体状态，例如 <code>queued</code>、<code>running</code>、<code>awaiting_gdd_review</code>、<code>succeeded</code>、<code>failed</code>。</li>
<li><code>currentStage</code> 表示当前流水线阶段，例如 <code>planning</code>、<code>m1_mvp</code>、<code>repair</code>。</li>
</ul>
<p>这样 UI 可以同时展示“任务还在 running”和“现在跑到 M3 visual/audio”。</p>
<h2 id="生成流水线">生成流水线</h2>
<p>控制中心把一次生成拆成规划和实现两大段。</p>
<h3 id="1-planning先写-gdd">1. Planning：先写 GDD</h3>
<p>Planning 阶段只做一件事：根据用户 prompt 生成 <code>docs/opengame-gdd.md</code>。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户 prompt
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>buildOpenGamePlanningPrompt()
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>@opengame/sdk 在 planning-workspace 运行
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>输出 docs/opengame-gdd.md
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>复制到主 workspace
</span></span></code></pre></div><p>这里有一个小设计：规划阶段运行在 <code>planning-workspace</code>，不是直接在主 workspace 里写。这样可以避免规划 Agent 误生成实现文件。规划完成后，只把 GDD 文档复制回主 workspace。</p>
<p>如果文件没写成功，系统还会尝试从 Agent 输出中的标记恢复：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>BEGIN_OPENGAME_GDD
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>END_OPENGAME_GDD
</span></span></code></pre></div><p>这能降低“Agent 说它写了文件，但文件实际不存在”的失败率。</p>
<h3 id="2-m1-m4分阶段实现">2. M1-M4：分阶段实现</h3>
<p>完整模式会跑四个实现阶段：</p>
<table>
  <thead>
      <tr>
          <th>阶段</th>
          <th>目标</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>M1 playable core</td>
          <td>最小可玩核心：输入、规则、胜负、重开</td>
      </tr>
      <tr>
          <td>M2 complete loop</td>
          <td>菜单、结算、三关或难度递进</td>
      </tr>
      <tr>
          <td>M3 visual and audio</td>
          <td>PNG 资源、UI 状态、动画反馈、音效</td>
      </tr>
      <tr>
          <td>M4 polish</td>
          <td>移动端、性能、加载、引导、最终观感</td>
      </tr>
  </tbody>
</table>
<p>每个阶段都会把 GDD、用户原始需求、上一阶段门禁证据一起喂给 Agent：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GDD + userPrompt + stageGoals + previousEvidence
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>buildOpenGameStagePrompt()
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>TaskRunner 调用 @opengame/sdk
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Agent 修改 workspace
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Gate 检查
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>通过则进入下一阶段
</span></span><span style="display:flex;"><span>失败则进入 repair
</span></span></code></pre></div><p>这个设计的好处是让 Agent 不必一次性完成所有要求。M1 先保可玩，M2 补完整循环，M3 做表现，M4 做收尾。</p>
<h2 id="sdk-调用设计">SDK 调用设计</h2>
<p>控制中心通过 <code>SdkClient</code> 封装 <code>@opengame/sdk</code>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>TaskRunner
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>SdkClient.runTask({ prompt, cwd })
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>query({
</span></span><span style="display:flex;"><span>  prompt,
</span></span><span style="display:flex;"><span>  options: {
</span></span><span style="display:flex;"><span>    cwd,
</span></span><span style="display:flex;"><span>    permissionMode: &#34;yolo&#34;,
</span></span><span style="display:flex;"><span>    debug: true,
</span></span><span style="display:flex;"><span>    mcpServers: {
</span></span><span style="display:flex;"><span>      &#34;opengame-assets&#34;: assetServer
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>关键点有三个：</p>
<ol>
<li><code>cwd</code> 指向任务 workspace，Agent 的所有文件操作都限制在这个任务目录里。</li>
<li><code>permissionMode: &quot;yolo&quot;</code> 允许 Agent 在任务目录内执行必要命令，适合受控的服务器环境。</li>
<li>注入 <code>opengame-assets</code> MCP Server，让 Agent 可以调用 <code>generate_game_asset</code> 生成 PNG 游戏资源。</li>
</ol>
<p>TaskRunner 会把 SDK 的流式消息转成统一的 <code>TaskEvent</code>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>SDK stream message
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>mapSdkMessageToTaskEvents()
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>task.log / task.completed / task.failed
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>写入 events.jsonl
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>通过 SSE 推送到前端
</span></span></code></pre></div><p>为了避免任务卡死，TaskRunner 还做了两层超时：</p>
<ul>
<li>idle timeout：一段时间没有新消息，认为 SDK 卡住。</li>
<li>wall timeout：单次尝试总运行时间超限。</li>
</ul>
<p>对 API 中断和 timeout 这类可重试错误，最多尝试 3 次。</p>
<h2 id="实时日志设计">实时日志设计</h2>
<p>日志有两条路径：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>TaskEvent
</span></span><span style="display:flex;"><span>  ├── append events.jsonl       # 持久化
</span></span><span style="display:flex;"><span>  └── emitTaskEvent()           # 内存事件总线
</span></span><span style="display:flex;"><span>        ↓
</span></span><span style="display:flex;"><span>      /api/tasks/events         # 全局 SSE
</span></span><span style="display:flex;"><span>      /api/tasks/:id/events     # 单任务 SSE
</span></span><span style="display:flex;"><span>        ↓
</span></span><span style="display:flex;"><span>      TaskLogPanel / Workbench
</span></span></code></pre></div><p>为什么既要 JSONL，又要 SSE？</p>
<ul>
<li>SSE 负责实时体验。</li>
<li>JSONL 负责刷新后还能看到历史日志。</li>
<li>如果 SSE 推送失败，不应该影响任务执行，所以日志写入也是 best effort。</li>
</ul>
<h2 id="质量门禁">质量门禁</h2>
<p>一次 Agent 生成结束后，控制中心不会立刻标记成功，而是跑 gate。</p>
<h3 id="static-preview-gate">Static Preview Gate</h3>
<p>先检查是否能找到可预览入口：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>npm run build
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>resolvePreviewEntry()
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>index.html / dist/index.html / 其他可预览入口
</span></span></code></pre></div><p>如果找不到入口，任务失败，失败原因会归类为 <code>build_failed</code> 或相关类型。</p>
<h3 id="playable-gate">Playable Gate</h3>
<p>Playable Gate 会启动一个临时预览服务器，再用 Playwright 打开页面：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>startPreviewServer()
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Playwright chromium
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>监听页面错误、console error、资源 404
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>检查 Loading 是否消失
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>检查 canvas 是否空白
</span></span></code></pre></div><p>这一步解决的是“能构建但不能玩”的问题。</p>
<h3 id="visual-gate">Visual Gate</h3>
<p>Visual Gate 更偏游戏观感：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>打开预览页
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>截图
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>采样 canvas 像素
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>检查 skin-manifest.json
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>检查 PNG 是否存在、尺寸是否足够、是否被实际引用
</span></span></code></pre></div><p>它会拒绝几类结果：</p>
<ul>
<li>没有 canvas。</li>
<li>canvas 全空白。</li>
<li>canvas 颜色过于单一，看起来像占位图。</li>
<li>没有 <code>skin-manifest.json</code>。</li>
<li>manifest 里的资源缺失。</li>
<li>active asset 不是 PNG。</li>
<li>资源太小或未被游戏引用。</li>
</ul>
<h2 id="自动修复逻辑">自动修复逻辑</h2>
<p>如果 Gate 失败，控制中心不会马上结束，而是进入 repair：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Gate failed
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>buildOpenGameRepairPrompt(errorMessage, gddDraft, stage)
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Agent 在原 workspace 中修复
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>再次运行 Gate
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>最多修复 2 次
</span></span></code></pre></div><p>修复 prompt 的核心要求是：</p>
<ul>
<li>不要从零重写。</li>
<li>在现有 workspace 中做最小修复。</li>
<li>保证 preview entry 存在。</li>
<li>保证所有本地 script、style、image、skin asset 都存在。</li>
<li>如果是视觉质量失败，就补 PNG、改布局、修按钮反馈。</li>
<li>如果是缺资源，就优先创建缺失路径。</li>
</ul>
<p>对“预览 HTML 引用了不存在的本地资源”这种问题，还有专门的 targeted repair prompt，要求 Agent 先创建这些精确路径，避免越修越偏。</p>
<h2 id="资源生成-mcp">资源生成 MCP</h2>
<p>控制中心为 OpenGame SDK 注入了一个 <code>generate_game_asset</code> 工具：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Agent
</span></span><span style="display:flex;"><span>  ↓ call generate_game_asset
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Asset MCP Server
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Image Provider
</span></span><span style="display:flex;"><span>  ├── tongyi
</span></span><span style="display:flex;"><span>  ├── doubao
</span></span><span style="display:flex;"><span>  ├── openai-compat
</span></span><span style="display:flex;"><span>  └── n1n
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>写入 workspace 内 PNG 文件
</span></span></code></pre></div><p>工具入参包括：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;prompt&#34;</span>: <span style="color:#f1fa8c">&#34;space tower turret&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;outputPath&#34;</span>: <span style="color:#f1fa8c">&#34;public/skins/active/turret.png&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;size&#34;</span>: <span style="color:#f1fa8c">&#34;1024*1024&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;kind&#34;</span>: <span style="color:#f1fa8c">&#34;sprite&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;transparentBackground&#34;</span>: <span style="color:#ff79c6">true</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;styleGuide&#34;</span>: <span style="color:#f1fa8c">&#34;dark sci-fi pixel art&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;negativePrompt&#34;</span>: <span style="color:#f1fa8c">&#34;text, logo, watermark&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>如果图片 provider 超时、限流或临时失败，系统会用 sharp 生成一个 fallback PNG。这样任务不会因为图片服务偶发不可用而完全中断。</p>
<h2 id="版本链设计">版本链设计</h2>
<p>游戏生成成功后，它不只是一个 task，也是一个 game version。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>games
</span></span><span style="display:flex;"><span>└── gameId
</span></span><span style="display:flex;"><span>    ├── V1 taskId
</span></span><span style="display:flex;"><span>    ├── V2 taskId
</span></span><span style="display:flex;"><span>    └── V3 taskId
</span></span></code></pre></div><p>当用户基于某个版本继续迭代时：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>选择 V1
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>输入 changeNote
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>复制 V1 workspace 到 version-2
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>构造迭代 prompt
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>创建 V2 task
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>进入同一套队列与 Gate
</span></span></code></pre></div><p>这个设计比“覆盖式修改”稳很多。每次迭代都有完整产物、日志、质量报告和回退路径。</p>
<h2 id="换皮设计">换皮设计</h2>
<p>换皮不是让用户直接上传任意图片覆盖目录，而是依赖 <code>skin-manifest.json</code>。</p>
<p>manifest 结构大致是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;version&#34;</span>: <span style="color:#bd93f9">1</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;assets&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;key&#34;</span>: <span style="color:#f1fa8c">&#34;player&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;label&#34;</span>: <span style="color:#f1fa8c">&#34;玩家角色&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;path&#34;</span>: <span style="color:#f1fa8c">&#34;public/skins/active/player.png&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;placeholderPath&#34;</span>: <span style="color:#f1fa8c">&#34;public/skins/placeholders/player.png&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">&#34;mimeType&#34;</span>: <span style="color:#f1fa8c">&#34;image/png&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>控制中心可以：</p>
<ol>
<li>根据 manifest 导出占位资源 zip。</li>
<li>用户替换 zip 内同路径 PNG。</li>
<li>上传 zip。</li>
<li>后端复制当前 workspace。</li>
<li>只替换 manifest 中声明的资源。</li>
<li>重新 <code>npm run build</code>。</li>
<li>创建一个新的成功版本。</li>
</ol>
<p>换皮本质上也是版本迭代，只是它不调用大模型。</p>
<h2 id="失败分类">失败分类</h2>
<p>失败不能只存一段错误文本。控制中心会把失败归类为 <code>failureReason</code>，例如：</p>
<table>
  <thead>
      <tr>
          <th>类型</th>
          <th>含义</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>api_terminated</code></td>
          <td>上游 API 中断</td>
      </tr>
      <tr>
          <td><code>timeout</code></td>
          <td>SDK 或任务超时</td>
      </tr>
      <tr>
          <td><code>build_failed</code></td>
          <td>构建失败</td>
      </tr>
      <tr>
          <td><code>playable_check_failed</code></td>
          <td>浏览器可玩性检查失败</td>
      </tr>
      <tr>
          <td><code>visual_quality_failed</code></td>
          <td>视觉质量检查失败</td>
      </tr>
      <tr>
          <td><code>cancelled</code></td>
          <td>被取消</td>
      </tr>
      <tr>
          <td><code>recovered</code></td>
          <td>进程重启后恢复失败状态</td>
      </tr>
  </tbody>
</table>
<p>结构化失败原因有两个作用：</p>
<ul>
<li>UI 可以显示更友好的失败说明。</li>
<li>retry 可以判断从哪个阶段恢复，例如只重跑 gate，还是回到 planning。</li>
</ul>
<h2 id="进程重启恢复">进程重启恢复</h2>
<p>服务启动时会扫描所有 <code>running</code> 任务：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>server boot
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>recoverRunningTasks()
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>running → failed
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>failureReason = recovered
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>写入 task.failed 日志
</span></span></code></pre></div><p>这样做很朴素，但非常重要。因为 Node 进程重启后，内存里的队列已经没了，继续显示 <code>running</code> 会误导用户。把它们标记为可解释的失败状态，再允许用户 retry，是更可靠的选择。</p>
<h2 id="总体逻辑图">总体逻辑图</h2>
<p>完整控制中心可以这样理解：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│     React UI        │
</span></span><span style="display:flex;"><span>│ create/list/log/play│
</span></span><span style="display:flex;"><span>└─────────┬──────────┘
</span></span><span style="display:flex;"><span>          │ HTTP / SSE
</span></span><span style="display:flex;"><span>          ▼
</span></span><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│   Next.js API       │
</span></span><span style="display:flex;"><span>│ route handlers      │
</span></span><span style="display:flex;"><span>└─────────┬──────────┘
</span></span><span style="display:flex;"><span>          │
</span></span><span style="display:flex;"><span>          ▼
</span></span><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│   TaskService       │
</span></span><span style="display:flex;"><span>│ lifecycle director  │
</span></span><span style="display:flex;"><span>└──────┬───────┬──────┘
</span></span><span style="display:flex;"><span>       │       │
</span></span><span style="display:flex;"><span>       │       ├──────────────┐
</span></span><span style="display:flex;"><span>       ▼       ▼              ▼
</span></span><span style="display:flex;"><span>┌──────────┐ ┌──────────┐ ┌───────────────┐
</span></span><span style="display:flex;"><span>│ SQLite   │ │ JSONL Log│ │ Workspace FS  │
</span></span><span style="display:flex;"><span>└──────────┘ └──────────┘ └───────────────┘
</span></span><span style="display:flex;"><span>       │
</span></span><span style="display:flex;"><span>       ▼
</span></span><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│    TaskQueue        │
</span></span><span style="display:flex;"><span>└─────────┬──────────┘
</span></span><span style="display:flex;"><span>          ▼
</span></span><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│    TaskRunner       │
</span></span><span style="display:flex;"><span>└─────────┬──────────┘
</span></span><span style="display:flex;"><span>          ▼
</span></span><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│  @opengame/sdk      │
</span></span><span style="display:flex;"><span>│  + asset MCP        │
</span></span><span style="display:flex;"><span>└─────────┬──────────┘
</span></span><span style="display:flex;"><span>          ▼
</span></span><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│ Generated Web Game  │
</span></span><span style="display:flex;"><span>└─────────┬──────────┘
</span></span><span style="display:flex;"><span>          ▼
</span></span><span style="display:flex;"><span>┌────────────────────┐
</span></span><span style="display:flex;"><span>│ Playable/Visual Gate│
</span></span><span style="display:flex;"><span>└──────┬────────┬─────┘
</span></span><span style="display:flex;"><span>       │        │
</span></span><span style="display:flex;"><span>       ▼        ▼
</span></span><span style="display:flex;"><span>  succeeded   repair / failed
</span></span></code></pre></div><h2 id="设计取舍">设计取舍</h2>
<h3 id="1-串行队列而不是并发队列">1. 串行队列，而不是并发队列</h3>
<p>当前实现使用单队列串行执行。这样吞吐低一些，但更稳：</p>
<ul>
<li>避免多个 Agent 同时抢 CPU、网络和图片 provider。</li>
<li>避免 Playwright 与构建进程把服务器打满。</li>
<li>更容易定位日志和失败原因。</li>
</ul>
<p>等单任务链路稳定后，再考虑按资源配额做并发。</p>
<h3 id="2-本地-sqlite而不是一开始上-postgresql">2. 本地 SQLite，而不是一开始上 PostgreSQL</h3>
<p>控制中心首先是单机工具，SQLite 足够简单可靠。任务元数据放 SQLite，产物放文件系统，日志放 JSONL，正好符合静态产物生成场景。</p>
<p>如果后续要多人使用、分布式队列、多机执行，再迁到 PostgreSQL + Redis Queue 会更合适。</p>
<h3 id="3-gate-失败自动修复但限制次数">3. Gate 失败自动修复，但限制次数</h3>
<p>自动修复有价值，但不能无限循环。当前最多修复 2 次。这样可以覆盖资源缺失、构建小错误、空白画面等常见问题，同时避免 Agent 在错误方向上无限消耗。</p>
<h3 id="4-半自动-gdd-审核是必要入口">4. 半自动 GDD 审核是必要入口</h3>
<p>全自动适合快速试验，但复杂游戏最好先审 GDD。GDD 是后续 M1-M4 的源头，如果需求理解错了，后面生成得越完整，返工越重。</p>
<h2 id="小结">小结</h2>
<p>OpenGame Web 控制中心真正解决的不是“给命令行加一个网页按钮”，而是把 AI 生成游戏这件事产品化：</p>
<ul>
<li>用任务队列承接长耗时生成。</li>
<li>用 GDD 把模糊需求变成工程计划。</li>
<li>用 M1-M4 把一次性生成拆成可控阶段。</li>
<li>用 Playwright 和视觉检查把“可玩”变成门禁。</li>
<li>用 repair prompt 把失败转成下一轮修复输入。</li>
<li>用版本链和换皮把一次生成变成可持续迭代。</li>
</ul>
<p>这套设计也可以迁移到其他 Agent 应用：只要任务耗时长、结果需要验收、失败需要自动修复，就应该把 Agent 调用包进类似的控制中心，而不是停留在一次性脚本调用。</p>
]]></content:encoded></item><item><title>OpenGame 技术文档：从自然语言生成可玩的 Web 游戏</title><link>https://blog.heyaohua.com/posts/2026/05/opengame-technical-guide/</link><pubDate>Wed, 06 May 2026 15:10:00 +0800</pubDate><guid>https://blog.heyaohua.com/posts/2026/05/opengame-technical-guide/</guid><description>OpenGame 是 CUHK MMLab 发布的开源智能体游戏开发框架，面向从自然语言提示词端到端生成可运行 Web 游戏。本文整理它的架构、安装方式、配置项、运行流程、适用场景与局限。</description><content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p><a href="https://www.opengame-project-page.com/">OpenGame</a> 是 CUHK MMLab 在 2026 年 4 月发布的开源智能体框架，目标是让用户用一段自然语言描述生成一个可运行、可交互的 Web 游戏。项目论文为 <a href="https://arxiv.org/abs/2604.18394">OpenGame: Open Agentic Coding for Games</a>，官方代码仓库在 <a href="https://github.com/leigest519/OpenGame">leigest519/OpenGame</a>。</p>
<p>它解决的问题不是“让大模型写一段游戏代码”，而是让 Agent 完成一整个游戏工程：选择技术模板、生成多个文件、维护游戏状态、处理资源、启动构建、检查运行错误，并反复修复到可玩状态。这个目标比普通代码生成更难，因为游戏开发同时依赖实时循环、输入系统、碰撞逻辑、场景连接、资源加载和 UI 状态。</p>
<p>截至 2026-05-06，OpenGame 已经公开框架代码和演示项目，npm release 仍在准备中，官方推荐从源码安装。</p>
<h2 id="结论">结论</h2>
<p>OpenGame 的核心价值可以概括为三点：</p>
<ol>
<li>它把“游戏生成”从单次代码补全变成了端到端 Agent 流程。</li>
<li>它用 Game Skill 管理模板选择和调试经验，降低跨文件工程崩坏的概率。</li>
<li>它用 OpenGame-Bench 的思路把可玩性纳入评估，而不是只看代码能否编译。</li>
</ol>
<p>如果你想快速生成 Web 小游戏原型、验证玩法、做 AI 生成式游戏开发实验，OpenGame 值得关注。如果你要生产级商业游戏，它更适合作为原型工具和研究框架，而不是直接替代完整游戏团队。</p>
<h2 id="系统架构">系统架构</h2>
<p>OpenGame 可以拆成四层来看：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>用户提示词
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>OpenGame CLI / Agent Runtime
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Game Skill
</span></span><span style="display:flex;"><span>  ├── Template Skill：选择 Canvas、Phaser、Three.js 等项目模板
</span></span><span style="display:flex;"><span>  └── Debug Skill：运行、检测、定位、修复构建和交互问题
</span></span><span style="display:flex;"><span>  ↓
</span></span><span style="display:flex;"><span>Web Game Project
</span></span><span style="display:flex;"><span>  ├── 源码
</span></span><span style="display:flex;"><span>  ├── 资源
</span></span><span style="display:flex;"><span>  ├── 配置
</span></span><span style="display:flex;"><span>  └── 可运行页面
</span></span></code></pre></div><h3 id="cli-与-agent-runtime">CLI 与 Agent Runtime</h3>
<p>OpenGame 当前主要通过命令行运行。用户传入一个 prompt，Agent 在目标目录中生成游戏项目，并在必要时修改文件、修复错误、输出运行方式。</p>
<p>官方示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>opengame -p <span style="color:#f1fa8c">&#34;Build a Snake clone with WASD controls and a dark theme.&#34;</span> --yolo
</span></span></code></pre></div><p>这里的 <code>--yolo</code> 允许 Agent 执行 shell 命令。普通试验环境可以使用，但如果目录中有重要文件，建议先在空目录或临时目录中运行。</p>
<h3 id="game-skill">Game Skill</h3>
<p>Game Skill 是 OpenGame 的关键设计。它不是一个单独的模型，而是一套可复用、可演进的 Agent 能力，主要包含两部分。</p>
<p>Template Skill 负责项目骨架。它会根据需求选择合适的游戏技术栈，例如基础 Canvas、Phaser 或 Three.js，并生成相对稳定的项目结构。这样可以减少模型从零创建工程时常见的目录混乱、入口文件缺失、资源路径错误等问题。</p>
<p>Debug Skill 负责验证和修复。游戏生成后，它会通过构建、运行、浏览器执行、控制台错误和交互状态来发现问题，再把修复经验沉淀成可复用协议。这个思路比只修语法错误更重要，因为很多游戏问题是“能编译但不能玩”。</p>
<h3 id="模型使用现状">模型使用现状</h3>
<p>OpenGame 论文中提到过 GameCoder-27B：一个面向游戏开发训练的代码模型。论文里的训练路径包括：</p>
<ol>
<li>持续预训练，让模型吸收游戏开发代码和引擎知识。</li>
<li>监督微调，让模型学习项目搭建、API 使用和错误修复轨迹。</li>
<li>基于执行反馈的强化学习，把实际可玩性作为奖励信号的一部分。</li>
</ol>
<p>但从当前公开资料看，截至 2026-05-06，我没有找到可直接下载使用的 GameCoder-27B 权重、模型仓库或稳定推理入口。也就是说，它更像论文中的专用模型方向和实验设定，不应该在实操文档里写成“用户现在可以直接安装使用”的组件。</p>
<p>实际落地时，OpenGame 主要依赖 OpenAI-compatible API 后面的通用大模型能力。你可以把它理解成：</p>
<ul>
<li>OpenGame 提供 Agent 流程、项目模板、调试协议和游戏生成工具链。</li>
<li>具体写代码、改代码、理解需求的能力来自你配置的大模型。</li>
<li>当前可操作的关键不是下载 GameCoder-27B，而是选择一个足够强、支持长上下文、代码能力稳定的 OpenAI-compatible 模型。</li>
</ul>
<p>因此在本地试用时，优先把 <code>OPENAI_API_KEY</code>、<code>OPENAI_BASE_URL</code> 和 <code>OPENAI_MODEL</code> 配好，用现有大模型跑通流程；等官方后续公开专用模型或推理服务后，再考虑切换。</p>
<h3 id="opengame-bench">OpenGame-Bench</h3>
<p>OpenGame-Bench 是论文中用于评估游戏生成 Agent 的 benchmark。它关注三个维度：</p>
<table>
  <thead>
      <tr>
          <th>维度</th>
          <th>关注点</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build Health</td>
          <td>项目是否能安装、构建、启动</td>
      </tr>
      <tr>
          <td>Visual Usability</td>
          <td>画面、UI、交互元素是否正常渲染</td>
      </tr>
      <tr>
          <td>Intent Alignment</td>
          <td>最终玩法是否符合用户最初描述</td>
      </tr>
  </tbody>
</table>
<p>这个评估方向很关键。普通代码 benchmark 通常看测试是否通过，但游戏是交互式应用，真正的问题经常出现在浏览器运行、输入响应、资源加载、状态推进和胜负条件上。</p>
<h2 id="安装">安装</h2>
<p>OpenGame 当前推荐从源码安装。</p>
<h3 id="环境要求">环境要求</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>node --version
</span></span></code></pre></div><p>建议使用 Node.js 20 或更高版本。</p>
<h3 id="从源码安装">从源码安装</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone https://github.com/leigest519/OpenGame.git
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">cd</span> OpenGame
</span></span><span style="display:flex;"><span>npm install
</span></span><span style="display:flex;"><span>npm run build
</span></span><span style="display:flex;"><span>npm link
</span></span></code></pre></div><p>安装完成后，系统 PATH 中会出现 <code>opengame</code> 命令：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>opengame --help
</span></span></code></pre></div><h2 id="配置">配置</h2>
<p>OpenGame 的 Agent Runtime 支持 OpenAI-compatible API，可以通过环境变量配置：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">export</span> <span style="color:#8be9fd;font-style:italic">OPENAI_API_KEY</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;your-api-key-here&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">export</span> <span style="color:#8be9fd;font-style:italic">OPENAI_BASE_URL</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;https://api.openai.com/v1&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">export</span> <span style="color:#8be9fd;font-style:italic">OPENAI_MODEL</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;gpt-4o&#34;</span>
</span></span></code></pre></div><p>如果你使用 OpenRouter、本地模型服务或兼容 OpenAI API 的推理网关，把 <code>OPENAI_BASE_URL</code> 和 <code>OPENAI_MODEL</code> 改成对应值即可。</p>
<h3 id="资源生成配置">资源生成配置</h3>
<p>除了主推理模型，OpenGame 还可以对接图片、音频、视频和推理类 provider。官方 README 中提到的配置方式类似：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">export</span> <span style="color:#8be9fd;font-style:italic">OPENGAME_IMAGE_PROVIDER</span><span style="color:#ff79c6">=</span>tongyi
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">export</span> <span style="color:#8be9fd;font-style:italic">OPENGAME_IMAGE_API_KEY</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;sk-...&#34;</span>
</span></span></code></pre></div><p>不同模态可以分别配置 provider。这样做的好处是：游戏逻辑可以用一个模型生成，图片或音频资源可以交给更适合的服务生成。</p>
<h3 id="设置文件">设置文件</h3>
<p>OpenGame 支持通过环境变量、CLI 参数和 settings 文件配置。当前仓库中仍保留 <code>.qwen/settings.json</code> 这类路径命名，这是继承上游 Agent Runtime 的兼容设计，官方说明后续可能迁移到 <code>.opengame</code>。</p>
<h2 id="快速运行">快速运行</h2>
<p>建议先在空目录中试验：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir -p ~/agent-test/games/snake-demo
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">cd</span> ~/agent-test/games/snake-demo
</span></span><span style="display:flex;"><span>opengame -p <span style="color:#f1fa8c">&#34;Create a dark-themed snake game controlled by WASD. Add score, pause, restart, and increasing speed.&#34;</span> --yolo
</span></span></code></pre></div><p>运行完成后，根据命令行输出打开生成的 <code>index.html</code>，或者执行项目中给出的 dev server 命令。官方 demo 的本地运行方式通常是：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install
</span></span><span style="display:flex;"><span>npm run dev
</span></span></code></pre></div><p>然后访问：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>http://localhost:5173
</span></span></code></pre></div><h2 id="prompt-编写建议">Prompt 编写建议</h2>
<p>OpenGame 的输入越像产品需求文档，成功率越高。建议 prompt 至少包含以下内容：</p>
<table>
  <thead>
      <tr>
          <th>类型</th>
          <th>示例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>游戏类型</td>
          <td>横版动作、塔防、卡牌、答题格斗、双摇杆射击</td>
      </tr>
      <tr>
          <td>核心循环</td>
          <td>收集资源、建造防御、击败一波敌人、进入下一关</td>
      </tr>
      <tr>
          <td>输入方式</td>
          <td>WASD、方向键、鼠标点击、双人键盘</td>
      </tr>
      <tr>
          <td>关卡规则</td>
          <td>三个关卡、每关一个 Boss、失败后可重新开始</td>
      </tr>
      <tr>
          <td>UI 要素</td>
          <td>血条、分数、技能冷却、暂停按钮、结算面板</td>
      </tr>
      <tr>
          <td>美术风格</td>
          <td>16-bit pixel art、暗黑科幻、手绘、低多边形</td>
      </tr>
      <tr>
          <td>验收标准</td>
          <td>能开始、能移动、能攻击、能失败、能胜利、能重开</td>
      </tr>
  </tbody>
</table>
<p>一个更工程化的 prompt 示例：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Create a Phaser web game: a top-down survival shooter.
</span></span><span style="display:flex;"><span>The player moves with WASD and aims with mouse.
</span></span><span style="display:flex;"><span>Enemies spawn in waves every 20 seconds.
</span></span><span style="display:flex;"><span>Add health, score, pause, restart, and a victory screen after wave 5.
</span></span><span style="display:flex;"><span>Use dark sci-fi pixel art style.
</span></span><span style="display:flex;"><span>Acceptance criteria: the game must start from a menu, the player can move and shoot, enemies can damage the player, defeated enemies increase score, and the restart button resets all state.
</span></span></code></pre></div><h2 id="工程实践">工程实践</h2>
<h3 id="先生成小原型">先生成小原型</h3>
<p>不要一开始就要求开放世界、复杂经济系统、多人同步和长剧情。更好的做法是先生成一个可玩的核心循环，再逐步扩展关卡、资源、角色和数值。</p>
<h3 id="把验收标准写进-prompt">把验收标准写进 prompt</h3>
<p>游戏生成最大的坑不是“代码无法运行”，而是“运行了但不像你要的游戏”。把可验证条件写清楚，例如：</p>
<ul>
<li>开始菜单必须能进入游戏。</li>
<li>玩家死亡后必须出现结算页。</li>
<li>重新开始必须清空敌人、分数和计时器。</li>
<li>移动端不要求支持，或者明确要求支持触控。</li>
</ul>
<h3 id="保留生成记录">保留生成记录</h3>
<p>每次生成后建议保存：</p>
<ul>
<li>原始 prompt</li>
<li>生成的源码</li>
<li>运行截图</li>
<li>控制台错误</li>
<li>修改记录</li>
</ul>
<p>这样后续可以比较不同 prompt、模型和 provider 的效果，也方便把成功经验沉淀为模板。</p>
<h3 id="注意版权边界">注意版权边界</h3>
<p>官方 demo 中展示了很多知名 IP 风格的游戏示例。自己实验时可以用“类似 90 年代街机像素风”“科幻赏金猎人”等描述来表达风格，避免直接把受版权保护的角色、名称、剧情和素材用于公开发布或商业项目。</p>
<h2 id="适用场景">适用场景</h2>
<p>OpenGame 适合：</p>
<ul>
<li>快速做 Web 游戏原型</li>
<li>测试玩法创意</li>
<li>研究代码 Agent 如何处理复杂交互项目</li>
<li>构建游戏生成 benchmark</li>
<li>给 Phaser、Canvas、Three.js 项目生成初始骨架</li>
<li>教学场景中演示从需求到可运行应用的完整链路</li>
</ul>
<p>暂时不适合：</p>
<ul>
<li>直接生成大型商业游戏</li>
<li>对物理、联网、性能和资产管线要求极高的项目</li>
<li>强版权 IP 的公开复刻</li>
<li>没有人工验收的自动上线流程</li>
</ul>
<h2 id="常见问题">常见问题</h2>
<h3 id="1-opengame-是-no-code-工具吗">1. OpenGame 是 no-code 工具吗？</h3>
<p>不是。它更像“代码生成 Agent + 游戏开发技能 + 自动调试流程”。最终产物仍然是可编辑的 Web 游戏源码。</p>
<h3 id="2-现在能直接使用-gamecoder-27b-吗">2. 现在能直接使用 GameCoder-27B 吗？</h3>
<p>目前不建议把 GameCoder-27B 当作可直接下载部署的依赖来规划。公开资料里能看到论文对它的描述，但我没有找到稳定可用的模型权重或下载入口。实际使用 OpenGame 时，重点是配置 OpenAI-compatible API，让 OpenGame 调用你选择的大模型完成代码生成和调试。</p>
<h3 id="3-为什么强调-web-游戏">3. 为什么强调 Web 游戏？</h3>
<p>Web 游戏更容易自动构建、启动和用 headless browser 验证。相比 Unity 或 Unreal，浏览器环境更适合 Agent 做端到端执行反馈。</p>
<h3 id="4---yolo-安全吗">4. <code>--yolo</code> 安全吗？</h3>
<p><code>--yolo</code> 会让 Agent 执行 shell 命令。建议只在隔离目录、临时目录或容器中使用，不要在含有重要文件的目录里直接运行。</p>
<h3 id="5-生成的游戏能不能商用">5. 生成的游戏能不能商用？</h3>
<p>需要分别检查代码许可证、生成素材来源、模型服务条款，以及 prompt 中是否包含受版权保护的 IP。OpenGame 仓库本身使用 Apache-2.0 license，但生成内容的权利边界还要看具体素材和模型服务。</p>
<h2 id="小结">小结</h2>
<p>OpenGame 的意义不只是“AI 写小游戏”。它把智能体软件工程推进到一个更难的交互式场景：多文件、一致状态、实时循环、视觉反馈和用户输入必须同时成立。</p>
<p>如果说普通代码 Agent 的验收标准是“能不能编译、测试能不能过”，OpenGame 这类框架的验收标准更接近真实应用：能不能打开、能不能玩、是否符合设计意图。这个方向对游戏开发有价值，对更广义的前端应用、仿真工具和交互式产品生成也有启发。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://www.opengame-project-page.com/">OpenGame Project Page</a></li>
<li><a href="https://github.com/leigest519/OpenGame">GitHub: leigest519/OpenGame</a></li>
<li><a href="https://arxiv.org/abs/2604.18394">arXiv: OpenGame: Open Agentic Coding for Games</a></li>
<li><a href="https://huggingface.co/papers/2604.18394">Hugging Face Papers: OpenGame</a></li>
</ul>
]]></content:encoded></item></channel></rss>