回写陶泥儿导出与发布媒体直传文档并删除临时计划
Project CI / AI game creator shell Rust crates (pull_request) Failing after 57s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m27s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Successful in 2m5s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m30s
Project CI / Native shell tests (pull_request) Successful in 6m1s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 57s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m27s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Successful in 2m5s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m30s
Project CI / Native shell tests (pull_request) Successful in 6m1s
- 技术方案/后端契约/玩法链路三份主规范改成当前状态:文件草稿、build:taonier、multipart objectKey 直传与域内公开读 - decision-log 2026-10-06 条目由计划态改写为落地态,含命名约定、硬切、E1、taptap skill-pack 例外 - pitfalls 新增 multipart 幂等摘要、发布账本、skill-pack 红与 zip 存档根入口四条 - 删除临时实施计划与里程碑规范,历史由 Git 保存
This commit is contained in:
@@ -1,226 +0,0 @@
|
||||
# 【实施计划】陶泥儿导出文件草稿化与发布媒体直传
|
||||
|
||||
- Version: `v1`
|
||||
- Status: 待评审
|
||||
- Date: `2026-10-06`
|
||||
- Parent Spec(待回写):
|
||||
- `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(AGC 导出面板章节)
|
||||
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`(游戏分发媒体合同)
|
||||
- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`(游戏分发领域合同)
|
||||
- `docs/project-memory/shared-memory/decision-log.md`、`pitfalls.md`
|
||||
|
||||
> 本文档是 grilling 后的完整迁移计划(临时产物)。按《规范驱动开发工作流》,先回写主规范并通过评审,再按里程碑逐个实现;实现发现行为变化时按「主规范 → 未完成里程碑规范 → 当前实现计划 → 代码」回改。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话交付与验收判据
|
||||
|
||||
把陶泥儿(Taonier)导出迁移到小红书(xhs)重构后的文件草稿模式:**草稿 = `.export/taonier.json`(唯一真相)**、**打包 = `vite-export-taonier` skill + `build:taonier` 脚本产出 `.export/taonier.zip` 并由 code agent 适配任意工程布局**、**发布 = 宿主把草稿声明图片以 `multipart/form-data` 原始二进制直传游戏分发 API(未变图片带 OSS objectKey 沿用)**;同时**整体退役 `exports/`** 与**发布链路中的 assetId**。
|
||||
|
||||
验收判据(逐条可复核):
|
||||
|
||||
1. 未适配工程进入陶泥儿页先弹「让陶泥儿帮我调通」;适配后 `build:taonier` 成功产出 `.export/taonier.zip`,包内根目录存在 `index.html`。
|
||||
2. 表单校验只在前端;宿主 save 不做业务校验;校验不过时「打包」停在表单、不触宿主、不跑构建(对齐 xhs `157b4eb86` 断言)。
|
||||
3. 构建/草稿失败按 xhs 分类:可修复的转发给 code agent(「让陶泥儿来修」),不可修复的只提示人。
|
||||
4. 发布请求为 `multipart/form-data`:元数据 + 新/换图片原始二进制 + 沿用图片的 objectKey;`exports/playtest-package-*.zip` 不再存在,`.export/taonier.zip` 走现有整包/分片上传。
|
||||
5. 发布后线上读取显示「线上值」;草稿不自动回填、不被覆盖;每字段/图片可「沿用线上值」。
|
||||
6. 仓库中不存在 `exports/` 相关创建、读取、包路径、prompts、run-trace 白名单与命令;`assetId` 不再出现在游戏分发媒体请求/冻结资料/游戏行/公开读授权判定中。
|
||||
7. 全部定向测试、`check:spacetime-schema`、`check:generated-bindings`、`check:encoding`、`git diff --check`、`check:doc-index` 通过(`agc:skill-pack:check` 的 taptap 未登记红为已知接受项,见 §7)。
|
||||
|
||||
## 1. 背景与现状
|
||||
|
||||
- xhs 重构**已落盘**:`a6974f3ca`(表单校验只留前端,删 `validation.rs` 与 `FormInvalid`/`XHSMiniToolExportField`)、`157b4eb86`(多字段报错 + 交给陶泥儿)。重构后 xhs 形状 = `read_*`(内容)/`read_*_hash`(独立基线)/`save_*_form`(零校验、只做 baseHash 冲突)/`run_*_build`;前端 `xhsMinitoolFields.ts` 是唯一表单门;错误按 `code agent 可修 / 仅人可修` 分类;`.export/` flat 目录。
|
||||
- 陶泥儿现状:`useTaonierTab.ts` 调宿主 `export_local_project_package` 产出 `exports/playtest-package-*.zip`;`useGameDistributionPublishForm.ts`(968 行)做静默预填 + AI 资料建议 + 封面生成 + 素材登记上传;发布走 `publish_local_project_game`,staging 只读 `exports/playtest-package-*.zip`。
|
||||
- `exports/` 绑定面:项目初始化、包生成/读取/列举、4 个 Tauri 命令、生成任务产物声明(shared-contracts / platform-agent / 前端镜像)、prompts、run-trace 白名单、5 处安全/排除列表、`README` 交付物。
|
||||
- 发布媒体耦合面:`resolve_owned_game_media` → `resolve_owned_image_object_key`(客户端先登记素材、再带 assetId 发布);公开读授权 `game_distribution_asset_has_public_read_grant` 按 assetId 匹配;`axum` multipart 已启用且有两处先例(`raw_image.rs`、`admin_templates.rs`)。
|
||||
- 游戏分发模块不在线、无历史数据:可硬切、可删/改名 SpacetimeDB 字段,但必须同步 `migration.rs` 与生成绑定,并运行 `check:spacetime-schema`。
|
||||
|
||||
## 2. 已定决策(grilling 结论)
|
||||
|
||||
| # | 决策 |
|
||||
| --- | --- |
|
||||
| D1 | 对齐 xhs 重构后模式:文件草稿、typed error 转发 code agent、删除宿主业务校验。 |
|
||||
| D2 | 草稿 `.export/taonier.json` 形状 `{ form: { title, summary, category, coverPath, screenshotPaths[] } }`;不含 description/tags/deviceSupport/inputModes/orientation。 |
|
||||
| D3 | 每个目标独立、语义清晰的命令接口;不做带 target 枚举/flag 的 all-in-one 命令;内部复用共享 util。 |
|
||||
| D4 | 保留现有平台发布链(AGC native → `publish_local_project_game` 语义);打包改为 skill + `build:taonier` + code agent 首次适配,走与 xhs 相同的 adapt/fill/repair(未适配弹「让陶泥儿帮我调通」)。平台本身无额外适配概念,适配只针对打包。 |
|
||||
| D5 | 彻底退役 AI 资料建议与封面生成(前端 service、Rust 命令、api-server 路由、测试、专属 DTO);图片生成计费链路本身不动。 |
|
||||
| D6 | 删除 `useGameDistributionPublishForm`、`GameDistributionPublishFormView`、`GamePublishBlockedDialog`(及其测试);保留 `GamePublishProgressDialog` / `GamePublishPhaseSteps`。 |
|
||||
| D7 | 草稿为唯一真相;线上值只读展示;新增「沿用线上值」操作;不自动 seed、不回填覆盖。 |
|
||||
| D8 | 移除 `hasRunnablePrototype` 与 `ensure_publish_project_stack_at` 的 phaser/vite 门(含 `stage_publish_package` 的重复检查);新门 = `build:taonier` 存在、退出 0、产物 `.export/taonier.zip` 根有 `index.html`;保留符号链接/越界拒绝与 `checking`/`unavailable` 按钮禁用。 |
|
||||
| D9 | 整体退役 `exports/`(E1,含 `exports/README.md` 交付物):目录创建、README 生成、playtest 包链、4 命令、任务产物声明、prompts、run-trace 白名单、5 处排除列表、前端类型、测试、文档。 |
|
||||
| D10 | 新增 `vite-export-taonier` skill + `build:taonier` 脚本 + **`dist-taonier`** + `.export/taonier.zip`(与 xhs/taptap 同构,不做 native 特例);标准 `build`/`dist` 不被占用。 |
|
||||
| D11 | 发布 API 改为 `multipart/form-data` + 原始图片二进制(`4b`:不做单独 upload 命令,formdata 直发);整包 zip 保持现有 raw `Bytes`/分片上传,不并入 multipart。 |
|
||||
| D12 | 硬切(hard cut):不保留旧 JSON / assetId 分支与兼容 DTO。 |
|
||||
| D13 | 服务端解耦深度取 (b):发布图片落**项目快照桶** `agc/project-snapshots/v1/game-distribution/media/...`,**不建 `asset_object`**;公开读走新路由 `GET /api/game-distribution/media/read-url`(先抽公共签名/中转逻辑复用),与素材库 ACL 分离。删除请求/游戏行/冻结资料/授权判定中的 `*_asset_id`。 |
|
||||
| D14 | web 平台端一起迁到 formdata + objectKey(`gameDistributionClient.ts`、`GamePublishPage.tsx`、`GameWorkMetadataEditor.tsx`、`gamePublishAssets.ts`、`gamePublishMediaDefaults.ts`)。 |
|
||||
| D15 | skill pack 只登记 `vite-export-taonier`;未跟踪的 `vite-export-taptaph5` WIP 不登记,接受 `agc:skill-pack:check` 红(Q23①,风险见 §7)。 |
|
||||
|
||||
## 3. 主规范(行为合同)
|
||||
|
||||
### 3.1 草稿文件
|
||||
|
||||
- 路径:`<projectRoot>/.export/taonier.json`(flat `.export/`,与 xhs 同层级)。
|
||||
- 形状:`{ "form": { "title", "summary", "category", "coverPath", "screenshotPaths": [] } }`;`coverPath`/`screenshotPaths` 是**项目相对路径**;
|
||||
- `deny_unknown_fields`,camelCase;宿主只做 IO 事实守护(内容 hash 冲突、目录不可用),不做业务校验。
|
||||
- 写:前端 600ms 防抖自动保存 + 冲突逐字段选择;`save` 带 `baseHash`,格式差异且内容相同则吸收,否则返回 `SaveConflict`,绝不覆盖。
|
||||
- 读:`read`(内容)+ `read_hash`(独立基线,避免刷新移动写基线);前端 2s 轮询,输入中不覆盖草稿,只更新 `hasScript` 等外部事实。
|
||||
|
||||
### 3.2 打包
|
||||
|
||||
- 约定(对齐 xhs/taptap,不做特例):项目根或 `game/` 的 `package.json` 提供 `build:taonier`;宿主 `run_taonier_export_build` 以 `command.exec`(子命令 `build:taonier`,300s)运行;退出 0 且产出非空、非符号链接、≤200 MiB 的 `.export/taonier.zip` 才算成功。
|
||||
- 独立产物目录:`build:taonier` 由 skill 的 `vite.config.taonier.mjs` 构建到 **`dist-taonier`**,再由 `pack.mjs` 打成 `.export/taonier.zip`;**标准 `build` 脚本与标准 `dist` 不被导出占用**(标准 build 不负责打包,预览/开发仍用项目自己的 `dist`)。
|
||||
- 宿主不传任何构建 flag;skill 的 `pack.mjs` 沿用 xhs 约定自带默认值(`--vite-built-dir dist-taonier`、`--zip-out ../.export/taonier.zip`(cwd 为 `game/` 时)/ `.export/taonier.zip`(根工程时))。非标准工具链/输出目录由 agent 在项目自己的 `build:taonier` 包装脚本里适配。
|
||||
- skill `vite-export-taonier` 指导 code agent 为任意布局补齐 `build:taonier` 与必要配置。
|
||||
- 失败语义(typed error,forward 给 code agent):脚本缺失、命令被拒、命令失败(含输出尾)、产物缺失、导出目录不可用。
|
||||
- 预览不受影响:预览直接从 `project_game_root` 起服务,不读该 zip;`dist` 与 `dist-taonier` 互不干扰。
|
||||
|
||||
### 3.3 发布媒体直传
|
||||
|
||||
- 端点:`POST /api/game-distribution/games`、`POST /api/game-distribution/games/{game_id}/versions`、`PATCH /api/game-distribution/my-games/{game_id}` 改为 `multipart/form-data`。
|
||||
- 字段:`metadata` 文本 part(JSON,即原请求体的元数据部分,去掉媒体字段);`cover` 二进制 part(可选,出现时覆盖 `metadata.coverObjectKey`);`screenshot` 二进制 parts(可重复,按出现顺序消费)。`metadata.coverObjectKey` 为沿用线上封面时的 objectKey;`metadata.screenshots` 为 `(string|null)[]`,`string` 沿用该 objectKey、`null` 取下一个 `screenshot` part,数组顺序即最终截图顺序。
|
||||
- 存储:新图直接落**项目快照桶** `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>`(与发行包同桶、与素材库解耦,不建 `asset_object`);沿用图校验 objectKey 必须属于该游戏当前媒体;构建冻结资料(只含 objectKey)。
|
||||
- 读:新增 `GET /api/game-distribution/media/read-url`,按 objectKey 返回签名读地址;判定 = 已发布且 active 的 game 且 objectKey 命中 `cover_object_key` / `screenshots_json`,**不经素材库 ACL**;需要同源字节时再补 `.../media/read-bytes`。签名与字节中转先抽公共函数,供 `/api/assets/read-url` 与新路由共用。
|
||||
- 整包:`PUT .../versions/{version_id}/package` 及分片端点保持现状(raw zip / octet-stream)。
|
||||
- 幂等:`request_digest` 重定义为「规范化元数据 + 图片字节 hash + 沿用 objectKey」的稳定摘要,覆盖 create / version / update 三条写路径。
|
||||
- 所有权/权限:仅 owner 可发布/编辑;沿用图校验 objectKey 必须属于该游戏当前媒体;附图校验 `image/*` 与张数/体积上限;公开读由新路由的域内判定负责。
|
||||
- 硬切:旧 JSON / assetId 请求直接不支持,不保留兼容分支。
|
||||
|
||||
### 3.4 线上值与沿用
|
||||
|
||||
- 读命令除草稿外返回只读 `online`:live `GameDistributionGame` 行的 `title/summary/category` + `coverObjectKey` / 截图 objectKey 列表 + 预览地址。
|
||||
- UI 并排显示「草稿值 vs 线上值」;不自动 seed、不覆盖草稿。
|
||||
- 「沿用线上值」操作:文字字段一键把线上值写入草稿(标 dirty → 自动落盘);图片字段「替换为本地图」/「沿用线上」(清空本地路径,表示沿用);`screenshotPaths` 为空表示整组沿用线上。
|
||||
- 发布:路径非空的图发原始二进制;路径为空的图带线上 objectKey;文案沿用现有状态语义,不混淆在审/驳回状态。
|
||||
|
||||
### 3.5 错误与修复
|
||||
|
||||
- 前端错误分类(对齐 xhs):code agent 可修(草稿格式错、脚本缺失、命令失败、产物缺失)→「让陶泥儿来修」/「让陶泥儿帮我调通」;仅人可修(命令被拒、导出目录不可用)→ 只提示。
|
||||
- 失败对话框把**原始结构化错误**经项目对话转发给 code agent;适配指令引用 `vite-export-taonier` skill 与草稿路径/形状;「帮我填」只写元数据,不改打包适配。
|
||||
- 表单字段级红字 + 「交给陶泥儿」批量转发当前值,转发后清除红字并允许自动重读采纳 agent 修复。
|
||||
|
||||
### 3.6 退役(E1)
|
||||
|
||||
- `exports/`:项目初始化目录、`exports/README.md` 生成与渲染、`playtest-package-*.zip` 生成/读取/列举、`prepare/upload/list` 命令、staging 依赖。
|
||||
- 生成契约:publish-package 任务对 `exports/README.md` 的 artifact 声明(3 处)+ prompts(3 个 JSON)+ run-trace 白名单 slot。
|
||||
- 5 处安全/排除列表中的 `exports` 条目。
|
||||
- 发布媒体中的 assetId 与素材库耦合:请求 DTO、游戏行字段、冻结资料字段、公开读授权判定、`GameDistributionFrozenScreenshot.asset_id`;发布图片不再建 `asset_object`。
|
||||
- AI 资料建议/封面生成的前端调用、Rust 命令、api-server 路由与测试。
|
||||
|
||||
### 3.7 非目标
|
||||
|
||||
- 不改图片生成计费/额度链路本身;不改素材工作台对 `platform_asset_upload` 的既有使用(仅移除发布表单里的调用)。
|
||||
- 不新增 `/api/external/v1` 路由/契约(游戏分发不在 external v1,OpenAPI 不动)。
|
||||
- 不做历史数据迁移(模块不在线)。
|
||||
- 不登记 `vite-export-taptaph5`(另一条 WIP)。
|
||||
|
||||
## 4. 里程碑
|
||||
|
||||
| 里程碑 | 目标 | 依赖 | 验收证据 |
|
||||
| --- | --- | --- | --- |
|
||||
| M1 发布媒体合同解耦 | 游戏分发 API/契约/schema 改为 objectKey + multipart 直传;媒体落项目快照桶并新增公开读路由;web 平台端同迁 | 无 | 后端定向测试、`check:spacetime-schema`、`check:generated-bindings`、api-server smoke、web 端发布用例 |
|
||||
| M2 AGC 打包链路与 `exports/` 全退役 | 新 skill + `build:taonier`/`dist-taonier` + `run_taonier_export_build`;删 `exports/` 全链与旧打包门 | 无(可与 M1 并行) | Rust 定向测试、skill pack 同步(taonier 部分)、AGC typecheck、契约测试 |
|
||||
| M3 AGC 陶泥儿面板与发布媒体直传 | 草稿 hook/面板/错误转发/线上值与沿用;删 AI 建议/封面与旧表单组件;发布改 multipart | M1、M2 | vitest(字段校验、hook、面板、失败转发)、Rust 命令测试、真实栈发布 smoke |
|
||||
| M4 文档与共享记忆回写、临时计划清理 | 主规范/decision-log/pitfalls 回写;删临时计划 | M1–M3 | `check:doc-index`、`check:encoding`、`git diff --check` |
|
||||
|
||||
## 5. 实现计划
|
||||
|
||||
### 5.1 M1 发布媒体合同解耦
|
||||
|
||||
**Step 0(先做,重构优先)**:抽出可复用的「按 objectKey 签名读 + 同源字节中转」公共函数。
|
||||
- 现状:`assets.rs` 的 `get_asset_read_url_with_query` / `get_asset_read_bytes` 把「解析 target → 授权 → `sign_get_object_url` → 响应/中转」写在一起并绑定 `state.oss_client()`。
|
||||
- 目标:把「签名 + 响应形状 + 同源字节中转」拆成公共 helper(输入 OSS client、objectKey、expire、已授权标志),`/api/assets/read-url`/`read-bytes` 与游戏分发媒体路由共用,避免复制。
|
||||
- 授权保持域内分离:素材域继续走素材授权;游戏分发域走「已发布且 active 的 game + objectKey 命中 cover/screenshots」。
|
||||
- 必须用素材域现有测试锁定 `/api/assets/read-url`/`read-bytes` 的授权与过期语义不变。
|
||||
|
||||
**Step 1 shared-contracts**
|
||||
- `server-rs/crates/shared-contracts/src/game_distribution.rs`:`GameDistributionCreateGameRequest` / `GameDistributionUpdateGameMetadataRequest` 去掉 `cover_asset_id`,`screenshots` 语义改 objectKey;新增 multipart 表单 DTO(元数据字段 + `coverObjectKey` + 沿用 `screenshots`)。
|
||||
- `packages/shared/src/contracts/gameDistribution.ts`:镜像同步;`GameDistributionGameSummary.coverObjectKey` / `screenshots` 维持 objectKey。
|
||||
|
||||
**Step 2 spacetime-module**
|
||||
- `GameDistributionGame`:删 `cover_asset_id`;`cover_object_key` 保留;`screenshots_json` 改为 objectKey 数组。
|
||||
- `GameDistributionFrozenMetadata`:删 `cover_asset_id`;`screenshots: Vec<String>`(objectKey);删 `GameDistributionFrozenScreenshot.asset_id`。
|
||||
- `apply_game_distribution_frozen_metadata`、`GameDistributionCreateGameInput` / `GameDistributionUpdateMetadataInput` / `GameDistributionGameSnapshot` 同步。
|
||||
- 公开读授权:删除 `game_distribution_asset_has_public_read_grant(ctx, asset_object_id)` 及其在 `editor_project_storage::asset_location_has_public_showcase_read_grant` 的调用(发布图不再经素材库读);公开判定迁到新路由的域内判定(已发布且 active + objectKey 命中)。
|
||||
- `migration.rs` + 表目录 + 生成绑定;运行 `npm run check:spacetime-schema`。
|
||||
|
||||
**Step 3 api-server**
|
||||
- `modules/game_distribution.rs`:`create_game` / `create_version` / `update_owner_game_metadata` 换 `Multipart` 解析(参照 `raw_image.rs` / `admin_templates.rs`);媒体写入用 `project_snapshot_oss_client` 的 `put_internal_object_with_retry`(专用前缀 + 内容类型/大小校验,不建 `asset_object`);沿用图校验改为「objectKey 必须属于该游戏当前媒体」;`resolve_version_metadata_json` 输出 objectKey;`request_digest` 重定义。
|
||||
- 新增 `GET /api/game-distribution/media/read-url`(+ 按需 `read-bytes`):匿名可读,用公共签名 helper + `project_snapshot_oss_client` + 游戏分发公开判定;更新平台 web 与 AGC 的封面/截图读地址来源。
|
||||
- 删除 `POST /api/game-distribution/publish-metadata/suggestions` 与封面生成相关路由;更新模块测试(冻结 JSON 形状、上传校验)。
|
||||
- `DefaultBodyLimit` 按图片大小设限(参照 `raw_image` 64 MB 级别,按 6 张截图 + 封面估算)。
|
||||
|
||||
**Step 4 web 平台端**
|
||||
- `src/services/gameDistributionClient.ts` / `GamePublishPage.tsx` / `GameWorkMetadataEditor.tsx` / `gamePublishAssets.ts` / `gamePublishMediaDefaults.ts`:发布/编辑改 `FormData`;未变图带 objectKey;删除发布前的素材登记步骤;封面/截图展示改走 `/api/game-distribution/media/read-url`。
|
||||
- 平台端测试与类型检查。
|
||||
|
||||
**Step 5 文档**:回写后端架构与玩法链路主规范 + OpenAPI 无需改动说明。
|
||||
|
||||
### 5.2 M2 AGC 打包链路与 `exports/` 全退役
|
||||
|
||||
**Step 1 skill**
|
||||
- 新增 `apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-taonier/`,镜像 xhs:`SKILL.md`、`scripts/vite.config.taonier.mjs`(`outDir: 'dist-taonier'` + 产物收尾插件)、`scripts/pack.mjs`(默认 `--vite-built-dir dist-taonier`、`--zip-out ../.export/taonier.zip`,纯 Node zip,根目录须有 `index.html`)、以及隐藏测试 `scripts/.pack.test.mjs`、`scripts/.vite.config.taonier.test.mjs`。
|
||||
- 项目侧 `build:taonier` = 用该 config 构建到 `dist-taonier` 后跑 `pack.mjs` 产出 `.export/taonier.zip`(具体命令由 agent 写进项目 `package.json`)。
|
||||
- 登记:`scripts/skill-pack-manifest.mjs` 的 `EXPECTED_SKILL_NAMES`、`skill_pack.rs` 的 `AGC_SKILL_PACK_EXPECTED_NAMES` / `AGC_SKILL_PACK_FILES`、`manifest.json`(`agc:skill-pack:sync` 生成 sha256 并 bump version)、`codex_app_server/mod.rs` 三处测试 mock 名单。
|
||||
|
||||
**Step 2 宿主打包命令**
|
||||
- 新增 `src-tauri/src/export/draft/taonier/`:`layout.rs`(`SCRIPT_NAME="build:taonier"`、`REGISTRY_FILE_NAME="taonier.json"`、`ARTIFACT_FILE_NAME="taonier.zip"`)、`build.rs`(跑脚本 + 产物校验)、`script.rs`(npm 工作目录发现,复用 xhs 逻辑)、`registry.rs`、`dto.rs`、`error.rs`、`commands.rs`(M3 接前端;本里程碑先落 build 侧与注册)。
|
||||
- `src/desktop.rs` 注册新命令。
|
||||
|
||||
**Step 3 `exports/` 全退役**
|
||||
- `project/manifest.rs:614`:从初始化目录移除 `exports`。
|
||||
- `project/export.rs`:删 `export_local_project_package_at`、`export_local_project_package_for_publish_at`、`ensure_project_export_readme`、`render_project_export_readme`、`next_project_export_package_relative_path`、`collect_project_export_package_files`、`read_local_project_export_package_at`、`list_local_project_export_packages_at` 及专属测试。
|
||||
- `commands/desktop.rs`:删 `export_local_project_package`、`prepare_local_project_game_package`、`upload_local_project_game_package`、`list_local_project_export_packages`;同步 `src/desktop.rs` 与 `scripts/check-config.mjs` 白名单。
|
||||
- `game_distribution_publish.rs`:`stage_publish_package` 改从 `.export/taonier.zip` 读取;删 L1194 的 phaser 重复检查。
|
||||
- 新门:删 `hasRunnablePrototype` 调用与 `ensure_publish_project_stack_at` 的 phaser/vite 门;保留符号链接/越界拒绝。
|
||||
- 生成契约:`shared-contracts/game_creation_app.rs:~427`、`platform-agent/game_creation.rs:~987`、`packages/shared/src/contracts/gameCreationApp.ts:~438` 移除 `exports/README.md` artifact;`prompts/runtime/texts/{execution,generation,media}.json` 同步;`main.rs:1346` 白名单删 slot。
|
||||
- 排除列表:`preview.rs:1112/1156`、`agent/direct_runtime/mod.rs:3606`、`agent/direct_validation.rs:261`、`repository_context.rs:720` 移除 `exports`。
|
||||
- `src/app/types.ts` 的 `LocalProjectExportPackageResult` 等类型与前端调用清理。
|
||||
|
||||
**Step 4 验证**:Rust 定向测试;`agc:skill-pack:check`(taonier 部分绿,taptap 红为已知接受项);AGC typecheck;契约测试。
|
||||
|
||||
### 5.3 M3 AGC 陶泥儿面板与发布媒体直传
|
||||
|
||||
**Step 1 草稿模块补齐**:`export/draft/taonier/{dto,error,registry,commands}.rs` 落地 4 命令(`read_taonier_export` / `read_taonier_export_hash` / `save_taonier_export_form` / `run_taonier_export_build`,前端另需线上值读取)。
|
||||
|
||||
**Step 2 前端状态与错误**
|
||||
- `export/state/taonierFields.ts`:`TaonierField = 'title'|'summary'|'category'`;`TAONIER_FORM_FIELDS`;`validateTaonierForm()` 返回**全部**错误(必填 + 长度,码点计数)。
|
||||
- `export/state/taonierApi.ts` / `taonierFailure.ts` / `taonierInstruction.ts`:三命令 Error 载体 + guard + 包装;可修复分类;adapt/fill/repair 指令(引用 `vite-export-taonier` 与 `.export/taonier.json` 形状)。
|
||||
- `export/state/useTaonierExport.ts`:自动保存(baseHash)、2s 轮询、冲突、失败、`packageAll`、`adapt`、`repair`、`fillForm`、`handFieldErrorsToAgent`、`online` 与「沿用线上值」动作;返回面与 xhs hook 同构。
|
||||
|
||||
**Step 3 UI**
|
||||
- `tabs/taonier/`:`Page.tsx`(2 列)、`ArtifactsPane.tsx`(表单槽/读取中/重试/打包/下载产物/冲突/线上值卡/发布链接/三个 dialog)、`FormCard.tsx`(逐字段红字 + 帮我填 + 交给陶泥儿)、`OnlineValuesCard.tsx`(线上值只读 + 沿用操作)、`ConflictCard.tsx`、`FailureDialog.tsx`、`AdaptPromptDialog.tsx`、`DownloadFailureDialog.tsx`。
|
||||
- 复用 `common/PaneButton`、`ThemedModal`,去掉旧 `PublishPane` 对 `GameDistributionPublishFormView` 的依赖。
|
||||
|
||||
**Step 4 发布媒体**
|
||||
- `src-tauri/src/game_distribution_publish.rs`:读 `.export/taonier.json` 与图片路径,构建 multipart 请求(新图读文件字节;空路径发线上 objectKey),沿用 `publish_local_project_game` 语义与异步任务状态;幂等与错误语义对齐 M1。
|
||||
- 删前端 `useGameDistributionPublishForm`、`GameDistributionPublishFormView`、`GamePublishBlockedDialog` 与 `services/gameDistributionPublish.ts` 中 `suggestGameDistributionPublishMetadata` / `generateGameDistributionCover` / `readGameCoverGenerationPrice` 调用;删对应 Rust 命令与注册。
|
||||
- 保留 `GamePublishProgressDialog` / `GamePublishPhaseSteps` / 发布状态机。
|
||||
|
||||
**Step 5 测试**:`tests/taonierFields.test.ts`、`tests/taonierExport.test.tsx`(对齐 xhs 用例面:指令、左半 UI、逐命令失败表、hook 状态与「校验不过不触宿主」)、`tests/taonierContract.test.ts`(前端常量 vs Rust `layout.rs`);Rust 命令与发布单测。
|
||||
|
||||
### 5.4 M4 文档与共享记忆
|
||||
|
||||
- 回写主规范章节;更新 `decision-log.md`(命名约定、硬切、objectKey 授权、E1、skill-pack 例外)、`pitfalls.md`(multipart 幂等摘要、幂等键、taptap 未登记红)。
|
||||
- 删除本临时计划与里程碑规范;运行 `npm run check:doc-index`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 6. 验证与证据矩阵
|
||||
|
||||
| 证据 | 内容 |
|
||||
| --- | --- |
|
||||
| 规范对照 | §3 各条 vs §4 里程碑验收逐项结果 |
|
||||
| 自动化验证 | 后端 `cargo test`、`check:spacetime-schema`、`check:generated-bindings`;前端 `typecheck` + 定向 vitest;`check:encoding`、`git diff --check`、`check:doc-index` |
|
||||
| 运行时验证 | `npm run dev:api-server` + `/healthz`;multipart 创建/编辑/发布 smoke;AGC 真实栈一次适配→打包→发布 |
|
||||
| 边界验证 | owner 归属、沿用 objectKey 校验、图片大小/张数上限、幂等重放、路径越界/符号链接拒绝、公开读授权只对已发布游戏生效 |
|
||||
| 未验证项 | 真实 OSS 与对象生命周期、web 平台端真实发布、`agc:skill-pack:check` 的 taptap 红 |
|
||||
|
||||
## 7. 风险与回滚
|
||||
|
||||
- **读签名重构回归**:抽公共 helper 会同时动 `/api/assets/read-url`/`read-bytes`;必须用素材域现有测试锁定授权与过期语义不变。
|
||||
- **SpacetimeDB 删字段**:无历史数据但仍需 `migration.rs` 与绑定;回滚 = 撤销 M1 提交,schema 随代码回滚。
|
||||
- **multipart 幂等**:digest 输入必须覆盖图片字节,否则重放会重复建图;以测试锁定。
|
||||
- **`agc:skill-pack:check` 红**:由未跟踪 taptap 造成,接受为已知项;若 CI 必须绿,需先收掉或登记 taptap(D15 之外)。
|
||||
- **发布范围大**:M1 与 M3 都改发布;小程序化落地顺序 = M1 → M2 → M3,M1 未验收前 M3 不依赖其接口。
|
||||
|
||||
## 8. 未决项
|
||||
|
||||
无。§2 决策与 §3 主规范已覆盖全部边界;taonier 与 xhs/taptap 同构(`build:taonier` + `dist-taonier`,标准 `build`/`dist` 不被占用),构建 flag 由 skill 的 `pack.mjs` 自带默认值、宿主不传;web 平台端不设灰度开关(硬切,D12)。实现中若发现行为需变化,按《规范驱动开发工作流》顺序回改:主规范 → 未完成里程碑规范 → 当前实现计划 → 代码与测试。
|
||||
@@ -1,33 +0,0 @@
|
||||
# 【里程碑】陶泥儿导出文件草稿化与发布媒体直传
|
||||
|
||||
- Version: `v1`
|
||||
- Status: 待评审
|
||||
- Date: `2026-10-06`
|
||||
- Parent Spec:
|
||||
- `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(导出产物面板与陶泥儿导出)
|
||||
- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`(游戏分发发布媒体直传合同)
|
||||
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`(游戏分发媒体与 schema)
|
||||
- 实现计划:`docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md`
|
||||
|
||||
## 目标
|
||||
|
||||
把陶泥儿导出迁移到文件草稿 + agent 适配打包,并把游戏分发发布媒体改为 formdata 原始二进制/objectKey 直传,整体退役 `exports/` 与发布链路里的 assetId。
|
||||
|
||||
## 边界
|
||||
|
||||
- 含:`.export/taonier.json` 草稿、`build:taonier`/`dist-taonier`/`.export/taonier.zip`、adapt/fill/repair、线上值与「沿用线上值」、发布媒体 formdata 直传与新媒体读路由、`exports/` 全退役、AI 建议/封面与旧发布表单退役、web 平台端同步迁移。
|
||||
- 不含:图片生成计费链路、`/api/external/v1`、历史数据迁移、`vite-export-taptaph5` 登记。
|
||||
|
||||
## 里程碑与验收标准
|
||||
|
||||
| 里程碑 | 目标 | 验收标准 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| M1 发布媒体合同解耦 | 公共读签名重构;shared-contracts/spacetime-module/api-server/web 改 objectKey + multipart;新媒体读路由 | 后端定向测试、`check:spacetime-schema`、`check:generated-bindings`、api-server smoke、web 发布用例 | 无 |
|
||||
| M2 AGC 打包链路与 exports 全退役 | skill + `build:taonier`/`dist-taonier` + `run_taonier_export_build`;删 `exports/` 全链与旧打包门 | Rust 定向测试、skill pack(taonier 部分)、AGC typecheck、契约测试 | 无 |
|
||||
| M3 AGC 陶泥儿面板与发布媒体直传 | 草稿模块与前端状态/UI;线上值与沿用;发布直传;退役 AI 建议/封面与旧表单 | vitest、Rust 命令测试、真实栈发布 smoke | M1、M2 |
|
||||
| M4 文档与共享记忆 | 主规范回写、decision-log/pitfalls、清理临时计划 | `check:doc-index`、`check:encoding`、`git diff --check` | M1–M3 |
|
||||
|
||||
## 依赖与门禁
|
||||
|
||||
- M1、M2 可并行;M3 依赖两者。
|
||||
- 每个里程碑验收通过前不得进入下一个;实现中行为变化按《规范驱动开发工作流》顺序回改主规范。
|
||||
@@ -9900,10 +9900,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
|
||||
## 2026-10-06:陶泥儿导出文件草稿化与游戏分发发布媒体直传
|
||||
|
||||
- 陶泥儿导出对齐小红书重构后的文件草稿模式:草稿 `.export/taonier.json` 为唯一真相,宿主零业务校验,校验只在前端;失败现场原样转发 code agent,adapt/fill/repair 与小红书同构。
|
||||
- 打包不做 native 特例:新增 `vite-export-taonier` skill,`build:taonier` 构建到 `dist-taonier` 并由 `pack.mjs` 打成 `.export/taonier.zip`;标准 `build`/`dist` 不被占用。
|
||||
- `exports/` 整体退役(E1,含 `exports/README.md` 交付物、playtest 包链、`prepare/upload/list` 命令、生成任务产物声明、prompts、run-trace 白名单与排除列表);AI 资料建议与封面生成、`useGameDistributionPublishForm`/`GameDistributionPublishFormView`/`GamePublishBlockedDialog` 一并删除,保留发布进度对话框。
|
||||
- 游戏分发发布媒体改为 formdata 原始二进制/objectKey 直传(硬切,无旧 JSON/assetId 兼容);图片落项目快照桶 `agc/project-snapshots/v1/game-distribution/media/...`,不建 `asset_object`;公开读新增 `GET /api/game-distribution/media/read-url`(先抽公共签名/字节中转 helper 复用),删除 `game_distribution_asset_has_public_read_grant`。
|
||||
- 游戏行/冻结资料删除 `cover_asset_id`,`screenshots_json`/冻结 `screenshots` 改存 objectKey 数组;无历史数据,仍需同步 `migration.rs` 与生成绑定。
|
||||
- skill pack 只登记 `vite-export-taonier`;未跟踪的 `vite-export-taptaph5` WIP 不登记,`agc:skill-pack:check` 的对应红为已知接受项。
|
||||
- 计划:`docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md`。
|
||||
- 导出目标命名约定与落点:草稿 `.export/taonier.json`(唯一真相、flat `.export/` 目录)、npm 脚本 `build:taonier`、独立构建目录 `dist-taonier`、交付/发布产物 `.export/taonier.zip`(存档根必须有 `index.html`);标准 `build`/`dist` 不被占用。新增 `vite-export-taonier` skill(`vite.config.taonier.mjs` + `pack.mjs`),宿主 `export/draft/taonier` 提供读内容 / 读指纹 / 存表单 / 跑构建四个命令。
|
||||
- 草稿模型对齐小红书重构:宿主零业务校验,校验只在前端 `taonierFields`;600ms 自动保存带 `baseHash`、2s 轮询只更新外部事实、冲突逐字段选择;失败现场原样转发 code agent,adapt/fill/repair 三类指令与小红书同构;线上值只读并排,可逐字段/逐图「沿用线上值」。
|
||||
- `exports/` 整体退役(E1):目录创建、README 生成/渲染、试玩包打包/读取/列举、`prepare/upload/list` 命令、staging 依赖、生成任务产物声明、prompts、run-trace 白名单与 5 处排除列表全部移除;发布暂存固定读取 `.export/taonier.zip`,phaser/vite 发布门删除。
|
||||
- 发布媒体硬切:`POST /api/game-distribution/games`、`POST /api/game-distribution/games/{gameId}/versions`、`PATCH /api/game-distribution/my-games/{gameId}` 改收 `multipart/form-data`(`metadata` 文本 JSON 含 `coverObjectKey` 与 `screenshots: (string|null)[]`,可选 `cover` part,可重复 `screenshot` part);`string` 槽位沿用线上 objectKey,`null` 槽位按序消费二进制 part。不保留旧 JSON / assetId 兼容分支;AGC 发布先读草稿本地路径发字节、空路径发线上 objectKey,幂等摘要纳入图片字节。
|
||||
- 媒体公开读按域内判定:新图直落项目快照桶 `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>`,不建 `asset_object`;公开读走 `GET /api/game-distribution/media/read-url`(及 `read-bytes`),判定 = 已发布且 active 的 game 且 objectKey 命中 `cover_object_key` / `screenshots_json`,不经素材库 ACL。删除 `game_distribution_asset_has_public_read_grant` 与发布链路中的 assetId;游戏行与冻结资料只存 objectKey,删除 `GameDistributionFrozenScreenshot`。
|
||||
- schema 删字段属 breaking:按既有先例用 `SPACETIME_SCHEMA_GUARD_ALLOW_BREAKING=1 npm run check:spacetime-schema` 验证,并同步 `migration.rs`、表目录与生成绑定。
|
||||
- AI 资料建议 / 封面生成退役:删前端调用、Rust 命令与注册、api-server 路由与专属 DTO;保留发布进度对话框。
|
||||
- skill pack 例外:`agc-skill-pack.v1` 只登记 `vite-export-taonier`;未跟踪的 `vite-export-taptaph5` 是另一条 WIP,不登记,`npm run agc:skill-pack:check` 对其未声明文件报红为已知接受项。
|
||||
- 权威行为已回写 AGC 实施计划「2026-10-06」章节、玩法链路「游戏分发发布媒体直传合同」与后端数据契约;临时实施计划与里程碑规范已删除,历史由 Git 保存。
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
|
||||
|
||||
## 2026-10-06 陶泥儿导出与发布媒体直传:幂等摘要、幂等账本、skill-pack 红与 zip 根入口
|
||||
|
||||
- **multipart 发布的幂等摘要必须纳入图片字节**:写路径改 `multipart/form-data` 后,`metadata` 文本 part 只描述槽位;若摘要只序列化元数据,同一 `Idempotency-Key` 换一张新图或换一个沿用 objectKey 会被判成同请求重放,服务端静默复用旧结果、新图丢失。现行口径:服务端 `publish_request_digest` = 规范化元数据 JSON + 每个 `cover`/`screenshot` part 的 SHA-256 + 槽位里的 objectKey;AGC 侧 `metadata_digest` 同样把图片原始字节喂进哈希。create / create version / update metadata 三条写路径共用;回归时「只换图不改文案」必须得到不同摘要。
|
||||
- **幂等键与发布账本口径**:AGC 根幂等键由账本按「账号 + origin + 本地项目 + 包摘要 + 目标游戏/版本 + 资料摘要」解析,同一次用户发布的重试、响应丢失后的重发与进程重启后的分片续传都复用同一 root key,包内容 / 目标版本 / 资料任一变化才换键;分包上传再用 `<root>:upload`。服务端 `game_distribution_idempotency_receipt` 以 `owner_user_id + action + idempotency_key` 唯一,同 key 同摘要回 `replayed=true` 并复用结果 ID,同 key 不同摘要返回 409。不要把重试实现成新 key,也不要让摘要漏掉图片字节或沿用 objectKey,否则媒体直传下的重放语义失效。
|
||||
- **`npm run agc:skill-pack:check` 因未登记 taptap 报红是已知接受项**:`vite-export-taptaph5` 是另一条 WIP,未进 `AGC_SKILL_PACK_EXPECTED_NAMES` / `EXPECTED_SKILL_NAMES`;check 会列出它的未声明文件并退出非零。当前接受该红,不为它补登记或改清单;要恢复全绿必须先收口或正式登记 taptap skill。taonier 自身的清单与指纹已锁步。
|
||||
- **taonier zip 必须存档根 `index.html`**:`.export/taonier.zip` 的存档根必须直接有 `index.html`,不能套外层文件夹(`taonier/index.html`、`game/index.html` 都算失败)。`vite-export-taonier/scripts/pack.mjs` 在落盘前按根入口校验并拒绝;即使绕过,服务端发行合同只认根 `index.html`,会以 `ReleasePackageError::MissingEntry` 映射 422 `PACKAGE_VALIDATION_FAILED` 拒收。适配非标准工程布局时,让 `build:taonier` 把 `--vite-built-dir` 指到真正含 `index.html` 的目录,或让 `pack.mjs` 不套外层;客户端 `run_taonier_export_build` 也在产物收尾复核根入口。
|
||||
|
||||
## 2026-10-05 PR #607 复核修复:档位点对齐/对比度、状态文案也走浮层、键盘去重、卸载 flush
|
||||
|
||||
- **档位圆点已删除(D1 的收口)**:这一轮把档位圆点**整体删除**(半透明备选方案未采用)。现在滑块只剩轨道 + 圆钮:轨道 6px 圆头、已选段 `--platform-accent` 由 `--strength-ratio` 驱动、**终点落在圆钮中心**(`calc(10px + ratio * (100% - 20px))`)、圆钮 20px 实心暖白(`--platform-panel-fill` + `--platform-subpanel-border` 1px 描边 + `color-mix` 柔影);强度区横向内边距 `4px 6px 2px` → `4px 0 2px`(滑块铺满卡片内容宽度,填充段与圆钮两端与轨道两端贴齐);相关 CSS(`space-between` 排布 / `z-index: 2` 抬层 / `.is-active{opacity:0}` / 点的 `color-mix` 底色 / 只为点对齐的 `padding: 0 7px`)与渲染标记一并删除。**判据**:`chatDialogFrameLayout.test.ts` 反向守卫(样式表里不再有 `.project-chat-composer-strength-stops` 规则、组件源码不再渲染该类名;滑块契约仍在:宽 100% / 高 26 / 圆钮 20×20、强度区左右内边距 0)+ `home.suite.ts` 首页菜单里查不到那组点。实测(447 视口,像素扫描):轨道 90..324(宽 234 = 卡片内容宽),档位 0 时圆钮左缘 90.5(距轨道左端 0.5px)、档位 4 时圆钮右缘 322.3(距右端 1.8px,扫描行不在圆钮正中所以略窄),填充段终点落在**圆钮中心**(`calc(10px + ratio * (100% - 20px))`,被圆钮盖住),因此圆钮右侧不会露出橙色(终点曾写成 `20px + …` = 圆钮右缘,4× 设备像素下能看到一小截溢出)。**历史成因(只留一句,细节由 Git 追溯)**:圆点此前被 6px 轨道盖住、且与圆钮两端错位 ±12.2px,曾用「抬到轨道之上 + space-between 对齐 + 浅暖色」修过一轮,最终整体删除。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -497,16 +497,16 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- Rust 结构体:`GameDistributionGame`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`
|
||||
- 用途:游戏分发稳定身份与公开版本指针。保存 owner、标题/简介/分类资料、设备与输入声明、`publication_revision`、当前 `active_version_id`、可见性和游玩计数;标签与输入模式按版本化 JSON 保存,展示资料由 `api-server` 通过 `spacetime-client` 归一后返回。
|
||||
- 公开素材:游戏行末尾追加可空 `cover_object_key` 与 `screenshots_json`(截图 `{assetId, objectKey}` 数组);创建游戏时 `api-server` 就复核封面/截图素材存在且属于当前作者(不存在 400、他人素材 403),创建版本时按同一口径再次复核并派生对象键。 发布写入受灰度配置键 `game-distribution:publish` 约束:**灰度默认关闭**,未配置或 `enabled=false` 时写入口(创建游戏/版本、确认包、送审、审核通过激活)返回 503 `GAME_DISTRIBUTION_PUBLISH_DISABLED`,`enabled=true` 且白名单/比例/标签命中才放行,读取与安全下架保持可用;同一判据在 `GET /api/runtime/frontend-config` 以 `gameDistributionPublishEnabled` 下发给前端入口,匿名恒为 `false`。只有可见性为 `published` 且存在有效 `active_version_id` 的游戏,其封面/截图素材才在 `/api/assets/read-url` 上获得匿名读授权。
|
||||
- 公开素材:游戏行末尾追加可空 `cover_object_key` 与 `screenshots_json`(objectKey 字符串数组)。发布写路径(创建游戏 / 创建版本 / 编辑资料)收 `multipart/form-data`:新图由服务端直写项目快照桶 `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>` 并只回 objectKey,不建 `asset_object`;沿用槽位带该作品当前媒体的 objectKey,不命中即 400,附图校验 `image/*`、张数与体积上限。 发布写入受灰度配置键 `game-distribution:publish` 约束:**灰度默认关闭**,未配置或 `enabled=false` 时写入口(创建游戏/版本、确认包、送审、审核通过激活)返回 503 `GAME_DISTRIBUTION_PUBLISH_DISABLED`,`enabled=true` 且白名单/比例/标签命中才放行,读取与安全下架保持可用;同一判据在 `GET /api/runtime/frontend-config` 以 `gameDistributionPublishEnabled` 下发给前端入口,匿名恒为 `false`。只有可见性为 `published`、存在有效 `active_version_id` 且未软删除的游戏,其 `cover_object_key` / `screenshots_json` 中的 objectKey 才在 `GET /api/game-distribution/media/read-url`(需要同源字节时用 `.../media/read-bytes`)上按游戏分发域内判定获得匿名读授权,不经素材库 ACL。
|
||||
- 复用规则:末尾可空列 `local_project_id` 保存发布方本地项目标识(AGC 的 `manifest.projectId`)。同一 `owner_user_id` 再次以相同 `local_project_id` 创建游戏时复用既有 `game_id` 并只新增版本,避免“更新”被实现成新建游戏;该字段只是复用提示,不构成所有权或路径凭证,也不能用于跨账号匹配。已软删除的游戏不参与复用:删除后重新发布同一本地项目应得到新的游戏身份。
|
||||
- 软删除:游戏行末尾追加可空 `deleted_at`(2026-10-01)。非空表示作者已删除该作品:`delete_game_distribution_game_and_return` 只写该时间戳并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料一律不改写。软删行不进入作者列表(`list_owner_game_distribution_games_and_return`)、公开目录(`list_public_game_distribution_games_and_return`)、公开详情(`get_public_game_distribution_game_and_return`)、发行网关素材授权(`game_distribution_asset_has_public_read_grant`)与审核队列;后台默认视图同样排除,只有显式 `status=deleted` 才会读到。作者侧版本回读对软删作品返回空(404),因此上传、确认与送审入口一并关闭。
|
||||
- 软删除:游戏行末尾追加可空 `deleted_at`(2026-10-01)。非空表示作者已删除该作品:`delete_game_distribution_game_and_return` 只写该时间戳并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料一律不改写。软删行不进入作者列表(`list_owner_game_distribution_games_and_return`)、公开目录(`list_public_game_distribution_games_and_return`)、公开详情(`get_public_game_distribution_game_and_return`)、公开媒体读取判定(`get_game_distribution_media_read_access_and_return`)与审核队列;后台默认视图同样排除,只有显式 `status=deleted` 才会读到。作者侧版本回读对软删作品返回空(404),因此上传、确认与送审入口一并关闭。
|
||||
- 资料编辑:`update_game_distribution_game_metadata_and_return` 覆盖游戏行上的展示字段(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向)并立即生效,要求 `expected_publication_revision` CAS;版本行与冻结资料不变,下一次审核通过仍会用新版本的冻结资料覆盖游戏行。**资料编辑不得直接改公开价格**:调价必须走新版本审核。
|
||||
- 买断制定价(2026-10-05):游戏行末尾追加 `price_mud_points: u64` 并设置 `#[default(0u64)]`;`0` 表示免费,上限 `1_000_000`(复用 `module-game-distribution::normalize_game_price_mud_points` 校验)。价格是版本冻结资料的一部分:作者在 `GameDistributionCreateVersionRequest.priceMudPoints` 提交,写入版本冻结 `metadata_json.priceMudPoints`,只有 `approve_game_distribution_version_and_return` 通过审核时才随资料整体生效到本行;未通过审核或资料编辑都不会改变当前公开价格。公开投影(`get_public_game_distribution_game_and_return` 等)在游戏快照上带出 `priceMudPoints`。
|
||||
- 索引:`by_game_distribution_game_owner_user_id` 用于作者私有游戏列表;`game_id` 为主键。公开目录只返回 `visibility = published`、`deleted_at` 为空且活动版本存在、状态为 `published`(有效 `active_version_id`)的投影。
|
||||
- 购买与播放鉴权 HTTP(2026-10-05):`POST /api/game-distribution/games/{gameId}/purchase`(`require_bearer_auth` + 必填 `Idempotency-Key`,请求体 `{ expectedPriceMudPoints }`)经 facade 调 `purchase_game_distribution_game_and_return`,返回 `{ purchase, walletBalance, replayed }`;余额不足 400 `INSUFFICIENT_MUD_POINTS`、价格已变化 409、免费游戏 400、作者本人自购 400 `GAME_PURCHASE_OWNER_EXEMPT`(作者免购买,绝不扣费)、管理员令牌 403 `GAME_PURCHASE_ADMIN_NOT_ALLOWED`(购买只接受普通用户 bearer)、游戏不可见 404、缺幂等键 400、未登录 401。`POST /api/game-distribution/games/{gameId}/play-session` 对免费作品直接回既有公开入口 `/games/{gameId}/`;付费作品同时接受管理员令牌(按现有 admin 鉴权)与用户令牌,已购买 / 作者本人 / 管理员才签发绑定 `gameId + userId`、2 小时有效期的进程内会话,令牌为内存态,进程重启即失效。网关 `GET /api/game-distribution/play-sessions/{token}[/{assetPath}]` 不挂登录中间件、凭令牌读取当前公开版本包,能解析出平台刷新会话 Cookie 时 403,令牌过期 / 不存在、游戏下架 / 封禁或没有有效公开版本一律 404,全部 `no-store`。公开详情 `GET /api/game-distribution/games/{gameId}` 可选鉴权读取查看者:`purchased` 只反映真实购买记录,付费作品对未购买且非作者 / 非管理员把 `currentVersion.entryUrl` 置 `null`(资料与价格仍可见);`GET /api/game-distribution/releases/{gameId}[/{assetPath}]` 在当前公开版本 `price_mud_points > 0` 时同样 404,付费作品只能经播放会话路径播放。
|
||||
- 游玩计数写入:`play_count` 只由批量 procedure `increment_game_distribution_game_play_counts_and_return`(输入 `GameDistributionPlayCountIncrementInput { increments: Vec<{ gameId, delta }> }`)累加。`api-server` 在内存里按 `identity + gameId` 做 30 分钟去重、按 `IP + gameId` 做固定窗口限流后,按 `GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS`(默认 5 秒)批量落库;事务内只对 `published` 且存在有效 `active_version_id` 的游戏 `saturating_add`,非公开静默跳过,且**不更新** `updated_at`。公开 HTTP 入口为 `POST /api/game-distribution/games/{gameId}/plays`,完整行为见玩法链路的「游玩计数(已实现)」。
|
||||
|
||||
- 发布媒体直传(2026-10-06):游戏行删除 `cover_asset_id`,`cover_object_key` 保留,`screenshots_json` 改存 objectKey 数组;版本冻结资料 `GameDistributionFrozenMetadata` 删除 `cover_asset_id`,`screenshots` 改为 `Vec<String>`(objectKey),删除 `GameDistributionFrozenScreenshot.asset_id`。请求 DTO(create game / create version / update metadata)删除 `coverAssetId`,截图语义改为 objectKey。发布写路径改收 `multipart/form-data`,新图由服务端写项目快照桶 `agc/project-snapshots/v1/game-distribution/media/...`(不建 `asset_object`),公开读新增 `GET /api/game-distribution/media/read-url`;`game_distribution_asset_has_public_read_grant` 及其在 `editor_project_storage` 的调用删除,`request_digest` 覆盖元数据 + 图片字节 hash + 沿用 objectKey。硬切、无历史数据,但删字段仍须同步 `migration.rs`、表目录与生成绑定并运行 `npm run check:spacetime-schema`。
|
||||
- 发布媒体直传(2026-10-06):游戏行只存 `cover_object_key` 与 objectKey 数组 `screenshots_json`;版本冻结资料 `GameDistributionFrozenMetadata` 用 `coverObjectKey`(`String`)与 `screenshots`(`Vec<String>`,objectKey),`GameDistributionFrozenScreenshot` 不存在。请求 DTO(create game / create version / update metadata)用 `coverObjectKey` 与 `screenshots: (string|null)[]`:`string` 槽位沿用该作品当前媒体的 objectKey,`null` 槽位按序消费可重复的 `screenshot` 二进制 part,可选 `cover` part 出现时覆盖 `metadata.coverObjectKey`。公开读走 `GET /api/game-distribution/media/read-url`(及 `read-bytes`),判定为「已发布且 active 的 game + objectKey 命中 `cover_object_key` / `screenshots_json`」,不经素材库 ACL;公开读授权 procedure 只按 objectKey 查询(`get_game_distribution_media_read_access_and_return`)。`request_digest` 覆盖规范化元数据 + 图片字节 hash + 沿用 objectKey,覆盖 create / version / update 三条写路径。硬切、无历史数据;schema 变更仍须同步 `migration.rs`、表目录与生成绑定并运行 `npm run check:spacetime-schema`。
|
||||
|
||||
### `game_distribution_review`
|
||||
|
||||
@@ -540,8 +540,8 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`
|
||||
- 用途:不可变发行版本与真实包确认事实。创建后冻结 `package_sha256`、字节数、文件数、根入口和版本号;后续只推进上传、校验、审核、公开、撤回状态,并记录私有对象键、文件清单、入口 URL、审核者和阶段时间。
|
||||
- 索引:`by_game_distribution_version_game_id`、`by_game_distribution_version_owner_user_id`。真实 ZIP 由 `api-server` 校验并写入私有 OSS 后,才通过 facade 确认 `uploaded`;表不保存 ZIP 正文。
|
||||
- 冻结资料:版本表末尾追加可空 `metadata_json`,保存创建版本时由 api-server 校验(标题/简介/分类/标签/设备/方向/必需封面/≤6 张截图/买断制价格 `priceMudPoints`)并从素材记录派生对象键后的资料快照;`approve_game_distribution_version_and_return` 通过审核时把该快照整体生效到游戏行,因此公开投影展示的始终是“已随版本审核通过”的资料与价格,旧版本(无快照)保持原值。**价格口径统一为「冻结资料缺 `priceMudPoints` 即免费(0)」**:待审列表、审核详情的版本价与审核通过后生效的游戏行价格都按 `0` 处理,不会回退到游戏行旧价。<code>parse_game_distribution_frozen_metadata</code> 会用 <code>normalize_game_price_mud_points</code> 校验价格上限,越界快照在审核时失败关闭。
|
||||
- 作者回读投影:版本回读(作者本人)与审核回读(管理员)在版本 payload 上追加 `frozenMetadata`(冻结快照原样 JSON,历史版本为 `null`)。只有公开投影会剥掉素材 ID,作者与管理员拿到 `coverAssetId` / `screenshots[].assetId`,因此作者续发时可以直接复用同一批封面与截图素材,不需要为了沿用封面重新上传一次;素材 ID 缺失(旧版本)时前端必须要求作者重新选择封面,不能用对象键反推素材身份。
|
||||
- 冻结资料:版本表末尾追加可空 `metadata_json`,保存创建版本时由 api-server 校验(标题/简介/分类/标签/设备/方向/必需封面/≤6 张截图/买断制价格 `priceMudPoints`)并从创建请求确定 `coverObjectKey` / 截图 objectKey 后的资料快照;`approve_game_distribution_version_and_return` 通过审核时把该快照整体生效到游戏行,因此公开投影展示的始终是“已随版本审核通过”的资料与价格,旧版本(无快照)保持原值。**价格口径统一为「冻结资料缺 `priceMudPoints` 即免费(0)」**:待审列表、审核详情的版本价与审核通过后生效的游戏行价格都按 `0` 处理,不会回退到游戏行旧价。<code>parse_game_distribution_frozen_metadata</code> 会用 <code>normalize_game_price_mud_points</code> 校验价格上限,越界快照在审核时失败关闭。
|
||||
- 作者回读投影:版本回读(作者本人)与审核回读(管理员)在版本 payload 上追加 `frozenMetadata`(冻结快照原样 JSON,历史版本为 `null`)。冻结资料只含 `coverObjectKey` 与截图 objectKey 数组,作者续发时直接把这些 objectKey 放进 `metadata` 的沿用槽位,不需要为了沿用封面或截图重新上传;公开投影仍只暴露 objectKey,不含任何素材 ID。
|
||||
- 撤回与回读:`cancel_game_distribution_version_and_return` 只允许把未参与当前公开投影的版本推进到 `cancelled`,并要求 `expected_publication_revision` 与游戏公开修订号一致;`get_game_distribution_version_and_return` 供管理员按版本 ID 直读。客户端看到的 `recoveryAction` 由 `api-server` 按 `status` 派生,不落表。
|
||||
|
||||
### 后台游戏管理读模型与恢复动作(2026-09-23)
|
||||
|
||||
@@ -56,16 +56,16 @@
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 0.1 |
|
||||
| Status | 待实施 |
|
||||
| Status | current(实现已落地;真实 OSS 与真实发布 smoke 待验收) |
|
||||
| Date | 2026-10-06 |
|
||||
| 适用边界 | game-distribution 发布资料媒体(封面/截图)的上传与公开读取;发行包上传链路不变 |
|
||||
|
||||
- 写:`POST /api/game-distribution/games`、`POST /api/game-distribution/games/{gameId}/versions`、`PATCH /api/game-distribution/my-games/{gameId}` 接受 `multipart/form-data`:`metadata` 文本 part(JSON,含 `coverObjectKey` 与 `screenshots: (string|null)[]`);`cover` 二进制 part(可选,出现时覆盖 `coverObjectKey`);`screenshot` 二进制 parts(≤6 张,按 `null` 槽位顺序消费)。`string` 槽位表示沿用线上 objectKey。硬切,不再接受 `coverAssetId`。
|
||||
- 写:`POST /api/game-distribution/games`、`POST /api/game-distribution/games/{gameId}/versions`、`PATCH /api/game-distribution/my-games/{gameId}` 接受 `multipart/form-data`:`metadata` 文本 part(JSON,含 `coverObjectKey` 与 `screenshots: (string|null)[]`);`cover` 二进制 part(可选,出现时覆盖 `coverObjectKey`);`screenshot` 二进制 parts(≤6 张,按 `null` 槽位顺序消费)。`string` 槽位表示沿用线上 objectKey。硬切,不接受素材 ID 形式的媒体引用。
|
||||
- 存储:新图由服务端写入项目快照桶 `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>`,**不建 `asset_object`**;冻结资料与游戏行只存 `coverObjectKey` / 截图 objectKey。
|
||||
- 读:`GET /api/game-distribution/media/read-url`(匿名)按 objectKey 返回签名读地址;判定 = 已发布且 active 的 game 且 objectKey 命中其冻结媒体;需要同源字节时用 `.../media/read-bytes`。
|
||||
- 幂等:`request_digest` 覆盖规范化元数据 + 新图字节 hash + 沿用 objectKey,保持重放语义。
|
||||
- 归属:仅 owner 可写;沿用 objectKey 必须属于该游戏当前媒体;附图校验 `image/*`、张数与体积上限。
|
||||
- 与素材库解耦:公开读判定不再使用 `game_distribution_asset_has_public_read_grant`(删除);素材桶 `/api/assets/read-url` 的通用签名/字节中转抽公共 helper,新路由复用,授权仍域内分离。
|
||||
- 与素材库解耦:公开读判定只按游戏分发域内规则(已发布且 active 的 game + objectKey 命中冻结媒体),不经素材库 ACL;素材桶 `/api/assets/read-url` 的通用签名与字节中转抽成公共 helper 供新路由复用,授权仍域内分离。
|
||||
|
||||
## AGC 游戏分发与在线游玩合同
|
||||
|
||||
@@ -98,11 +98,11 @@
|
||||
|
||||
### 真实发行包与资料合同
|
||||
|
||||
1. AGC 发布取当前 npm 工程已成功构建的 `dist/` 内容,重新检查入口和实际字节;ZIP 内部必须把 `dist/index.html` 归一化为根 `index.html`,其余路径相对发行根保持不变。不得上传整个项目、源码快照或仅发送本地路径。网页 ZIP 同样要求根 `index.html`,不猜测并自动剥离多层目录。
|
||||
2. 所有运行依赖都必须在发行包内。资源 URL 使用与发行版本目录兼容的相对地址;前导 `/assets`、本地文件 URL、外部脚本/样式/媒体/字体地址均不属于可接受发行合同。AGC 发布归一化阶段只把包内已知根路径 `/assets/`、`/game/`、`/ui/` 转成相对引用,不修改项目源码;其它外部绝对地址仍由客户端/服务器拒绝,静态校验不能代替运行时 CSP 阻断。
|
||||
1. AGC 发布读取项目 `build:taonier` 产出的 `.export/taonier.zip`(`vite-export-taonier` skill 把 `dist-taonier` 打成存档根直接含 `index.html` 的 zip),服务端重新检查入口和实际字节。不得上传整个项目、源码快照或仅发送本地路径。网页上传 ZIP 同样要求根 `index.html`,不猜测并自动剥离多层目录。
|
||||
2. 所有运行依赖都必须在发行包内。资源 URL 使用与发行版本目录兼容的相对地址;前导 `/assets`、本地文件 URL、外部脚本/样式/媒体/字体地址均不属于可接受发行合同。`vite-export-taonier` 用 `base: './'` 生成相对路径,并在构建收尾(`assertH5Root`)拒绝仍引用根路径资源的 `index.html`,不重写项目源码;外部绝对地址由运行期最小权限 CSP 阻断,静态校验不能代替运行时 CSP 阻断。
|
||||
3. 建议首版限额:压缩包 100 MiB、展开总量 250 MiB、单文件 64 MiB、最多 10,000 个文件、展开/压缩比不超过 100。服务端拒绝加密 ZIP、重复或大小写冲突路径、绝对路径、`..`、符号链接/重解析点、设备文件和嵌套压缩包;拒绝 `.agent`、版本控制目录、`node_modules`、凭据文件与源码映射文件。超限返回明确错误,不截断后继续发布。
|
||||
4. 提交声明 ZIP 的 SHA-256 与字节数,服务端对收到的真实 ZIP 重新计算,再对展开文件建立相对路径、字节数和 SHA-256 清单。摘要不一致、缺文件或入口损坏时停止;只有 metadata 而没有已确认完整对象的提交必须失败。
|
||||
5. 游戏资料随发行版本冻结:标题 2–40 字、短简介不超过 120 字、详细介绍不超过 2,000 字、一个分类、最多 5 个标签(每个不超过 20 字)、必需封面、最多 6 张截图、操作方式不超过 240 字。分类首版为休闲、益智、动作、冒险、模拟、策略、其他;封面/截图复用平台图片上传与归属校验,不接受任意外链作为审核图片。作者不需要自己构建或打 ZIP:AGC 发布时对 `game/` 子工程按需执行 `npm install` 和 `npm run build`,将 `dist` 归一化为根 `index.html` ZIP;为兼容 AGC 上传素材的运行 URL,会补入项目根 `assets/**` 中 dist 未包含的文件,同路径以 dist 构建产物为准,不修改项目源码。
|
||||
5. 游戏资料随发行版本冻结:标题 2–40 字、短简介不超过 120 字、详细介绍不超过 2,000 字、一个分类、最多 5 个标签(每个不超过 20 字)、必需封面、最多 6 张截图、操作方式不超过 240 字。分类首版为休闲、益智、动作、冒险、模拟、策略、其他;封面/截图由发布写路径以 `multipart/form-data` 直传项目快照桶,沿用槽位必须命中该作品当前媒体的 objectKey,不接受任意外链作为审核图片。作者不需要自己构建或打 ZIP:导出产物面板运行项目声明的 `build:taonier`(`vite-export-taonier` skill 的 `vite.config.taonier.mjs` 构建到 `dist-taonier`、`pack.mjs` 打成存档根含 `index.html` 的 `.export/taonier.zip`),发布暂存固定读取该 zip。
|
||||
6. `supportedDevices` 至少包含 `desktop` 或 `mobile`;`inputModes` 来自 `keyboard`、`mouse`、`touch`;声明移动端必须包含 `touch`。`orientation` 为 `landscape`、`portrait` 或 `responsive`。这些是待人工复核的作者声明,目录只显示已经随版本审核通过的值。
|
||||
7. 原始 ZIP、未审核展开目录、审核资料均为私有对象;公开版本不暴露源码镜像键、本地路径、访问凭据或私有账号元数据。运行文件只能由发行网关按游戏、版本和文件白名单读取,不能绕过网关访问公开 OSS bucket。
|
||||
8. 现役发行网关由 `api-server` 提供:`GET /api/game-distribution/releases/{gameId}`(含尾斜杠)等价于该游戏的 `index.html`,`GET /api/game-distribution/releases/{gameId}/{assetPath}` 只服务当前已公开版本包内的文件,私有 ZIP 与未公开版本不因知道 ID 而可读。响应按扩展名白名单设定内容类型,未知扩展名返回 404;全部响应带 `X-Content-Type-Options: nosniff`、`Cross-Origin-Resource-Policy: cross-origin` 与不带 credentials 的 `Access-Control-Allow-Origin: *`(发行文档运行在 `allow-scripts` 的 opaque origin 沙箱里,`same-origin` 会让游戏自己的脚本被浏览器拦下),HTML 追加最小权限 CSP,并在游戏脚本前注入隔离的运行期 `localStorage` / `sessionStorage` 兼容层,避免游戏直接读取 opaque origin 原生 storage 时抛 `SecurityError`。兼容层只在当前运行实例内存中有效,不读取平台 Cookie、主站 DOM 或账号数据。公开发行与审核预览只拒绝真实平台 refresh Cookie;审核预览 Token 绑定单个版本且短期有效。发行包按对象键在进程内做有界缓存,单个超预算包不进入缓存。
|
||||
@@ -146,7 +146,7 @@
|
||||
| `GET /game-distribution/play-sessions/{token}[/{assetPath}]` | 凭播放令牌 | **已实现**:凭令牌读取当前公开版本包内文件,不读 Cookie(带可解析平台 refresh Cookie 的请求一律 403,边缘/dev 按该前缀清 Cookie),`no-store`;令牌不存在或过期、游戏下架/封禁、无有效公开版本一律 404 |
|
||||
| `GET /my-games` | 登录作者 | **已实现**:当前账号游戏、最近版本状态与驳回理由;owner 只从认证主体派生,单次最多 48 项 |
|
||||
| `GET /my-games/{gameId}` | 登录作者 | **已实现**:作者读自己名下单个游戏的详情,条目与 `GET /my-games` 同形(含全部版本私有状态、驳回理由与已公开版本的 `entryUrl`)。作者要能打开「审核中 / 被驳回 / 已下架 / 已撤回」的作品,公开详情只服务已公开投影,所以作者视角必须走这条 owner 作用域路由;游戏不存在或不属于当前主体都返回 404 |
|
||||
| `PATCH /my-games/{gameId}` | 登录作者 | **已实现**:作者编辑自己名下游戏的展示资料(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向),立即生效并落 `tracking_event` 审计;要求 `Idempotency-Key` 与 `expectedPublicationRevision` CAS,随版本冻结的包摘要与资料快照不受影响,缺封面/截图归属不符仍按创建口径拒绝 |
|
||||
| `PATCH /my-games/{gameId}` | 登录作者 | **已实现**:作者编辑自己名下游戏的展示资料(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向),立即生效并落 `tracking_event` 审计;要求 `Idempotency-Key` 与 `expectedPublicationRevision` CAS,随版本冻结的包摘要与资料快照不受影响,缺封面或沿用 objectKey 不命中该作品当前媒体仍按创建口径拒绝 |
|
||||
| `DELETE /my-games/{gameId}?expectedPublicationRevision=` | 登录作者 | **已实现**:作者软删除自己的作品。只写 `deleted_at` 并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料保留;作者列表/公开目录/公开详情/发行网关/审核队列与后台默认视图都不再返回,后台可用 `status=deleted` 查看。要求 `Idempotency-Key`(同 key 同请求返回原结果),不受发布灰度开关约束 |
|
||||
| `POST /games` | 登录作者 | **已实现**:幂等创建游戏身份,尚不公开;带 `localProjectId` 时同一作者复用既有 `gameId` |
|
||||
| `POST /games/{gameId}/versions` | owner | **已实现**:创建不可变待上传版本,冻结包摘要/字节数/文件数与资料 |
|
||||
@@ -529,7 +529,7 @@
|
||||
- 详情必须展示:发布者 ID/名称/头像、游戏标题、简介、详细介绍、分类、标签、操作方式、支持设备、输入方式、方向、封面、截图、版本号、包大小、文件数、SHA-256、提交时间、审核时间、审核理由和 `publicationRevision`。
|
||||
- 展示的游戏资料优先使用该版本冻结的 `metadata_json`;不能用审核期间作者后来修改的 game 行资料替代待审快照。
|
||||
- 发布者和游戏资料仅通过管理员受保护接口读取;公开目录不得因此增加作者私有字段或待审版本字段。
|
||||
- 封面与截图通过现有后台素材换签接口读取 Object Key,不把私有 Object Key 当作浏览器直链。
|
||||
- 封面与截图通过 `GET /api/game-distribution/media/read-url` 按 objectKey 换取签名读地址,不把私有 Object Key 当作浏览器直链。
|
||||
|
||||
### 待审版本试玩
|
||||
|
||||
|
||||
Reference in New Issue
Block a user