# 上传与发布

完整游戏包通过 MCP 上传，站主后台支持管理资料、封面、预览和发布。以下流程使用[已授权的 MCP 客户端](/docs/mcp)。JSON 是对应工具的参数；UUID、修订号与本机路径需要替换为真实返回值。

## 准备发布文件

在游戏项目内使用自己的 Node.js / pnpm 等工具完成构建，得到包含入口 HTML 和完整资源的目录，也可以打包为 ZIP。独立 MCP 客户端使用 Node.js 24.x 或 26.x，已包含运行依赖，不要求游戏项目降级或使用平台的 pnpm 版本。入口默认为 `index.html`；ZIP 根目录应直接包含该入口，或显式指定实际入口路径。相对资源路径需保持完整。

平台读取并上传实际文件，不运行构建脚本、不解释远端客户端的本地路径。先完成[游戏适配器接入](/docs/game-integration)，再提交发布包。

| 项目 | 当前限制 |
| --- | --- |
| ZIP 压缩包 | 100 MiB |
| 展开后总量 | 500 MiB |
| 文件数 | 10000 |
| 单文件 | 64 MiB |
| 封面 | PNG、JPEG、WebP、AVIF；500 KB、1600 万像素以内；公开访问使用 640px WebP 缩略图 |

封面上限为 512000 字节（500 × 1024）。平台保留上传原图，公开页面使用最长边不超过 640px 的 WebP 缩略图；浏览器缓存通过 ETag 重新验证，下架或替换封面后旧地址停止公开访问。

拒绝路径穿越、符号链接、歧义重名、损坏 ZIP 和内部保留路径。上传前确认包里没有 `.env`、私钥、授权凭据或开发环境配置。

## 创建游戏工作稿

调用 `create_game`。`slug` 为 1–80 个小写字母或数字及单个分隔连字符；用作稳定访问标识。分类 ID 必须来自已发布站点分类，尚无分类时可传空数组。

```json
{
  "input": {
    "slug": "my-web-game",
    "snapshot": {
      "title": "我的网页游戏",
      "summary": "一句话介绍玩法。",
      "description": "游戏介绍。",
      "instructions": "点击开始，使用方向键移动。",
      "devices": ["desktop", "mobile"],
      "categories": [],
      "coverId": null
    }
  },
  "idempotencyKey": "my-web-game-create-001"
}
```

保存写入结果中的 `data.gameId`、`data.draftRevision` 和 `data.stateRevision`。只声明实际支持的设备；更新现有游戏时先用 `get_game` 获取工作稿和修订号，再调用 `save_game_draft`，不要反复创建同一 slug。

## 上传游戏文件

调用 `submit_game_files`。它直接接收参数，不使用 `input` 包装。下面假设初始工作稿修订仍为 0；如果已修改资料或封面，需要使用最新修订。

```json
{
  "gameId": "11111111-1111-4111-8111-111111111111",
  "expectedDraftRevision": 0,
  "localPath": "/ABSOLUTE/PATH/my-game/dist",
  "entry": "index.html",
  "idempotencyKey": "my-web-game-files-001"
}
```

`localPath` 也可指向 ZIP。成功完成后保存 `data.draftRevision`。上传会更新工作稿修订，不会自动生成候选或发布。文件传输中断且内容未变化时可用同一幂等键重试；若包内容改变或上传会话已失效，应重新读取修订并开始新上传，使用新键。

封面可在[站主后台](/admin)上传。上传封面也会更新工作稿修订；之后创建候选时再次读取最新修订。HTTP 上传方式见[平台 API](/docs/api)。

## 冻结候选并检查预览

调用 `create_game_preview`，将当前资料与文件冻结为不可变候选。假设上传后修订为 1：

```json
{
  "input": {
    "gameId": "11111111-1111-4111-8111-111111111111",
    "expectedDraftRevision": 1
  },
  "idempotencyKey": "my-web-game-preview-001"
}
```

结果包含 `data.candidateId`、`data.baseStateRevision` 和 `data.previewPath`。站主在正式平台打开返回的预览路径，核对资料差异、入口、资源与实际玩法。私有文件预览需要有效网页会话，最长 15 分钟；分享资源 URL 不会授予预览权限。

候选创建后再改工作稿，不会改变已经冻结的候选。若检查后修改了文件或资料，需要重新创建并检查新候选；发布时明确使用自己检查过的候选 ID。

## 明确发布

确认候选后调用 `publish_game`，使用候选返回的 `baseStateRevision` 作为预期状态修订。下面的 0 只是首次发布示例：

```json
{
  "input": {
    "gameId": "11111111-1111-4111-8111-111111111111",
    "candidateId": "22222222-2222-4222-8222-222222222222",
    "expectedStateRevision": 0
  },
  "idempotencyKey": "my-web-game-publish-001"
}
```

发布成功后检查 `/games/my-web-game` 和 `/play/my-web-game`，验证外壳内的身份、侧栏、防 Boss、音频、移动端和本机存档。首次正式游戏还需要验证独立游戏子域的 HTTPS 访问。若游戏此前已下架，更新或回退不会自动恢复上架，需要明确调用 `set_game_availability`。

## 修改、下架和回退

- 修改游戏：`get_game` → `save_game_draft` 或上传新文件 → 创建新候选 → 检查 → 发布。
- 下架或恢复：`set_game_availability`，提供 `available` 与最新 `expectedStateRevision`。
- 历史回退：`rollback_game`，只能选择曾经正式发布的 `versionId`；它不会恢复下架状态。
- 站点介绍、分类、精选和排序：`get_site_config` → `save_site_draft` → `create_site_preview` → `publish_site_config`，使用独立的站点修订与候选。

遇到 `REVISION_CONFLICT`，先重新读取并检查其他连接的新改动，重新生成候选，不盲目替换修订号后重发发布。相同连接、相同幂等键和相同输入会返回原成功结果；不同请求复用同键会产生 `IDEMPOTENCY_CONFLICT`。

## 授权边界

普通玩家不能上传和发布。多个 agent 可以由同一站主授权为不同管理连接，各自保存凭据、独立撤销；也可以由主管理员在[后台账号管理](/admin/accounts)将已验证邮箱的玩家添加为站主，各站主使用自己的账号和独立 MCP 连接。站主可以管理全站内容，操作记录显示实际操作账号及网页或 MCP 来源。上传、生成候选和正式发布是分开的步骤，给 agent 安排任务时请明确需要做到哪一步。
