# 平台 API

正式基础地址为 `https://scalia.ltd`，管理路径为 `/api/v1/manage`。本文覆盖现有管理 HTTP 接口和健康检查；[OpenAPI JSON](/docs/openapi.json) 包含从当前代码生成的动作 input 结构和权限。

## 认证与适用范围

推荐通过[本地 MCP 客户端](/docs/mcp)完成站主浏览器授权、PKCE、回调校验、令牌刷新和本机文件读取。HTTP 管理客户端使用授权后取得的 `Authorization: Bearer <access_token>`。访问令牌、刷新令牌、网站密码和 SMTP 授权码不可互换。

当前未提供公共远程 MCP URL 或通用 API Key 签发页面。独立客户端可从 [MCP 接入文档](/docs/mcp)下载安装，无需完整平台源码。需要自定义 HTTP 客户端时，应复用客户端授权实现；不要把管理令牌放到公开游戏 JavaScript。浏览器后台使用自己的同源会话与 CSRF 校验，不能照搬 Bearer 示例到公开网页。

平台玩家账号、收藏、评论和启动接口用于平台本身的网页流程；游戏接入使用[适配器协议](/docs/game-integration)，本版不提供可交给游戏后端的统一身份令牌或云存档 API。

## HTTP 入口

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/v1/health` | 公开健康检查，正常 200，降级 503 |
| GET | `/api/v1/manage/status` | 验证管理连接；需要有效 Bearer |
| POST | `/api/v1/manage` | 执行 action 指定的管理动作 |
| PUT | `/api/v1/manage/uploads/{uploadId}?path=...` | 单文件二进制上传，需要 manage:draft |

文档、Markdown、[游戏适配器](/youxi-game-v1.js)和 OpenAPI 可匿名读取。管理接口会持续检查站主、连接、权限和账号状态。请求体 JSON 最大 128 KiB。

## 请求与返回

只读示例：使用已有授权令牌，在内存中设置环境变量 `YOUXI_ACCESS_TOKEN`，不要把实际值写进公开脚本或聊天。调用概览不修改数据：

```sh
curl --fail-with-body https://scalia.ltd/api/v1/manage \
  -H "Authorization: Bearer $YOUXI_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"action":"get_overview","input":{}}'
