diff --git a/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md b/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md deleted file mode 100644 index e654bb348..000000000 --- a/docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md +++ /dev/null @@ -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 草稿文件 - -- 路径:`/.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//{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 deleted file mode 100644 index 9231f46e4..000000000 --- a/docs/project-memory/plans/【里程碑】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md +++ /dev/null @@ -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 依赖两者。 -- 每个里程碑验收通过前不得进入下一个;实现中行为变化按《规范驱动开发工作流》顺序回改主规范。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 2fe8e7123..6018d9008 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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//{cover|screenshot}-.`,不建 `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 保存。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 54d255a4e..0048d5630 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -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,包内容 / 目标版本 / 资料任一变化才换键;分包上传再用 `: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 对齐 + 浅暖色」修过一轮,最终整体删除。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 8ec3c21bf..7801fd433 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -2,28 +2,28 @@ ## 2026-10-06 陶泥儿导出文件草稿化与游戏分发媒体直传 -陶泥儿(Taonier)目标从「宿主打包 + 素材登记 + AI 预填」迁移到与小红书同构的**文件草稿 + agent 适配打包**模式;发布媒体改为向游戏分发 API **formdata 原始二进制直传**。 +陶泥儿(Taonier)导出采用与小红书同构的**文件草稿 + agent 适配打包**模式:草稿 `.export/taonier.json` 是唯一真相,打包由 `vite-export-taonier` skill 与项目 `build:taonier` 脚本完成,发布媒体向游戏分发 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)。 +- 打包:项目 `package.json` 提供 `build:taonier`(既有 `command.exec` 的 `build:*` 白名单),由 skill `vite-export-taonier` 的 `vite.config.taonier.mjs` 构建到 `dist-taonier`、`pack.mjs` 打成 `.export/taonier.zip`;**标准 `build`/`dist` 不被占用**。宿主只运行脚本并校验产物(普通文件、非空、非符号链接、能作为 ZIP 打开、根有 `index.html`、读取安全线 512 MiB),并按 `shared-contracts` 的 `GAME_DISTRIBUTION_MAX_PACKAGE_BYTES`(平台发行包上限 200 MiB)在构建收尾与发布读包两处预检,超限当场给「精简资源后重新导出再发布」,不等上传吃服务端 413;读取安全线与平台上限不是一个概念。 - 适配与错误:未适配弹「让陶泥儿帮我调通」;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 兼容。 +- 退役收口:`exports/` 目录、历史试玩包链、`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`(及 `read-bytes`),判定 = 已发布且 active 的 game 且 objectKey 命中封面/截图,不经素材库 ACL。硬切,无旧 JSON/assetId 兼容。 - 验收:草稿/打包/失败转发/沿用线上值与发布直传的定向测试;真实 vite 工程首轮适配与一次真实发布 smoke。 ## 2026-10-05 导出产物面板与小红书小工具导出 -导出产物面板(`view/project-development/export`)面向「把项目导出成第三方平台制品」,当前只交付小红书小工具(vite 项目)一个目标。边界是**宿主只做四件确定性的事**:读写项目内 `.export/` 下的注册表、校验表单与构建脚本是否存在、以既有 `command.exec` 边界运行项目自己的 npm 脚本、把产物交给用户;**适配与打包逻辑全部由 code agent 在首次导出时实验出来并固化成脚本**,宿主不复刻平台规范、不解析 zip 结构、不代跑 `pack.mjs`。 +导出产物面板(`view/project-development/export`)面向「把项目导出成第三方平台制品」,当前交付小红书小工具与陶泥儿两个目标(另有 `vite-export-taptaph5` WIP,未登记、未纳入验收)。边界是**宿主只做四件确定性的事**:读写项目内 `.export/` 下的注册表、校验表单与构建脚本是否存在、以既有 `command.exec` 边界运行项目自己的 npm 脚本、把产物交给用户;**适配与打包逻辑全部由 code agent 在首次导出时实验出来并固化成脚本**,宿主不复刻平台规范、不代跑 `pack.mjs`;陶泥儿额外只复核产物 zip 的根入口与体积门。 -- 工作目录:`EXPORT_WORK_RELATIVE_DIR = ".export"` 锚在**项目根**,所有目标共用一个 flat 目录。小红书小工具的文件为 `.export/xhs-minitool.json`(注册表)、`.export/xhs-minitool.zip`(交付产物)、`.export/xhs-minitool.pack.mjs` / `.export/xhs-minitool.validate.mjs` / `.export/xhs-minitool.vite.config.mjs`(agent 从未包 `vite-export-xhs-minitool` skill 显式拷贝,允许改)。共享 icon 放 `.export/` 根,注册表只存项目内相对路径,多目标可指同一文件。`.export/` 排除在**项目快照同步**之外(`PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS`),恢复或换机后适配成果不跟随,需要重新让 agent 适配;它**不进通用排除口径**(那份列表同时是 Agent 的写入门禁,并进去 `agc_apply_patch` 就会拒绝 `.export/` 路径,首次适配无法落地);本地 checkpoint 仍包含 `.export/`,可作为同机恢复适配脚本的兜底。发布包按白名单收集(`game/`、`assets/`、`exports/README.md`),`.export/` 进不去。 +- 工作目录:`EXPORT_WORK_RELATIVE_DIR = ".export"` 锚在**项目根**,所有目标共用一个 flat 目录。小红书小工具的文件为 `.export/xhs-minitool.json`(注册表)、`.export/xhs-minitool.zip`(交付产物)、`.export/xhs-minitool.pack.mjs` / `.export/xhs-minitool.validate.mjs` / `.export/xhs-minitool.vite.config.mjs`(agent 从未包 `vite-export-xhs-minitool` skill 显式拷贝,允许改)。共享 icon 放 `.export/` 根,注册表只存项目内相对路径,多目标可指同一文件。`.export/` 排除在**项目快照同步**之外(`PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS`),恢复或换机后适配成果不跟随,需要重新让 agent 适配;它**不进通用排除口径**(那份列表同时是 Agent 的写入门禁,并进去 `agc_apply_patch` 就会拒绝 `.export/` 路径,首次适配无法落地);本地 checkpoint 仍包含 `.export/`,可作为同机恢复适配脚本的兜底。发布暂存固定读 `.export/taonier.zip`,`.export/` 的注册表与脚本不进 manifest、素材、UI State 或客户端投影。 - 表单:`name`、`introduction` 各 ≤14 个 Unicode 字符且非空;`iconPath` 必须存在、是项目内普通文件、不是符号链接、扩展名 ∈ {png, jpg, jpeg}、≤5 MiB。上限来自小红书平台上传表单,宿主**硬校验**(不是提示);不支持的格式先直接拒绝,格式转换工具是后续事项(代码处留 TODO)。表单不是构建输入——zip 规范里没有任何 app 元数据;它的唯一用途是让用户复制粘贴到平台上传页。 - 注册表:首次打开该目标时由宿主自动创建,**所有字段为空字符串**。注册表不存 status、不存「是否已适配」、不存经验文本;agent 的经验写在它自己拷进项目的脚本注释里。「有没有脚本」永远现场从 npm 包的 `package.json` scripts 读取(`hasScript`),不落盘。 - 构建契约:项目 npm 包里存在脚本 `build:xhs-minitool`(冒号形式是既有 `command.exec` 白名单 `build:*` 的要求),由宿主负责运行;跑完必须存在 `.export/xhs-minitool.zip`。脚本内部怎么 `vite build`、怎么调 `pack.mjs`、用什么中间目录由 agent 决定并写进脚本;**每次导出都重新构建 zip**,不做「产物已存在」的短路。 - 首次适配:用户点显式按钮 → 前端组装适配指令(正文在 `view/project-development/export/state/xhsMinitoolInstruction.ts`,纯函数 + 契约常量)→ 经 `useDirectProjectChatController` 的 `chat.submit` 入队(该链路自带会话写权限门与 clientTurnId),进的就是同一个项目主 Direct 会话,不另开「直接调 agent」的旁路。不自动跑构建、不自动叫 agent;内容与脚本判据按固定间隔(2 秒,窗口在后台时跳过)自动重读,所以没有刷新按钮——agent 回合结束后用户只需要点「导出」。重读时两条事实分开处理:`hasScript` 是宿主的现算事实,任何时刻都采纳;表单是用户的编辑对象,只有用户手上没有未保存输入(也不在冲突里)时才覆盖,否则只更新 `hasScript`,绝不覆盖正在打的字。没有构建脚本时,「还没适配」的结论只由注册表读回来之后下(`hasScript` 初值是 `false`,读取中不下结论),提示卡片自带那颗入队按钮,按钮行里就不再重复一颗;失败卡片按变体的 `action` 分派三颗按钮——`adapt` 发首次适配指令(还没适配,修复指令会引用一份还不存在的适配说明)、`repair` 发同一份契约加宿主失败现场、`retry` 只重试,同一时刻只有一张卡片、一个入口。 - 命令切分:`read_xhs_minitool_export` 只返回**内容**(`form` + `hasScript`),自动刷新反复调的就是这一条;**注册表指纹**(写回时的 `baseHash`)由 `read_xhs_minitool_export_hash` 单独给,只在进编辑会话、或重读到的新内容真的被采纳时取一次。两者分开是刻意的:刷新这条高频路径在类型上就动不了写回基线,否则「顺手刷新」会把两次刷新之间的外部改动认成自己的基线、把冲突吞掉。指纹是同步细节,不进前端对外状态(hook 里只在 `baselineRef`),界面也不显示任何同步状态(没有「保存中」「待保存」文案)。`save_xhs_minitool_export_form` 成功后回新的指纹。 - 冲突:注册表可被 agent(改文件)与用户(面板编辑)两方写入。宿主保存时带 `baseHash`;宿主重读比对,hash 不同再逐字段 diff,只要有字段真的不同就判冲突,**绝不覆盖**,返回两侧值让前端逐字段选择;纯格式化改动静默吸收。注册表内不存 revision / updatedAt 之类 token。 -- 权限与副作用:跑脚本复用 `command.exec` 的权限口径(只查 deny,UI 按钮即用户确认),**不新增** `GAME_CREATION_APP_COMMANDS` 条目;导出链路**不拿项目写锁、不推进全局 revision**(`.export/` 不进 manifest、素材、UI State 或客户端投影)。发布包是白名单收集,`.export/` 不会进入。 +- 权限与副作用:跑脚本复用 `command.exec` 的权限口径(只查 deny,UI 按钮即用户确认),**不新增** `GAME_CREATION_APP_COMMANDS` 条目;导出链路**不拿项目写锁、不推进全局 revision**(`.export/` 不进 manifest、素材、UI State 或客户端投影)。发布暂存固定读 `.export/taonier.zip`,`.export/` 的其它文件不会进入发布包或项目正式状态。 - 非目标:平台自动上传(无 API)、PNG/JPG 之外的 icon 格式转换、宿主侧 zip 结构校验、非 vite 项目、第二个导出目标,以及 `GAME_CREATION_APP_COMMANDS` / `shared-contracts` / SpacetimeDB / OpenAPI 的任何改动。 - 验收:注册表自动建空表单与严格解析拒绝、表单首错、`hasScript` 判定、内容与指纹两条命令的切分(自动刷新拿不到写回基线)、`contentHash` 冲突与逐字段选择、icon 越界与符号链接拒绝、产物路径;自动重读不覆盖用户未保存的输入、agent 事后加上脚本不用点刷新就能亮起导出按钮;首次适配在真实 vite 项目上由 agent 跑通并产出可人工上传的 zip,二次导出零 agent 调用;变更 skill 脚本后指纹同步。 - 落地情况:Rust(`export/{mod,registry,draft/xhs_minitool/*}` 四个命令)、前端(`export/{state,tabs/xiaohongshu,generated}`)、skill(`pack.mjs --zip-out`)与 `.export/` 快照同步排除均已实现。定向单测与前端 vitest 通过,**真实 vite 项目上的首轮适配仍是唯一未验证项**。 @@ -743,7 +743,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 ## 目标 -在 Genarrative 内建设独立桌面 App:普通用户通过项目开发工作台中的陶泥儿对话、资源画布、运行状态和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览;主窗口提供运行时配置入口,用于保存发布版 AppData / Tauri 配置目录里的 LLM 配置及受控开发者 External Editor 配置,设置弹窗同时提供独立“关于”页并显示从客户端构建版本注入的版本号。普通客户素材画布使用平台登录态调用内部编辑器 API,不展示或要求填写画板 Base URL / API Key。任务明细、原始文件、命令日志和专业 Agent 调试控制只通过显式开发调试入口查看,不随普通客户端启动额外打开窗口。v1 的生成闭环仍以 Web 小游戏为主,同时允许用户打开已有 Godot 项目:用户选择的目录始终作为工作区根,`.agent/`、Session、Runtime、文件工具和外围资料都留在该根;客户端检查根目录及一层直接子目录中的普通文件 `project.godot`,将唯一命中的实际目录以工作区相对 `godotProjectRoot` 记录到 manifest。Agent 使用标准运行档继续修改,不创建 `game/`、`assets/`、`memory/`、`exports/` 平行目录;本期不扩展 Unity、Godot 内嵌预览、云同步或插件市场。新增的 Cocos Creator bridge 核心独立为插件 `plugins/agc-cocos-editor`(native 模块位于其 `native/cocos-editor-bridge`),AGC 仅通过通用插件宿主和 feature 转发桌面进程发现、受控 execute 和 Windows 注入能力;它不改变服务端路线,也不把原始 pipe、句柄或未绑定项目身份的代码执行面暴露给 Agent。 +在 Genarrative 内建设独立桌面 App:普通用户通过项目开发工作台中的陶泥儿对话、资源画布、运行状态和确认操作,让平台生成保存在本地的可运行 Web 游戏原型,并通过本地 HTTP server 预览;主窗口提供运行时配置入口,用于保存发布版 AppData / Tauri 配置目录里的 LLM 配置及受控开发者 External Editor 配置,设置弹窗同时提供独立“关于”页并显示从客户端构建版本注入的版本号。普通客户素材画布使用平台登录态调用内部编辑器 API,不展示或要求填写画板 Base URL / API Key。任务明细、原始文件、命令日志和专业 Agent 调试控制只通过显式开发调试入口查看,不随普通客户端启动额外打开窗口。v1 的生成闭环仍以 Web 小游戏为主,同时允许用户打开已有 Godot 项目:用户选择的目录始终作为工作区根,`.agent/`、Session、Runtime、文件工具和外围资料都留在该根;客户端检查根目录及一层直接子目录中的普通文件 `project.godot`,将唯一命中的实际目录以工作区相对 `godotProjectRoot` 记录到 manifest。Agent 使用标准运行档继续修改,不创建 `game/`、`assets/`、`memory/` 平行目录;本期不扩展 Unity、Godot 内嵌预览、云同步或插件市场。新增的 Cocos Creator bridge 核心独立为插件 `plugins/agc-cocos-editor`(native 模块位于其 `native/cocos-editor-bridge`),AGC 仅通过通用插件宿主和 feature 转发桌面进程发现、受控 execute 和 Windows 注入能力;它不改变服务端路线,也不把原始 pipe、句柄或未绑定项目身份的代码执行面暴露给 Agent。 ## 技术选择 @@ -1073,7 +1073,6 @@ game-project/ agents/ / .md - exports/ .agent/ agent.db manifest.json @@ -1277,7 +1276,7 @@ game-project/ - 登录检查、未登录和客户端页面加载失败三种认证外壳统一使用共享陶泥儿产品形象与平台浅色主题 token;登录卡片、输入框、方式切换和主按钮沿用平台暖色视觉,并在不超过 `480px` 的窄视口收紧留白。视觉优化不改变服务器选择、验证码、密码、登录态安装或错误恢复语义。 - `npm run agc` 的本地 SpacetimeDB owner identity 以独立 `spacetimeDataDir` 为作用域,不绑定可能漂移的监听端口;旧端口作用域记录仅在同一 data dir 下身份唯一时自动迁移,出现多个不同旧身份时失败关闭。`.app/dev-stack.json` 必须记录规范化 `spacetimeDataDir`,独立壳只复用数据库名和该目录同时匹配且健康的后端,旧 schema 状态或共享目录状态缺少此字段时不得复用。POSIX 子进程在 `spawn` 返回时立即登记 `error / exit` 生命周期、保存 detached leader 的 PGID 并把句柄交给外层;即使 direct leader 已先退出,也必须继续向负 PGID 发信号清理同组后代。后端 ready 前的 SIGINT、SIGTERM、超时或 ENOENT 都必须走同一进程组清理链路,不能遗留 npm、Cargo 或 SpacetimeDB。非 Linux Runtime 执行 `project.verify` 时,`npm run` 参数校验必须允许受控的 `--silent`、`--ignore-scripts` 位于脚本名前,并继续拒绝缺少真实脚本名的调用。 - Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。 -- 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。 +- 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。 - “打开项目”自动识别 Godot:先检查用户所选工作区根的普通文件 `project.godot`;根未命中时只检查一层直接子目录。根命中优先,子目录只有唯一命中才通过;多个子工程时必须在任何 `.agent` 写入前明确报错,要求用户改选具体工程,不能猜测。候选目录与 `project.godot` 都拒绝符号链接和 Windows reparse point,二层及更深目录不递归。首次导入只在用户所选工作区根初始化 `.agent/manifest.json`、`.agent/agent.db`、`.agent/logs/` 与 `.agent/runtime/`,manifest 以 `.` 或单段直接子目录名记录可移植的 `godotProjectRoot`;Agent DB 的 `project.import` 同步记录该相对根。后续项目上下文、文件工具、Session、Runner 和 Agent Runtime 始终使用工作区根;只有未来 Godot 专属启动、构建、测试或导出动作才以经过校验的 Godot 根作为相对 cwd。已有 `.agent/manifest.json` 缺少或记录错误的 `godotProjectRoot` 时,仅在 Godot 打开边界按唯一文件布局校准;歧义时不改写 manifest。 - v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。 - 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果,也可对当前 run 执行取消 / 重试,待确认 run 还可执行“确认继续”或“拒绝并继续”。后台任务会向 `.agent/runtime/tasks/.jsonl` 追加任务视角记录,任务状态使用 `pending / running / waiting-for-confirmation / cancelled / completed / failed`,读取时按 `runId` 去重返回最近任务;`runId` 在同一 Agent 内是单个 run 的身份,后台入队会自动把重复 runId 改写为唯一实际 runId,防止不同任务互相覆盖;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行;若某个工具动作命中确认策略,该 Agent 会停在 `waiting-for-confirmation` 并暂停继续消费队列,等待后续确认或策略调整;Runtime 会把完整 `AgentRuntimePendingToolAction` 经过敏感内容和项目绝对路径校验后原子写入 `.agent/runtime/pending-actions//.json`,公共 `pendingToolAction` 只公开安全摘要;确认或拒绝必须匹配 `runId + actionId` 并通过工具名与完整输入 JSON 的 SHA-256 校验。确认在同一 run 直接执行原 action 并把 observation 接回后续 loop;拒绝不执行工具,而是写入 `blocked` observation 后在同一 run 继续规划。待确认状态可跨 App 重启读取并回收上一进程锁;等待期间同 Agent 新任务保持 pending,确认/拒绝续跑结束后由同一 drain 串行排空;若用户取消 pending 任务,drain 不再消费该 run,若取消 running 任务,则在当前 LLM 或工具调用返回后的检查点停止,不继续执行工具或保存最终 assistant 回复。客户端重开项目时会对当前项目路径自动尝试一次 Runtime 恢复;恢复命令必须通过 `agent.resume` 自动权限,默认确认策略下不会静默启动;同一 Agent 同时存在上一进程遗留 `running` 和 `pending` 时,先重接 `running`,再由 drain 继续 `pending`。开发构建和后台 Agent 工具箱都可通过 `agent.schedule_ready` 权限确认入口把 manifest ready task 投递进同一后台队列,命令会先把 ready task 标成 `running`,再用 taskId 作为 Agent id 入队,source 为 `agent-ready-task-scheduler`;该入口不新增独立 worker。后台任务的核心 loop 每 6 轮形成一个上下文压缩窗口:每轮把已有 observation 带回 LLM 让 Agent 重新规划;只有合法工具计划返回空 actions,且不存在未通过或项目修改后未重跑的 `project.verify` 时才收束,response 为空时进入独立最终回复生成。窗口边界会压缩 observation;有新的独立观察时在同一 run 继续下一窗口,最近窗口重复无进展时才以 `budget-exhausted / loop-budget-exhausted` 失败。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.background_task.recovered` / `agent.runtime.ready_task.scheduled` / `agent.runtime.tool_observation` / `agent.runtime.tool_confirmation_required` / `agent.runtime.tool_confirmation.approved` / `agent.runtime.tool_confirmation.rejected` / `agent.runtime.memory.write` / `agent.runtime.project.verify` / `agent.runtime.file.write` / `agent.runtime.file.patch` / `agent.runtime.file.delete` / `agent.runtime.tool_plan.repair` / `agent.runtime.task.create` / `agent.runtime.task.update` / `agent.runtime.command.run_limited` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.agent.delegate` / `agent.runtime.background_task.cancelled` / `agent.runtime.background_task.retry` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.search`、`project.diff`、`file.list`、`file.read`、`task.list`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`project.checkpoint`、`project.restore`、`project.verify`、`file.write`、`file.patch`、`file.delete`、`task.create`、`task.update`、`command.run_limited`、`preview.start`、`canvas.asset_generate`、`blackboard.write`、`agent.message`、`agent.delegate` 和 `agent.schedule_ready`;`memory.write scope=agent` 只允许写当前 Agent 自己的私有记忆,跨 Agent 共享必须改用 `blackboard.write` 或 `agent.message`;`project.checkpoint` 只创建本地 checkpoint,不返回本机绝对路径;`project.restore` 只按 checkpoint id 恢复当前项目,不返回本机绝对路径,默认确认策略下不会静默回滚;`file.delete` 只删除项目内普通文件,默认确认且不能访问 `.agent/**`;`task.create` 只追加新 manifest 任务,`task.update` 只更新已有任务状态;`agent.schedule_ready` 只调度 manifest ready task,不创建平行 runtime;若项目策略拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent 修正计划;若项目策略要求确认,Runtime 会持久化精确待确认动作并保留 waiting 状态,不执行该工具;只有确认入口通过 `runId + actionId + SHA-256` 校验后才直接执行原 action,拒绝入口则生成 `blocked` observation。`toolPolicy` 保存当前工具级权限快照,供 planning prompt 和状态面板展示;`recentToolCalls` 保存最近 20 条结构化工具动作及安全 `inputSummary`,供状态面板展示最近动作和确认目标;append-only JSONL 写入按目标文件路径在当前进程内串行追加完整行,覆盖 `.agent/agent.db`、对话、Runtime events/tasks、activity 和 output,减少多个 Agent 同时完成时的行交错风险。 @@ -1333,14 +1332,14 @@ game-project/ - `.agent/run.latest.json` 增加可选 `lifecycleStatus`,把一次生成 run 映射到本地最小生命周期:`scheduled / running / waiting / pending / done / failed / killed`;该字段与 run 的 `.agent/activity.jsonl` / `.agent/output.jsonl` / `.agent/context.bundle.json` 写入由 Runtime 维护。v1 只做本地状态控制,不承诺真正中断已在上游执行中的 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。 - v1 的 agent 状态列表和单 agent 对话都复用上述本地文件事实源:状态从 manifest / run trace 派生,单 agent 消息写对应 Session conversation JSONL;Session 支持创建、切换和只改元数据的归档,不引入对话 fork,也不把 `pending` 包装成已经具备后台 claim / resume runner。 - `game.generate_draft` 写入最终产物后会复用白名单受限命令 `game.static_smoke` 做一次生成后自检,至少检查 `game/index.html` 包含 canvas、canvas 渲染上下文、绘制调用、主循环、非空输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval`、`new Function`、`localStorage`、`fetch`、`WebSocket` 或 `ServiceWorker`,也不得包含固定星核传送门模板词、纯按钮计分模板或 `TODO` / `待实现` / `这里省略` 等未完成实现;画板资源占位引用允许出现在 asset id 或说明中,并把该工具调用写入 `.agent/run.latest.json` 与 `.agent/logs/command.log`;自检失败则本次命令失败,不继续启动预览。 -- `ArtifactWriter` step 使用 `file.write.local_artifacts` 工具调用记录最终写入的 `memory/`、`memory/agents/`、`game/`、`assets/`、`exports/` 和 `.agent/manifest.json` 路径;写入完成后 `nextStep` 指向 `game.static_smoke`。 -- `preview.start` / `preview.stop` 会追加 `.agent/logs/preview.log`,并在 `.agent/run.latest.json` 已存在时追加 `Preview` step 和 `preview.*` toolCall,记录本地 HTTP 预览 URL 与停止事件;单全局本地预览被新项目替换时,会 best-effort 把旧项目 manifest、preview log 和 trace 记录为 stopped,避免旧项目残留 running;本地 HTTP server 的 `/` 映射到 `game/index.html`,只允许读取 canonical 后仍位于项目真实 `game/` 或真实 `assets/` 下的文件,拒绝 `memory/`、`.agent/`、`exports/`、`..`、一级 `game` / `assets` 符号链接目录和内部符号链接越界,并为常见图片、音频、视频和 Web 资源返回对应 MIME;静态 `HEAD` 返回真实 `Content-Length` 但不返回 body,确保浏览器和媒体资源探测可用;上传和画板回流资产可被生成游戏引用但不会暴露记忆或 trace;没有 run trace 的手动预览启动不阻断。 +- `ArtifactWriter` step 使用 `file.write.local_artifacts` 工具调用记录最终写入的 `memory/`、`memory/agents/`、`game/`、`assets/` 和 `.agent/manifest.json` 路径;写入完成后 `nextStep` 指向 `game.static_smoke`。 +- `preview.start` / `preview.stop` 会追加 `.agent/logs/preview.log`,并在 `.agent/run.latest.json` 已存在时追加 `Preview` step 和 `preview.*` toolCall,记录本地 HTTP 预览 URL 与停止事件;单全局本地预览被新项目替换时,会 best-effort 把旧项目 manifest、preview log 和 trace 记录为 stopped,避免旧项目残留 running;本地 HTTP server 的 `/` 映射到 `game/index.html`,只允许读取 canonical 后仍位于项目真实 `game/` 或真实 `assets/` 下的文件,拒绝 `memory/`、`.agent/`、`..`、一级 `game` / `assets` 符号链接目录和内部符号链接越界,并为常见图片、音频、视频和 Web 资源返回对应 MIME;静态 `HEAD` 返回真实 `Content-Length` 但不返回 body,确保浏览器和媒体资源探测可用;上传和画板回流资产可被生成游戏引用但不会暴露记忆或 trace;没有 run trace 的手动预览启动不阻断。 - 当前单窗口项目开发页的普通聊天文本只进入 `project-supervisor` 的 active Session;匹配 Session 已有非终态 run 时转为 same-run steer,不切换父 Session/run。它读取当前 Supervisor Session、只读兼容的 legacy 项目对话、短期 / 长期记忆、项目黑板、本地资产和有界仓库上下文,可以正常澄清、使用 Runtime 白名单工具、按项目权限请求确认,并把专业工作委派给现有 Agent;同一父 run 同时活跃的静态专业委派不超过 3 个。新的 user 消息和最终 assistant 只由 Runtime 写入 Supervisor Session,流式草稿只作可丢失展示缓存;React 不再把同一轮写回 legacy 项目对话。静态 delivery/claim 与 isolated all-join 都必须由同一父 run 收齐、认领并完成 observation 持久化后才允许形成唯一最终回复;专业 Agent 的内部对话、工具计划和动态 child 不直接展示给普通用户。显式 `/generate <创作想法>` 或 `/draft <创作想法>` 继续保留为确定性旧生成 loop 的待确认快捷命令;它不替代 Supervisor 的自主 Runtime。用户确认旧生成命令后,正式用户聊天仍展示 Planner、Orchestrator、6 组角色 brief、Generator、Evaluator、ArtifactWriter 和 `game.static_smoke` 的真实进度,并按原契约写入本地产物与记忆。 - `game.generate_draft` 的 `game/index.html` 必须是可试玩原型,至少包含输入、主循环、目标、失败或胜利状态和重开路径;不能只输出按钮计分或纯展示页。 - `game.generate_draft` 会校验 LLM 输出:`balance`、美术清单和音乐清单必须是 JSON object,`gameHtml` 必须是自包含 HTML、包含 `canvas` 与 `requestAnimationFrame`,不得加载远程脚本或资源,不得使用 `eval` / `new Function` / `localStorage` / `fetch` / `WebSocket` / `ServiceWorker`,不得把包含 `<` / `>` 的用户输入原样写入 HTML。 - 同一项目内多次 `game.generate_draft` 不覆盖记忆文件,而是继续追加短期对话记录、长期创作目标记录、项目黑板摘要和角色私有摘要,保留用户迭代历史。 - `game.generate_draft` 会把本轮协作写入 `.agent/logs/agent.log` 和 manifest `commandRuns`,并将已完成的组内角色任务标记为完成,`preview-readiness` 在生成后等待本地 `game.static_smoke`,预览试玩任务等待用户确认。 -- `game.generate_draft` 会把组内角色 brief、专业组汇总 brief、专业组 / 角色 / 产物交接摘要写入 `.agent/passes/pass-N/groups/`、`game/game_design.md`、`exports/README.md` 和 `.agent/logs/agent.log`,用于追踪 6 组 agent 的最小协作结果。 +- `game.generate_draft` 会把组内角色 brief、专业组汇总 brief、专业组 / 角色 / 产物交接摘要写入 `.agent/passes/pass-N/groups/`、`game/game_design.md` 和 `.agent/logs/agent.log`,用于追踪 6 组 agent 的最小协作结果。 - 文件选择会生成待确认的 `asset.upload` 内置命令;用户确认后才把文件写入 `assets/uploads/` 并登记到 manifest。 - `game.generate_draft` 会从 `.agent/manifest.json` 派生本地资产摘要,把已上传、已登记和画板回流资产的 id、类型、媒体类型、本地路径、来源和画板追踪 ID 注入 Planner / 组内角色 / Generator 共用的上下文;资产摘要排在长期记忆前,避免长期记忆过长时被 prompt 截断;不新增平行资产记忆文件。 - 主窗口“灵感草稿”只把一条可编辑的小游戏需求写入聊天输入框,不提交、不创建项目、不调用 LLM;用户仍需自行发送并确认生成命令。 @@ -1356,8 +1355,7 @@ game-project/ - 聊天输入 `/commands` 会只读列出 Tauri runtime 暴露的受限命令白名单,读取失败或非 Tauri 环境下回退到共享契约默认列表;该命令不执行白名单命令,也不要求先初始化项目。 - 聊天输入 `/smoke` 会生成待确认的 `command.run_limited` 内置命令,当前只映射到白名单 `game.static_smoke`,不开放任意命令解析。 - 聊天输入 `/run` 会生成待确认的 `game.run_local` 内置命令,确认后复用白名单 `game.static_smoke` 运行当前 `game/index.html`,通过后启动只读本地 HTTP 预览并切换到客户端内运行视图;该命令不开放任意 shell。 -- 聊天输入 `/export` 会生成待确认的 `project.export_package` 内置命令,确认后只把 `game/**`、`assets/**` 和 `exports/README.md` 打包到 `exports/playtest-package-*.zip`;缺少 `exports/README.md` 时先按项目 manifest 生成最小试玩说明,已有文件原样保留。发布前若 `code-prototype` 未完成且没有运行中的预览,直接阻止发布,不触发用户项目构建;可运行原型完成后才允许按 `build` 脚本补齐产物。发布进度使用独立模态弹窗展示,遮罩覆盖整个工作区并阻止交互,不再使用聊天确认卡;导出前重新校验可玩入口,拒绝符号链接和越界路径,不把 `.agent/`、`memory/`、日志、trace、运行时配置或密钥文件写入 ZIP。 -- 聊天输入 `/exports` 会只读执行 `project.export_list`,列出当前项目 `exports/playtest-package-*.zip` 历史试玩包,并提供显示目录或继续 `/export` 的草稿;该命令不删除文件、不分享文件、不新增面板。 +- 游戏分发发布复用 `project.export_package` 权限位:导出产物面板以 `.export/taonier.json` 为草稿、由项目 `build:taonier` 产出 `.export/taonier.zip`,发布暂存固定读取该 zip,不再生成或列举旧试玩包。发布进度使用独立模态弹窗展示,遮罩覆盖整个工作区并阻止交互,导出前重新校验可玩入口,拒绝符号链接和越界路径,不把 `.agent/`、`memory/`、日志、trace、运行时配置或密钥文件写入 ZIP。 - 聊天输入 `/preview` 会生成待确认的 `preview.start` 内置命令,确认后启动只读本地 HTTP 预览并切换到客户端内运行视图;`/open-preview` 在本地项目已初始化后生成待确认的 `preview.open`,只激活当前授权项目对应的 `127.0.0.1` 运行容器;`/preview-status` 只查询当前授权项目的本地 HTTP 预览并写入 `preview.status` 命令日志;`/preview-stop` 只停止当前项目预览,不展示或停止其它项目遗留的全局预览。 - 短期、长期和黑板记忆由现役 Runtime 工具与原生读写入口维护;记忆斜杠命令已按 2026-09-22 退役 ADR 清理,未接入正式调用的本地记忆删除 helper 及专属测试一并移除,不删除用户现存记忆文件。 - 聊天输入 `/canvas 画板项目ID` 会生成待确认的 `canvas.project_open`,只打开本机 Genarrative 编辑器里的指定画板项目,不开放任意 URL;确认后聊天先反馈正在打开,再回写真实打开 URL。画板项目 ID 为空或包含控制字符时在聊天侧直接拒绝。 @@ -1579,7 +1577,7 @@ game-project/ - 首页提供“做游戏 / 做方案”两个创作类型(`game` / `doc`),默认“做游戏”;“做素材”入口已退役,素材生成在项目内按实际工作流触发。每次首页提交自动创建一个新项目并进入项目工作台。用户正文原样进入项目对话,`game|doc` 作为受限结构化首轮上下文传给同一 Codex thread;持久草稿里遗留的 `art` 按 `game` 处理(`effectiveCreationType` 映射),不再产生第三种首轮上下文。 - 「策划补全」是“做游戏”专属的提交前勾选(2026-09-15 起):勾选时该档提交 `planning`,未勾选时提交 `direct-build`;“做方案”始终走立项策划链路,与本勾选无关。复选框只在“做游戏”档渲染;切换创作类型时首页表面整体重挂,勾选状态随之清除,切回来必须是未勾选——这条可观察契约由 `appSurface` 的「scopes the 策划补全 option to the game entry」与「submits the game entry with planning when 策划补全 is checked」两条用例钉住,实现侧不额外维护重置逻辑。 - 2026-09-23 清理了未注册的 DirectHome 用户对话命令及旧附件 sidecar 渲染链;首页仍先创建项目再进入 DirectProject,不恢复无项目对话。自动项目命名和提示润色仍调用内部 `direct_game_creator_home_codex_chat`,其 DirectHome 只读隔离通道与测试继续保留。附件作为 canonical `userItem.content` 中的 `agc_attachment_reference` 携带名称、媒体类型、大小、项目相对路径及状态,经现役 validation/wire 校验与投影;路径映射不等于灌入全文,也不按 GDD 特判。附件清洗与数量上限继续复用 `direct_codex_attachments.rs`,旧 sidecar DTO、header、专属 prompt key 和测试不再是保留合同。 -- `agc-skill-pack.v1` 包含完整游戏交付流程、项目结构、陶泥儿美术、Web 游戏实现、真实浏览器试玩、客户端资源投影、Unity/Godot 编辑器常用操作、平台抽象收口和小红书小工具导出十项审核 Skill。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。同步统一运行 `npm run agc:skill-pack:sync`,只读校验由 AGC `typecheck` 和 release build 自动执行,发现漂移时直接列出 Skill 与实际摘要,不让失配内容进入构建产物。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown;把 Skill 自带脚本或配置原样复制进项目时用 `agc_install_skill_resource`(skillName/relativePath/destinationPath),宿主直接读内置审核字节并按 `agc_write_file` 的入参规则与合同 lease 写盘,正文不经过模型上下文,避免大文件截断和原生 `cp` 审批。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。Skill 目录里以 `.` 开头的文件和目录是本地开发辅助(例如 `vite-export-xhs-minitool/.selective_rule.txt`、`scripts/.pack.test.mjs`):不写进清单、不参与指纹、不安装,也不能经 `agc_read_skill_resource` 读取;JS 清单工具 `isHiddenSkillEntryName` 与 Rust `is_hidden_skill_entry_name` 共用同一拒绝口径,新增 Skill 时无需为它们补 `include_bytes!`。 +- `agc-skill-pack.v1` 包含完整游戏交付流程、项目结构、陶泥儿美术、陶泥儿导出、Web 游戏实现、真实浏览器试玩、客户端资源投影、Unity/Godot 编辑器常用操作、平台抽象收口和小红书小工具导出十一项审核 Skill。未登记的 `vite-export-taptaph5` 是另一条 WIP,不进入清单,`npm run agc:skill-pack:check` 对它报红属于已知接受项。清单记录用途、触发条件、所需工具、版本和内容 SHA-256;审核文本按 UTF-8 读取并将 CRLF 规范为 LF 后计算指纹和安装,避免混合换行造成 Windows / Linux 构建结果漂移,语义内容变化时必须同步重算对应清单指纹并提升版本。同步统一运行 `npm run agc:skill-pack:sync`,只读校验由 AGC `typecheck` 和 release build 自动执行,发现漂移时直接列出 Skill 与实际摘要,不让失配内容进入构建产物。客户端把审核文件安装到隔离目录后通过 app-server `skills/extraRoots/set + skills/list` 注册并复核,完整正文由 Codex 原生 Skill 机制按意图加载,一层引用只能经 `agc_read_skill_resource` 读取清单内 Markdown;把 Skill 自带脚本或配置原样复制进项目时用 `agc_install_skill_resource`(skillName/relativePath/destinationPath),宿主直接读内置审核字节并按 `agc_write_file` 的入参规则与合同 lease 写盘,正文不经过模型上下文,避免大文件截断和原生 `cp` 审批。引用路径按平台无关规则拒绝反斜杠、盘符、UNC、绝对路径和 `..`,不能依赖当前宿主的 `std::path` 语义判断其它平台路径。Skill 目录里以 `.` 开头的文件和目录是本地开发辅助(例如 `vite-export-xhs-minitool/.selective_rule.txt`、`scripts/.pack.test.mjs`):不写进清单、不参与指纹、不安装,也不能经 `agc_read_skill_resource` 读取;JS 清单工具 `isHiddenSkillEntryName` 与 Rust `is_hidden_skill_entry_name` 共用同一拒绝口径,新增 Skill 时无需为它们补 `include_bytes!`。 - DirectProject 连接客户端内置的 `agc_tools` STDIO MCP,并在启动时接入客户端扩展仓库中用户已启用的独立第三方 STDIO/HTTP MCP 配置。内置工具包括审核引用读取、图片生成、标准陶泥儿美术准备、已登记资源有界查询、视频 / 角色动画 / 音效 / BGM 的 create-or-derive 语义生成、已登记图片去背景、desktop/mobile 浏览器试玩和受控 `agc_web_search`。内置 MCP 进程负责协议;真实浏览器、付费平台调用与受控搜索通过随机 loopback 地址回到客户端主进程,GUI 登录态、开发者 Key、项目路径、revision、operation 与幂等键由客户端持有并隔离于模型上下文。内置与用户启用的第三方 MCP 工具沿用 DirectProject 自动批准方式;付费资源工具由客户端绑定稳定回合身份、串行执行并优先恢复匹配账本。`llm.webSearchEnabled` 控制 DirectProject 的 AGC 受控搜索工具暴露与执行。原生工具与审批权限以下方“DirectProject Codex 完整访问覆盖”为准。 - 陶泥儿生成复用持久幂等账本、operation 恢复、来源/下载/PNG 解码和 manifest 登记;普通客户端使用当前 AGC 登录会话及账号路由,受控的 ExternalDeveloper 发布模式在客户端内部使用按服务器 origin 隔离的私有 Key。凭据失效、来源不明或结果未知时失败关闭,不能自动换 Key 或重新扣费。 - 自定义 LLM API Key 路由在 DirectProject 及内部 DirectHome 辅助调用中经 loopback `/responses` 流式代理转发。代理使用请求自带的 Bearer,并剥离开发网关错误携带的 `X-Codex-*` ChatGPT 账户额度头,按实际 API Provider 响应判断请求结果。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index b583e1743..a9179bef2 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -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//{cover|screenshot}-.` 并只回 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`(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`,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` 处理,不会回退到游戏行旧价。parse_game_distribution_frozen_metadata 会用 normalize_game_price_mud_points 校验价格上限,越界快照在审核时失败关闭。 -- 作者回读投影:版本回读(作者本人)与审核回读(管理员)在版本 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` 处理,不会回退到游戏行旧价。parse_game_distribution_frozen_metadata 会用 normalize_game_price_mud_points 校验价格上限,越界快照在审核时失败关闭。 +- 作者回读投影:版本回读(作者本人)与审核回读(管理员)在版本 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) diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index db197810b..5d90058f7 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -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//{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,新路由复用,授权仍域内分离。 +- 与素材库解耦:公开读判定只按游戏分发域内规则(已发布且 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 当作浏览器直链。 ### 待审版本试玩