diff --git a/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md b/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md new file mode 100644 index 000000000..d7a396474 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md @@ -0,0 +1,226 @@ +# 【实施计划】陶泥儿导出文件草稿化与发布媒体直传 + +- 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 草稿文件 + +- 路径:`/.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`。 +- 字段:元数据 JSON 文本 part;`cover` 二进制 part(可选);`coverObjectKey` 文本 part(沿用线上封面时);`screenshots` 文本 JSON 数组(沿用线上的 objectKey 列表);截图二进制 parts(新增/替换,≤6 张)。 +- 存储:新图直接落**项目快照桶** `agc/project-snapshots/v1/game-distribution/media//{cover|screenshot}-.`(与发行包同桶、与素材库解耦,不建 `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`(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)。实现中若发现行为需变化,按《规范驱动开发工作流》顺序回改:主规范 → 未完成里程碑规范 → 当前实现计划 → 代码与测试。 diff --git a/docs/project-memory/plans/【里程碑】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md b/docs/project-memory/plans/【里程碑】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md new file mode 100644 index 000000000..9231f46e4 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md @@ -0,0 +1,33 @@ +# 【里程碑】陶泥儿导出文件草稿化与发布媒体直传 + +- 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 依赖两者。 +- 每个里程碑验收通过前不得进入下一个;实现中行为变化按《规范驱动开发工作流》顺序回改主规范。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e9144cb56..2fe8e7123 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -9897,3 +9897,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - Codex 15/110 分钟事件静默与 120 分钟硬上限、Claude 独立超时保持不变;取消、回合归属、并发、交付复核次数与进程退出证明继续有效。 - 正式执行账本为 v4,旧上限只在已发布数据的加载迁移边界识别;不保留旧运行时路径。App 管理配置安全写回,共享只读配置不改源文件;旧终态与报告不重开。 - 权威行为与验证边界见 [AGC 主规范](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#direct-宿主运行时间上限移除2026-10-06)。 + +## 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`。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 1b544abe4..8ec3c21bf 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -1,5 +1,17 @@ # AI 游戏创作智能体 App 实施计划 +## 2026-10-06 陶泥儿导出文件草稿化与游戏分发媒体直传 + +陶泥儿(Taonier)目标从「宿主打包 + 素材登记 + AI 预填」迁移到与小红书同构的**文件草稿 + agent 适配打包**模式;发布媒体改为向游戏分发 API **formdata 原始二进制直传**。 + +- 草稿:`.export/taonier.json`,形状 `{ form: { title, summary, category, coverPath, screenshotPaths[] } }`;内容 hash 冲突逐字段选择,宿主零业务校验,校验只在前端 `taonierFields`。 +- 打包:项目 `package.json` 提供 `build:taonier`(既有 `command.exec` 的 `build:*` 白名单),由 skill `vite-export-taonier` 的 `vite.config.taonier.mjs` 构建到 `dist-taonier`、`pack.mjs` 打成 `.export/taonier.zip`;**标准 `build`/`dist` 不被占用**。宿主只运行脚本并校验产物(非空、非符号链接、根有 `index.html`、≤200 MiB)。 +- 适配与错误:未适配弹「让陶泥儿帮我调通」;adapt/fill/repair 三类指令与小红书一致,失败现场原样转发 code agent;「帮我填」只写元数据,不改打包适配。 +- 线上值:草稿是唯一真相;读命令额外返回只读线上值(live game 行),不自动 seed/回填;文案字段与图片均可「沿用线上值」,图片路径为空表示沿用(发布时带线上 objectKey)。 +- 退役:`exports/` 整体退役(目录创建、`exports/README.md` 生成、playtest 包读写、`prepare/upload/list` 命令、生成任务产物声明、prompts、run-trace 白名单与排除列表);AI 资料建议与封面生成、`useGameDistributionPublishForm`/`GameDistributionPublishFormView`/`GamePublishBlockedDialog` 一并删除,保留发布进度对话框。 +- 发布媒体:`POST /api/game-distribution/games`、`POST .../versions`、`PATCH .../my-games/{id}` 收 `multipart/form-data`(元数据文本 + 封面/截图原始二进制 + 沿用 objectKey);整包仍走 raw/chunked。图片落项目快照桶 `agc/project-snapshots/v1/game-distribution/media/...`,不建 `asset_object`;公开读走新路由 `GET /api/game-distribution/media/read-url`。硬切,无旧 JSON/assetId 兼容。 +- 验收:草稿/打包/失败转发/沿用线上值与发布直传的定向测试;真实 vite 工程首轮适配与一次真实发布 smoke。 + ## 2026-10-05 导出产物面板与小红书小工具导出 导出产物面板(`view/project-development/export`)面向「把项目导出成第三方平台制品」,当前只交付小红书小工具(vite 项目)一个目标。边界是**宿主只做四件确定性的事**:读写项目内 `.export/` 下的注册表、校验表单与构建脚本是否存在、以既有 `command.exec` 边界运行项目自己的 npm 脚本、把产物交给用户;**适配与打包逻辑全部由 code agent 在首次导出时实验出来并固化成脚本**,宿主不复刻平台规范、不解析 zip 结构、不代跑 `pack.mjs`。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index d963fee35..b583e1743 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -506,6 +506,8 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - 购买与播放鉴权 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`(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`。 + ### `game_distribution_review` - Rust 结构体:`GameDistributionReview`,源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`;私有表,仅通过受信 API 服务身份的 procedure 与 BFF 提供评价投影。 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 4ea954452..5c736a021 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -51,6 +51,22 @@ - 供应商密钥、Token、Cookie 和本地私密路径不得进入前端或文档示例。 - 生成或导入资产必须经过后端鉴权、对象确认和换签读取;上传失败或换签失败时显示可操作错误,不回退为公开裸路径。 +## 游戏分发发布媒体直传合同(2026-10-06) + +| 字段 | 值 | +| --- | --- | +| Version | 0.1 | +| Status | 待实施 | +| 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`:元数据 JSON 文本 part;`cover` 二进制 part(可选);`coverObjectKey` 文本 part(沿用线上封面);`screenshots` 文本 JSON 数组(沿用线上 objectKey);截图二进制 parts(≤6 张)。硬切,不再接受 `coverAssetId`。 +- 存储:新图由服务端写入项目快照桶 `agc/project-snapshots/v1/game-distribution/media//{cover|screenshot}-.`,**不建 `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,新路由复用,授权仍域内分离。 + ## AGC 游戏分发与在线游玩合同 | 字段 | 值 |