```

写操作还必须提供 `requestId` 与 `idempotencyKey`。请求结构如下；其中 UUID 是格式示例，需要替换为真实目标：

```json
{
  "action": "create_game_preview",
  "input": {
    "gameId": "11111111-1111-4111-8111-111111111111",
    "expectedDraftRevision": 1
  },
  "requestId": "22222222-2222-4222-8222-222222222222",
  "idempotencyKey": "my-web-game-preview-001"
}
```

只读动作直接返回业务数据。写操作成功返回以下信封；业务数据读取 `data`，不要给只读结果强套同一信封：

```json
{
  "requestId": "22222222-2222-4222-8222-222222222222",
  "action": "create_game_preview",
  "target": "11111111-1111-4111-8111-111111111111",
  "source": "mcp",
  "result": "ok",
  "data": {
    "gameId": "11111111-1111-4111-8111-111111111111",
    "candidateId": "33333333-3333-4333-8333-333333333333",
    "baseStateRevision": 0,
    "draftRevision": 1,
    "previewPath": "/admin/preview/33333333-3333-4333-8333-333333333333"
  }
}
```

此处 `data` 只展示关键字段；候选还返回资料快照等字段。游戏查询 `get_game` 返回游戏状态、`draft.revision` 与历史 `versions`；创建和上传返回的 `draftRevision` 用于下一次修改，候选的 `baseStateRevision` 用于发布前的状态检查。

## 权限与动作

| 权限 | 只读动作 | 写动作 |
| --- | --- | --- |
| `manage:read` | get_overview、get_analytics、list_games、get_game、get_site_config、list_operations | 无 |
| `manage:draft` | get_upload | create_game、save_game_draft、create_game_preview、save_site_draft、create_site_preview、begin_upload、finish_upload |
| `manage:publish` | 无 | publish_game、set_game_availability、rollback_game、publish_site_config、rollback_site_config |
| `manage:accounts` | find_accounts、list_recent_accounts、list_owners、list_connections、list_comments | set_account_status、revoke_connection、moderate_comment |

另有 `add_owner` / `remove_owner` 两个 HTTP 动作，只允许主管理员的有效网页会话通过同源请求与 CSRF 校验执行，不能使用 Bearer 令牌，也不暴露为 MCP 工具。请在[后台账号管理](/admin/accounts)操作：目标账号需先注册并验证邮箱，再查询并确认添加。只有主管理员能停用或恢复其他站主；主管理员本身不能被停用或移除。移除站主会撤销其所有旧连接和私有预览，重新添加不会使旧授权恢复。`list_connections` / `revoke_connection` 仅能管理当前账号自己的连接。

以上共 29 个 HTTP 管理动作，MCP 暴露其中 27 个。MCP 另外提供 `platform_login`、`platform_status` 和 `submit_game_files` 三个客户端工具；`submit_game_files` 在本机读取目录或 ZIP 后调用上传接口，不是 HTTP action。

工作台只读查询 `get_analytics` 的 input 为 `{"days":7}`；days 仅支持 1、7、30，默认 7，按北京时间统计（含今日）。返回账号汇总 summary、窗口去重汇总 period、每日趋势 trends、前 50 款游戏排行 games、采集起始时间 startedAt 和更新时间 generatedAt。注册用户按账号、游客按匿名浏览器标识去重；同日游客登录后活动合并到账号。游戏成功就绪后才确认启动，重复确认不累计；上线前访问与游玩不补算。`list_recent_accounts` 的 input 为 `{}`，返回最近 10 个账号的邮箱、昵称、状态、注册时间与最近前台访问时间，仅供站主管理，要求 `manage:accounts`。工作台入口为[后台工作台](/admin)。

网页 API 页面下方提供每个动作的 input JSON Schema；同样的结构可在 [OpenAPI JSON](/docs/openapi.json) 的 POST 管理入口获取。跨字段唯一性、已发布引用、文件与业务状态等额外约束由服务器运行时检查。

## 游戏评论

游客可用 `GET /api/v1/games/{gameId}/comments?limit=20&cursor=...` 读取上架游戏评论。返回 `items` 和 `nextCursor`，最新在前；limit 为 1–50，游标原样传回，不跨游戏复用。公开结果只包含作者 ID、昵称与预设头像，不含邮箱或举报人信息。

已登录、邮箱已验证且未停用的玩家可使用以下站内接口；写请求都需要同源 Origin、JSON、`X-Youxi-Request: 1` 和当前会话 `X-Youxi-Csrf`，不接受管理 Bearer：

- POST 上述路径，JSON 为 `{"requestId":"UUID","body":"评论内容"}`；纯文字 1–1000 字，每 30 秒一条、24 小时最多 50 条。重试保持相同请求标识和内容。
- DELETE `/api/v1/games/{gameId}/comments/{commentId}`，仅限删除自己的评论。删除不可恢复，不返还发表额度。
- POST `/api/v1/games/{gameId}/comments/{commentId}/report`，JSON 为 `{"reason":"spam"}`，reason 还支持 abuse、other。同一账号对每条评论只能举报一次，每 24 小时最多 20 次，不能举报自己。

站主通过 `list_comments` 查询评论。input 支持 filter（reported 默认、visible、hidden、all）、可选 gameId、cursor、limit；返回 items 和 nextCursor。每条包含 revision、hiddenAt、pendingReports、reasons 与游戏、作者公开资料。用户已删除的内容不再返回。

`moderate_comment` 的 input 为 `{"commentId":"UUID","expectedRevision":0,"decision":"hide"}`，decision 支持 hide、restore、dismiss_reports。隐藏及忽略会处理当前待处理举报；恢复不撤销用户删除。新举报会推进修订号，发生 REVISION_CONFLICT 时刷新列表再核对。两个管理动作需要 manage:accounts，写入进入操作记录；举报本身不会自动隐藏。入口为[后台评论管理](/admin/comments)。

## 幂等与并发

`requestId` 使用 UUID，写入的 `idempotencyKey` 为 8–128 个 ASCII 字母、数字、下划线或连字符。对同一业务操作进行网络重试时保持相同键与相同 input；新业务请求换新键。同一主体下复用键却改变请求会返回 `IDEMPOTENCY_CONFLICT`。

工作稿修改使用 `expectedDraftRevision`，发布、上下架和回退使用 `expectedStateRevision`，账号停用与站主增删使用 `expectedAccountRevision`。修订冲突时先重新读取并检查差异，避免覆盖其他连接操作。候选不可变，发布指定候选；完整流程见[上传与发布](/docs/publishing)。

## 文件上传

1. POST 管理动作 `begin_upload`，传入 gameId、expectedDraftRevision、kind 和 entry；保存 `data.uploadId`。kind 为 game 或 cover，游戏 entry 必须是 HTML。
2. 对每个文件 PUT `/api/v1/manage/uploads/{uploadId}?path=包内路径`，查询路径进行 URL 编码。请求体为原始字节，Content-Type 为 `application/octet-stream`，并设置 `X-Youxi-Digest` 为文件内容 SHA-256 的 64 位小写十六进制摘要。
3. POST 动作 `finish_upload`，传入 uploadId，完成校验与工作稿关联，保存新的 `data.draftRevision`。
4. 调用 `create_game_preview` 后检查候选，再调用 `publish_game`。上传本身不会发布。

```json
{
  "action": "begin_upload",
  "input": {
    "gameId": "11111111-1111-4111-8111-111111111111",
    "expectedDraftRevision": 0,
    "kind": "game",
    "entry": "index.html"
  },
  "requestId": "22222222-2222-4222-8222-222222222222",
  "idempotencyKey": "my-web-game-upload-begin-001"
}
```

相同路径与相同摘要可重复 PUT，内容或大小不符会拒绝。HTTP 上传逐文件发送；ZIP 的展开由本地 MCP 客户端完成。单文件、总量、文件数和封面限制见[上传与发布](/docs/publishing)。

## 错误处理

失败使用非 2xx 状态并返回 `code`；若请求带有效 requestId，错误通常也会返回 requestId 和 retryable。修订冲突可能附带 `details.currentDraftRevision`、`currentStateRevision` 或 `currentAccountRevision`。不要假设每种错误都带齐所有字段。

| 错误 | 建议处理 |
| --- | --- |
| `UNAUTHENTICATED`、`CLIENT_REVOKED` | 检查连接，必要时重新授权 |
| `FORBIDDEN`、`INSUFFICIENT_SCOPE` | 核对站主身份和连接权限 |
| `PRIMARY_OWNER_REQUIRED`、`WEB_ADMIN_REQUIRED` | 请由主管理员在网页后台管理站主账号 |
| `OWNER_PROTECTED` | 主管理员不可停用或移除 |
| `VERIFIED_ACCOUNT_REQUIRED`、`ACCOUNT_DISABLED` | 先验证邮箱、恢复账号后再添加为站主 |
| `VALIDATION_FAILED` | 按当前动作 Schema 修正参数 |
| `REVISION_CONFLICT` | 重新读取状态、检查差异并重新准备候选 |
| `IDEMPOTENCY_CONFLICT` | 同键请求内容不同，核对业务后使用对应键 |
| `UPLOAD_EXPIRED`、`UPLOAD_INCOMPLETE`、`DIGEST_MISMATCH` | 核对上传会话、入口及文件内容 |
| `CATEGORY_NOT_PUBLISHED` | 先发布对应站点分类，再重新准备游戏候选 |
| `SERVICE_UNAVAILABLE` | 稍后重试；保留同一业务幂等键并记录 requestId |

HTTP 429 应等待后重试，避免紧密循环。日志仅保留动作、目标、状态与请求 ID，不记录令牌、Cookie 或密码；后台操作记录显示操作人账号，便于追溯。
