diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 9361d272f..02859c19e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -8189,11 +8189,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 问题回答携带被回答卡片的 questionId,在已有回合锁内核对 Session 和当前问题;自由文本回答同样绑定问题,已完成回合保留幂等重放。此身份匹配服务于用户提交,不增加恢复门禁或模型输出要求。 - hydrate 结果(包括空结果)写入前端状态前同时核对请求序列和当前项目路径;过期结果直接丢弃,不重试、不阻塞正常 run。 -## 2026-09-09 AGC asset-canvas 精修草稿整体退役(四不写) +## 2026-09-09 AGC 图片编辑改走资源派生,asset-canvas 精修草稿退役(四不写) -- V3.0 决策取消"单个图片的精修草稿":图片编辑统一走 `derive_local_project_resource` 的 `editKind='image-reference'`,结果固定创建新素材,源素材不变。旧草稿画布按四不写整体删除,不保留兼容实现、墓碑注释或防御性测试。 +- 决策:V3.0 取消"单个图片的精修草稿"这一形态。理由有三:草稿画布把"编辑中间态"做成了需要持久化、锁、revision CAS、恢复对账和事务 journal 的第二套业务真相,成本与出错面远超它提供的价值;素材不可变(编辑产出新素材)与替换队列已经覆盖同一批用户场景,不需要原地改稿;网站美术编辑器与客户端各维护一份草稿画布,长期必然分叉。因此图片编辑统一走 `derive_local_project_resource` 的 `editKind='image-reference'`,结果固定创建新素材,源素材不变,旧草稿画布按四不写整体删除。 +- 现权威口径:图片及其它类型的来源核验、请求 DTO、幂等与恢复、资源身份血缘以新增的 `docs/technical/【技术方案】AGC资源派生与非破坏性编辑合同-2026-09-09.md` 为准;资源画布交互、投影与焦点竞态以 `docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md` 为准;共享画布包、Host Port 与共享 chrome 以 `docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md` 为准;工作台状态机以 `docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md` 为准。原《客户端素材创作无限画布阶段一合同》整篇退役。 - 删除范围:Rust `project/asset_canvas.rs`、`project/asset_canvas/generation.rs`、`project/asset_canvas_tests.rs`、`project.rs` 的模块声明与再导出、`commands.rs` 的 18 个 Tauri 命令、`main.rs` 的 18 行注册;前端 `features/asset-canvas/` 四个文件、`index.tsx` 的草稿路由与提交事件链路、`styles.css` 的 `resources.asset-canvas` 选择器、`tests/assetCanvasSurface.test.tsx` 与相关草稿用例。 - 保留项:`canvas.asset_generate` 命令契约(`packages/shared` 与 `shared-contracts` 的正式权限合同,Agent Runtime 仍在使用);`asset-canvas-references` 与 `legacyPrefix=generated-character-drafts` 是 api-server 直传票据白名单契约,服务端白名单本次不动;`packages/image-canvas-core|react` 继续服务网站美术编辑器与资源画本视口;manifest 资产来源 `kind='canvas'`(网页画板项目)与本次退役无关。 - 路由口径:`resolveProjectResourceEditCapability` 不再返回 `image-canvas`,PNG/JPEG/WebP 与其他图片类型一样走 `derive + image-reference`,不再要求先正规化为正式 manifest asset。 -- 临时项:`scripts/check-config.mjs` 的 native-only 白名单新增 `normalize_local_project_raster_resource`(原前端调用方随草稿画布删除);资源画布重建快速编辑后重新获得调用方,届时删除该白名单条目。 +- 过渡项(带删除触发条件):`scripts/check-config.mjs` 的 native-only 白名单新增 `normalize_local_project_raster_resource`,因为原前端调用方随草稿画布一起删除。资源画布重建快速编辑后,该命令重新获得前端调用方,届时删除这条 allowlist 条目。 - 存量数据:`.agent/workbench/asset-canvas/` 不写清理代码,不做迁移;只对目录做一次性手工清理,`.agent/workbench/` 下 resource-edits、replacement-queue 等在役目录不得整体删除。 diff --git a/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md b/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md deleted file mode 100644 index f6b36ac0a..000000000 --- a/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md +++ /dev/null @@ -1,1011 +0,0 @@ -# 客户端素材创作无限画布阶段一合同 - -更新时间:`2026-08-15` - -状态:阶段一产品与技术合同已冻结;截至 2026-08-15,普通客户远端编辑统一使用陶泥儿网站登录态与固定官方服务地址,泥点计费链路、中央进度/失败态、Tauri 正式资产提交及画布 UI 对齐阶段三至五已落地。当前产品入口按第 16 节禁用新增,并把编辑入口扩展到资源总览的全部现役类型;后续增量仍受本文合同约束。 - -本文是网站与 AI 游戏创作 Tauri 客户端共享图片画布能力的下一阶段编码依据。若本文与资源管理阶段七的“美术编辑暂缓”口径冲突,以本文对后续素材创作切片的更新决定为准;资源总览既有布局、依赖图和只读聚焦合同继续有效。 - -## 1. 冻结结论与范围 - -1. 项目工作台中央主视窗必须把“资源总览画布”和“素材创作无限画布”建模为两个不同状态;两者复用同一个工作台壳,不创建平行项目页。 -2. 资源总览顶部的“新增资源”和图片资源聚焦态的“精修资源”进入素材创作无限画布。资源总览卡片仍不可拖动;素材创作无限画布中的图片图层必须可选择、移动和缩放,二者不是同一种交互。 -3. 阶段一正式闭环只覆盖图片。PNG、JPEG、WebP 的导入、画布平移与缩放、单选与多选、图层移动与缩放、层序、显隐、锁定、翻转、分组、撤销与重做、裁剪/扩图等基础编辑、图片生成、导出和 Tauri 本地正式回写均属于目标,不得再列为非目标。 -4. 现有一键去背景能力可以通过共享 Host Port 接入;毛发级抠图、可编辑蒙版和高级边缘修复后续分期。图集、角色动画、视频时间线编辑和音频波形级编辑不进入本阶段正式闭环;视频源引用派生和音频语义重制按第 16 节执行。 -5. 网站和 Tauri 必须实际 import 同一份画布 core 与 React/UI 源码。现役 `src/components/image-editor/` 是抽取来源,不得把整个目录复制到客户端,也不得形成网站版和 Tauri 版两份长期分叉的画布实现。 -6. 网站继续负责账户、钱包、服务端编辑器项目和云端素材库;Tauri 继续负责本地项目、受控文件读写、manifest、项目 mutation revision,并以当前平台登录态复用同一服务端编辑器与计费链路。第三方 Agent/CLI 使用的 External Editor API 是独立开发者通道,不是普通客户素材画布的鉴权方式。共享画布不知道这些事实来自哪个宿主。 -7. 本阶段不修改 SpacetimeDB schema,不新增前端业务真相,不把草稿 sidecar 当成正式资产。 - -## 2. 中央主视窗状态与入口 - -运行时状态固定为: - -```ts -type AssetCanvasIntent = 'create' | 'refine'; - -type ProjectWorkbenchCenterState = - | { kind: 'resource-overview' } - | { - kind: 'asset-canvas'; - sessionId: string; - draftId: string; - intent: AssetCanvasIntent; - sourceAssetId: string | null; - } - | { kind: 'run' }; -``` - -- `sessionId` 是每次进入素材画布时生成的规范小写 UUID v4,只用于当前前端生命周期和迟到结果门禁,不写入草稿或 manifest。 -- `draftId` 是规范小写 UUID v4,是可恢复草稿身份;重新打开同一草稿时保留 `draftId`,但必须生成新的 `sessionId`。 -- `create` 的 `sourceAssetId` 必须为 `null`;`refine` 必须指向当前 manifest 中唯一存在的图片资产 ID,缺失、重复或非图片资产都不得打开精修流程。 -- “新增资源”创建 `create` 草稿;“精修资源”创建 `refine` 草稿并把源图片作为首个锁定前可编辑图层载入。进入素材画布只替换中央主视窗,左侧导航、右侧 Supervisor 和底部 Agent 状态栏继续存在。 -- 素材画布退出到资源总览时恢复进入前的 dependency/type 模式、搜索、筛选和滚动上下文;素材画布自己的 viewport、图层和选择来自草稿合同,不写入资源总览布局 sidecar。 - -## 3. 共用源码与宿主边界 - -### 3.1 目标目录与依赖方向 - -下一阶段固定抽取为: - -```text -packages/image-canvas-core/src/ -packages/image-canvas-react/src/ -src/components/image-editor/host/webImageCanvasHostAdapter.ts -apps/ai-game-creator-shell/src/features/asset-canvas/tauriImageCanvasHostAdapter.ts -``` - -- 包名固定为 `@genarrative/image-canvas-core` 和 `@genarrative/image-canvas-react`。 -- `image-canvas-core` 只含纯 TypeScript 的画布模型、几何、选择、图层命令、历史、序列化、防御校验和状态机;不得依赖 React、DOM、Tauri、HTTP、账号、钱包或浏览器存储。 -- `image-canvas-react` 只含 React 视图、hooks、交互控制器和通用 UI,依赖 core 和注入的 Host Port;不得直接 import Tauri API、站点请求客户端、账户 store 或钱包 store。 -- 网站 adapter 可以依赖账户、钱包、现有服务端 editor project、云端素材库、OSS/asset object 和生成 API。 -- 依赖方向只能是“宿主 adapter -> React/UI -> core”。core/react 不得反向 import 任一宿主。 - -### 3.2 禁止复制的验收门 - -- 现役 `src/components/image-editor/` 中的通用模型、hooks 和视图应移动或抽取到上述共享包,网站改为 import 共享包;不得先完整复制到 `apps/ai-game-creator-shell` 再各自维护。 -- 网站与 Tauri 对相同画布命令、序列化 fixture 和交互状态机必须运行同一组共享测试。宿主测试只覆盖 adapter 差异。 -- 阶段完成时,客户端目录不得出现共享包已有文件的镜像副本;允许存在只负责 Tauri 命令、错误翻译和能力注入的薄 adapter。 - -### 3.3 Host Port - -共享 React 只依赖以下语义,不依赖具体传输: - -```ts -type ImageCanvasHostKind = 'web' | 'tauri'; - -type ImageCanvasHostCapabilities = { - account: boolean; - wallet: boolean; - cloudAssetLibrary: boolean; - localProject: boolean; - externalEditorGeneration: boolean; - advancedBackgroundRemoval: boolean; -}; - -type ImageCanvasHostScope = { - projectId: string; - draftId: string; - intent: 'create' | 'refine'; - sourceAssetId: string | null; -}; - -type ImageCanvasHostResult = - | { status: 'ok'; value: T } - | { - status: 'unsupported-capability'; - capability: keyof ImageCanvasHostCapabilities; - message: string; - } - | { - status: 'conflict'; - conflictKind: 'project-identity' | 'host-revision' | 'draft-revision'; - draft: AssetCanvasDraft | null; - hostRevision: string | null; - } - | { status: 'failed'; code: string; message: string }; - -type ImageCanvasHostInputImage = { - name: string; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - bytes: Uint8Array; -}; - -type ImageCanvasHostImage = { - mediaRef: AssetCanvasMediaRef; - previewUrl: string; - resourceId: string | null; -}; - -type ImageCanvasHostGenerationResult = { - generation: AssetCanvasGenerationRecord; - images: ImageCanvasHostImage[]; -}; - -type ImageCanvasHostCommitResult = { - resourceId: string; - draftRevision: number; - hostRevision: string; -}; - -interface ImageCanvasHostPort { - readonly kind: ImageCanvasHostKind; - readonly capabilities: ImageCanvasHostCapabilities; - loadDraft( - input: ImageCanvasHostScope, - ): Promise>; - createDraft( - input: ImageCanvasHostScope, - ): Promise>; - updateDraft(input: { - scope: ImageCanvasHostScope; - expectedDraftRevision: number; - status: 'editing' | 'generating' | 'cancelled'; - canvas: AssetCanvasDraft['canvas']; - generations: AssetCanvasGenerationRecord[]; - }): Promise>; - importImages(input: { - scope: ImageCanvasHostScope; - expectedDraftRevision: number; - images: ImageCanvasHostInputImage[]; - }): Promise>; - generateImage(input: { - scope: ImageCanvasHostScope; - expectedDraftRevision: number; - generationId: string; - idempotencyKey: string; - prompt: string; - referenceResourceIds: string[]; - }): Promise>; - removeBackground(input: { - scope: ImageCanvasHostScope; - expectedDraftRevision: number; - generationId: string; - idempotencyKey: string; - sourceLayerId: string; - }): Promise>; - exportImage(input: { - scope: ImageCanvasHostScope; - name: string; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - quality: number | null; - bytes: Uint8Array; - }): Promise< - ImageCanvasHostResult<{ - disposition: 'downloaded' | 'saved'; - byteLength: number; - }> - >; - commitImage(input: { - scope: ImageCanvasHostScope; - expectedHostRevision: string; - expectedDraftRevision: number; - commitId: string; - idempotencyKey: string; - name: string; - assetKind: string; - referenceResourceIds: string[]; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - bytes: Uint8Array; - }): Promise>; - discardDraft(input: { - scope: ImageCanvasHostScope; - expectedDraftRevision: number; - }): Promise>; -} -``` - -- 方法必须存在;能力不可用时返回共享的结构化 `unsupported-capability` 结果,不得靠方法缺失、捕获任意异常或宿主名称分支判断。 -- `previewUrl` 只供当前 React 生命周期显示,可以是 object URL、受控 data URL 或短期读 URL,但不得进入 core 序列化、草稿、history、manifest 或日志。`mediaRef` 才是可持久身份;Tauri 的 `project-asset.assetId` 是 manifest asset ID,网站由 adapter 映射为现有服务端项目资源 ID。 -- `hostRevision` 是宿主权威提交版本的字符串表示:Tauri 使用十进制项目 mutation revision,网站使用现有服务端 editor project revision。共享 UI 只透传/展示,不比较不同宿主的 revision。 -- `commitId/idempotencyKey` 由共享流程在第一次正式保存前生成;响应未知时两宿主都复用原完整请求。`expectedHostRevision` 由 adapter 从已加载的权威宿主快照提供,Tauri 必须无损解析为本文的安全整数 `expectedRevision`。 -- Web adapter 把草稿、导入、生成、导出和提交映射到现有服务端 editor project、云端素材库及账户/钱包链路。 - -### 3.4 主站 UI 对齐与共享画布 chrome - -实现状态(2026-08-06,阶段一至五):以下四个共享组件及作用域样式已经进入 `@genarrative/image-canvas-react`;主站默认 `EditorIconButton`、底部画布工具栏及画布内直接使用的常规图标/短标签动作已经消费共享按钮,网站专属 `surfaceFloating` / `darkMini` 仍保留薄适配。客户端 `AssetCanvasSurface` 的常规动作、三组工具、两个分隔符、保存/生成/失败动作和浮动缩放栏也已直接消费同一 chrome;宿主只保留业务回调、字段、面板布局和平台 token 映射。 - -第一批通用视觉组件固定由 `@genarrative/image-canvas-react` 暴露: - -```ts -CanvasChromeButton; -CanvasToolbar; -CanvasToolbarGroup; -CanvasToolbarDivider; -``` - -- `CanvasChromeButton` 统一原生 button 的可访问名称、tooltip、`aria-pressed`、`aria-expanded`、禁用态和画布 action 视觉;图标、短文案、业务事件和禁用条件由宿主传入。共享组件不得 import Lucide、平台账号 store、钱包 store 或宿主请求客户端。 -- `CanvasToolbar` 只负责 `role=toolbar`、可访问名称、plain/floating surface 和横向收纳;`CanvasToolbarGroup`、`CanvasToolbarDivider` 只表达动作分组,不知道具体工具 ID 或业务顺序。 -- 共享 CSS 继续位于 `packages/image-canvas-react/src/styles.css`,选择器必须以 `.genarrative-image-canvas` 前缀开头。颜色优先消费宿主继承的 `--image-canvas-brand-*`,再回退 `--platform-*`,最后才使用包内安全默认值。 -- 主站现役 `EditorIconButton` 可以保留为传入 Lucide icon 的薄 adapter,但默认画布按钮必须委托 `CanvasChromeButton`;特殊网站浮层或暗色媒体按钮仍可留在宿主,不能为了共享视觉把网站 portal、账号或媒体业务下沉。 -- 主站底部画布工具栏必须委托 `CanvasToolbar`、`CanvasToolbarGroup` 和 `CanvasToolbarDivider`,位置、引导态和工具 option hover 仍由网站宿主控制。共享抽取完成后,主站业务 CSS 只保留定位和主站专属状态,不重复维护通用按钮、浮动 surface、分组和分隔符样式。 -- Tauri 直接消费同一组件,且没有把生成、保存、草稿、manifest、账号、钱包、HTTP 或 Runtime 逻辑带入共享包;这些事实继续由现有 Host Adapter 和宿主回调负责。 -- 主题事实源仍是 `packages/shared/src/theme.css`。Tauri 只能导入共享主题、`@genarrative/image-canvas-react/styles.css` 和自身宿主样式,禁止导入网站整包 `src/index.css`。 -- 共享组件测试必须覆盖名称、默认 `type=button`、pressed/expanded、禁用、带短标签动作、floating toolbar surface 和 ref/class/event 透传;主站测试继续锁定工具列表、选中态和 option tool 的 hover/focus 行为。 - -## 4. 图片首版能力边界 - -### 4.1 必须交付 - -- 画布 viewport 平移、缩放、适配内容和复位。 -- PNG/JPEG/WebP 导入;媒体正文由宿主保存,core 只持有受控引用和元数据。 -- 图片图层单选、多选、框选、移动、等比/非等比缩放、层序、显隐、锁定、水平/垂直翻转和分组/解组。 -- 撤销/重做最多保留当前会话 `60` 个历史步骤;历史覆盖图层和 viewport 命令,但不把远端生成请求重新发送。 -- 裁剪、扩图、画布背景和现有可复用的一键去背景;这些操作的结果保存为新的草稿媒体引用,不能把 base64 媒体塞入 JSON。 -- 图片生成的提交、轮询、失败和恢复;相同生成幂等键不得重复扣费或重复创建任务。 -- PNG/JPEG/WebP 导出。网站使用浏览器下载/云端资产链路,Tauri 使用系统保存对话框或正式本地资产提交,均不得让共享 UI 接受任意绝对输出路径。 -- Tauri 正式保存后立即进入 manifest 投影、依赖图、两种布局协调和选中流程。 - -### 4.2 后续分期 - -- 高级蒙版、毛发级抠图、逐像素笔刷和可编辑边缘通道。 -- 图集切片/打包、角色动画和 image-sequence 编辑。 -- 视频时间线、转码、字幕和音轨编辑。 -- 音频剪辑、混音、波形和效果器。 - -这些后续项不得阻塞图片正式闭环,也不得以保留旧“全部美术编辑非目标”的文字把图片闭环再次关闭。 - -## 5. 项目身份、资源身份与容量 - -### 5.1 项目身份 - -- `projectPath` 只作为 Tauri command 输入,用既有安全路径能力解析为项目根;不得写入草稿、commit ledger、transaction journal 或 manifest。 -- 权威项目身份是当前项目根 `.agent/manifest.json` 的 `projectId`。 -- 权威项目 mutation revision 是 `.agent/runtime/project-revision.json` 的 `game-creator-project-revision.v1.revision`。本文所有跨 JSON/Tauri/TypeScript 的 revision 都限制在 `0..=9_007_199_254_740_991`。 -- 所有写命令必须携带 `expectedProjectId`;Tauri 在创建目录、锁文件或临时文件前只读验证一次,在取得对应系统锁后再次验证。路径被重建成另一个项目时必须零副作用失败。 - -### 5.2 资源身份与血缘 - -- manifest asset ID 与资源关系 ID 是两个命名空间。`referenceResourceIds` 只保存 `GameCreationAppAssetManifestEntry.source.resourceId`,绝不直接保存裸 manifest asset ID。 -- 本地资产缺少 `source.resourceId` 时,规范资源身份固定为 `local-asset:`。精修提交必须在同一 manifest 事务中为源资产补齐该值,并让新资产的 `referenceResourceIds` 包含它。 -- 新的本地画布资产 ID 固定为 `canvas-`,其 `source.resourceId` 固定为 `local-asset:canvas-`。 -- `refine` 默认保留源资产文件和 manifest 条目,新建一项资产;禁止覆盖源文件或复用源 asset ID。`referenceResourceIds` 至少包含源资产的规范资源身份,并可追加本次直接引用的其它唯一资源身份。 -- `create` 可以没有引用;只要使用了 manifest/云端参考资源,就必须把直接引用的规范资源身份登记到 `referenceResourceIds`。数组稳定去重,最多 `128` 项,每项 `1..512` 个 Unicode 字符,禁止空白和控制字符。 - -### 5.3 固定容量 - -- 单个草稿 JSON 最大 `2 MiB`,最多 `4096` 个图片图层、`64` 条生成记录和 `128` 个直接血缘引用。 -- 单个导入、生成结果、staging 或正式输出文件最大 `64 MiB`;图片宽高分别为 `1..16384`,总像素不超过 `268_435_456`。 -- 单个草稿的受控媒体总量最大 `512 MiB`。达到上限时拒绝新媒体,不删除仍被草稿引用的文件。 -- layer title 最大 `200` 个 Unicode 字符,生成 prompt 最大 `32_000` 个 Unicode 字符,`assetKind` 必须匹配 `[a-z0-9][a-z0-9._-]{0,63}`。 -- 草稿、commit 和 transaction JSON 中禁止 `data:`、`blob:`、带签名 URL、Cookie、API Key、绝对路径和媒体正文。 - -## 6. 草稿 sidecar 合同 - -### 6.1 路径与锁 - -```text -.agent/workbench/asset-canvas/ -├─ .drafts.lock -├─ drafts/ -│ ├─ .json -│ └─ .recovery/ -│ ├─ .json -│ └─ .sha256 -├─ media//. -├─ staging//image. -├─ commits/.json -└─ transactions// - ├─ journal.json - ├─ manifest.before.json - ├─ manifest.after.json - ├─ project-revision.before.json - └─ project-revision.after.json -``` - -- 草稿读写使用持久 `.drafts.lock` 的 OS 句柄互斥;Unix 使用 `flock`,Windows 使用不共享句柄。应用不得按 mtime、PID 或文本内容判断 stale,也不得删除锁文件来抢锁。 -- 草稿/媒体写入使用项目安全相对路径、普通文件与链接检查、同目录临时文件和原子替换。只读草稿不存在时不得创建目录、锁或空草稿。 -- `mediaId`、`stagedImageToken` 由 Tauri 生成,是不含路径语义的 256-bit base64url 随机值。扩展名只能由已验证媒体类型映射为 `png/jpg/webp`。 - -### 6.2 schema - -schema 固定为 `game-creator-asset-canvas-draft.v1`: - -```ts -type AssetCanvasDraftStatus = - | 'editing' - | 'generating' - | 'commit-prepared' - | 'committed' - | 'cancelled' - | 'reconciliation-required'; - -type AssetCanvasMediaRef = - | { kind: 'project-asset'; assetId: string } - | { - kind: 'draft-media'; - mediaId: string; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - sha256: string; - byteLength: number; - pixelWidth: number; - pixelHeight: number; - }; - -type AssetCanvasLayer = { - layerId: string; - resourceId: string; - title: string; - mediaRef: AssetCanvasMediaRef; - x: number; - y: number; - width: number; - height: number; - originalWidth: number; - originalHeight: number; - zIndex: number; - groupId: string | null; - hidden: boolean; - locked: boolean; - flipX: boolean; - flipY: boolean; -}; - -type AssetCanvasGenerationRecord = { - generationId: string; - idempotencyKey: string; - status: 'accepted' | 'polling' | 'completed' | 'failed'; - prompt: string; - operationId: string | null; - referenceResourceIds: string[]; - outputMediaIds: string[]; - errorCode: string | null; - createdAt: number; - updatedAt: number; -}; - -type AssetCanvasDraft = { - schemaVersion: 'game-creator-asset-canvas-draft.v1'; - draftId: string; - projectId: string; - intent: 'create' | 'refine'; - sourceAssetId: string | null; - sourceResourceId: string | null; - revision: number; - status: AssetCanvasDraftStatus; - canvas: { - viewport: { x: number; y: number; scale: number }; - backgroundColor: string; - layers: AssetCanvasLayer[]; - selectedLayerIds: string[]; - primarySelectedLayerId: string | null; - }; - generations: AssetCanvasGenerationRecord[]; - pendingCommit: { - commitId: string; - idempotencyKey: string; - requestFingerprint: string; - } | null; - lastCommit: { - commitId: string; - idempotencyKey: string; - assetId: string; - eventId: string; - committedProjectRevision: number; - } | null; - createdAt: number; - updatedAt: number; -}; -``` - -### 6.3 字段与 revision 规则 - -- `draftId`、`generationId` 使用规范小写 UUID v4;数组内 `layerId/resourceId/generationId/mediaId/zIndex` 分别唯一。 -- create 必须 `sourceAssetId/sourceResourceId=null`;refine 必须同时保存源 manifest asset ID 和其规范资源身份。 -- `revision` 从 `0` 开始,每次成功草稿 CAS、生成状态落账、取消状态或正式 commit 状态更新严格增加 `1`。Tauri 生成 `updatedAt`;前端不得提交 revision 或时间戳的新值。 -- viewport `x/y`、图层几何必须为有限数;`scale` 固定在 `0.025..3.2`,宽高必须为正,`zIndex` 为非负安全整数。选择只能引用当前未隐藏图层,primary 必须为 selected 的成员或 `null`。 -- sidecar 保存当前画布快照,不保存撤销/重做栈。崩溃恢复后以恢复快照建立新的 clean history 基线;不得把 60 份全量图层快照写入 sidecar 击穿容量。 -- 裁剪、扩图、去背景和生成结果生成新的 `draft-media`;旧媒体只有在没有任何当前草稿、恢复副本、staging、commit 或 transaction 引用后才可受控垃圾回收。 - -### 6.4 读取、更新与恢复 - -- 读取不存在的草稿返回 `{ status: 'not-found' }`,不得合成新草稿。 -- 主文件损坏、未知 schema、超限、媒体摘要不匹配或 project/draft 身份不匹配时失败关闭,不能用默认空草稿覆盖。 -- 每次原子安装前把上一份已验证草稿写入 `.recovery/.json`,并把其字节 SHA-256 写入同名 `.sha256`。只有恢复文件摘要正确、schema 支持、`projectId/draftId` 精确匹配且全部受控媒体仍可验证时才允许恢复;否则进入 `reconciliation-required`,不得按“文件看起来较新”猜测。 -- 更新必须携带 `expectedProjectId + draftId + expectedDraftRevision`。CAS 冲突返回最新完整草稿;前端不得解析错误字符串。两个窗口同时编辑同一草稿时最多一个 revision 更新成功,失败窗口必须载入最新草稿或显式另存为新草稿。 - -## 7. Tauri 草稿、媒体与 staging 命令 - -命令名固定为: - -```text -create_local_project_asset_canvas_draft -read_local_project_asset_canvas_draft -update_local_project_asset_canvas_draft -store_local_project_asset_canvas_media -stage_local_project_asset_canvas_image -discard_local_project_asset_canvas_draft -recover_local_project_asset_canvas_transactions -commit_local_project_asset -``` - -关键输入/结果固定为: - -```ts -type CreateLocalProjectAssetCanvasDraftInput = { - projectPath: string; - expectedProjectId: string; - draftId: string; - intent: 'create' | 'refine'; - sourceAssetId: string | null; -}; - -type CreateLocalProjectAssetCanvasDraftResult = { - status: 'created' | 'existing'; - draft: AssetCanvasDraft; -}; - -type ReadLocalProjectAssetCanvasDraftInput = { - projectPath: string; - expectedProjectId: string; - draftId: string; -}; - -type ReadLocalProjectAssetCanvasDraftResult = - | { status: 'found'; draft: AssetCanvasDraft } - | { status: 'not-found'; draft: null } - | { status: 'project-identity-conflict'; draft: null }; - -type UpdateLocalProjectAssetCanvasDraftInput = { - projectPath: string; - expectedProjectId: string; - draftId: string; - expectedDraftRevision: number; - status: AssetCanvasDraftStatus; - canvas: AssetCanvasDraft['canvas']; - generations: AssetCanvasGenerationRecord[]; -}; - -type UpdateLocalProjectAssetCanvasDraftResult = - | { status: 'updated'; draft: AssetCanvasDraft } - | { status: 'conflict'; draft: AssetCanvasDraft }; - -type StageLocalProjectAssetCanvasImageInput = { - projectPath: string; - expectedProjectId: string; - draftId: string; - expectedDraftRevision: number; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - bytes: number[]; -}; - -type StageLocalProjectAssetCanvasImageResult = - | { - status: 'staged'; - stagedImageToken: string; - draftId: string; - draftRevision: number; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - sha256: string; - byteLength: number; - pixelWidth: number; - pixelHeight: number; - expiresAt: number; - } - | { status: 'conflict'; draft: AssetCanvasDraft }; - -type StoreLocalProjectAssetCanvasMediaInput = { - projectPath: string; - expectedProjectId: string; - draftId: string; - expectedDraftRevision: number; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - bytes: number[]; -}; - -type StoreLocalProjectAssetCanvasMediaResult = - | { - status: 'stored'; - draftId: string; - draftRevision: number; - mediaRef: Extract; - } - | { status: 'conflict'; draft: AssetCanvasDraft }; - -type DiscardLocalProjectAssetCanvasDraftInput = { - projectPath: string; - expectedProjectId: string; - draftId: string; - expectedDraftRevision: number; -}; - -type DiscardLocalProjectAssetCanvasDraftResult = - | { status: 'cancelled'; draft: AssetCanvasDraft } - | { status: 'conflict'; draft: AssetCanvasDraft } - | { status: 'commit-in-progress'; draft: AssetCanvasDraft }; - -type RecoverLocalProjectAssetCanvasTransactionsInput = { - projectPath: string; - expectedProjectId: string; -}; - -type RecoverLocalProjectAssetCanvasTransactionOutcome = { - commitId: string; - status: - | 'committed' - | 'already-committed' - | 'rolled-back' - | 'reconciliation-required'; - eventId: string | null; - assetId: string | null; -}; - -type RecoverLocalProjectAssetCanvasTransactionsResult = { - projectId: string; - projectRevision: number; - manifest: GameCreationAppManifest; - outcomes: RecoverLocalProjectAssetCanvasTransactionOutcome[]; -}; -``` - -- create 成功固定返回 revision `0` 的完整草稿;同 draftId 已存在且身份/初始请求相同则幂等返回现有草稿,不同则失败关闭。refine 创建时从当前 manifest 解析源图片并计算规范 `sourceResourceId`,但直到正式 commit 才为缺失身份的源条目回写 manifest。 -- store media 只创建与 `projectId + draftId + expectedDraftRevision` 绑定的受控媒体,不递增草稿 revision;随后一次 update CAS 把 mediaRef 纳入草稿。update 冲突或流程退出留下的无引用媒体只能由引用扫描安全清理。 -- 普通 update 只允许前端提交 `editing/generating/cancelled`;`commit-prepared/committed/reconciliation-required` 只能由 commit/recovery 命令推进,防止 WebView 伪造事务终态。 -- staging token 绑定 `projectId + draftId + draftRevision + mediaType + sha256 + byteLength + pixel size`,有效期固定 `24` 小时;被 prepared transaction 引用后不因到期删除,直到事务终态。 -- staging 命令只接收 WebView 已渲染的最终 Blob 字节,不接收源路径、目标路径、URL 或 data URL;Tauri 重新计算摘要、识别魔数和像素尺寸。 -- discard 对 clean 草稿可直接执行;dirty/generating 草稿必须先经过独立确认弹窗。`keep-draft` 只退出中央画布并保留 sidecar;`discard-draft` 把状态 CAS 为 `cancelled`,再删除仅由该草稿持有的媒体。已进入 `commit-prepared` 的草稿禁止普通 discard,必须先恢复/对账事务。 - -## 8. 正式资产提交合同 - -### 8.1 输入 - -```ts -type CommitLocalProjectAssetInput = { - projectPath: string; - expectedProjectId: string; - expectedRevision: number; - expectedDraftRevision: number; - draftId: string; - commitId: string; - idempotencyKey: string; - intent: 'create' | 'refine'; - sourceAssetId: string | null; - stagedImageToken: string; - name: string; - assetKind: string; - referenceResourceIds: string[]; - generationProvenance: { - taskId: string; - prompt: string; - model: string; - generationRoute: string; - generationKind: string; - } | null; -}; -``` - -- `commitId` 和 `idempotencyKey` 都是规范小写 UUID v4;第一次提交前生成,网络/IPC 响应丢失、进程重启和对账重试必须复用原值。 -- Tauri 先完成 name NFC、assetKind 校验、referenceResourceIds 去重并按 Unicode code point 升序、refine 源身份补入和空值规范化,再按 RFC 8785 规范 JSON(排除 `projectPath`)计算 SHA-256 小写十六进制请求指纹。相同 commitId 或 idempotencyKey 绑定不同指纹必须失败关闭。 -- `name` 先做 Unicode NFC,之后必须为 `1..80` 个 Unicode 字符;拒绝控制字符、`/\\:*?\"<>|`、`.`、`..`、尾部空格/点以及 Windows 保留设备名。正式相对路径固定为 `assets/canvas/--.`,扩展名由 staging 的已验证媒体类型唯一决定。 -- `intent/sourceAssetId` 必须和权威草稿一致。refine 时 Tauri 自行保证源规范资源身份位于 `referenceResourceIds`;前端遗漏时补入,传入冲突身份时拒绝。create 时 `sourceAssetId` 必须为 null。 -- `generationProvenance` 只有当最终输出可唯一归属于一次生成时才提交,五个字段必须同时非空;组合多个生成结果或纯编辑输出传 `null`,不得猜测 prompt/model/task。 - -### 8.2 返回 DTO - -```ts -type CommitLocalProjectAssetSuccess = { - status: 'committed' | 'already-committed'; - projectId: string; - projectRevision: number; - committedProjectRevision: number; - draftId: string; - draftRevision: number; - commitId: string; - idempotencyKey: string; - eventId: string; - asset: GameCreationAppAssetManifestEntry; - manifest: GameCreationAppManifest; -}; - -type CommitLocalProjectAssetConflict = { - status: 'conflict'; - conflictKind: 'project-identity' | 'project-revision' | 'draft-revision'; - expectedProjectId: string; - projectId: string | null; - expectedRevision: number; - projectRevision: number | null; - expectedDraftRevision: number; - draftRevision: number | null; - commitId: string; - idempotencyKey: string; - asset: null; - manifest: GameCreationAppManifest | null; -}; - -type CommitLocalProjectAssetResult = - | CommitLocalProjectAssetSuccess - | CommitLocalProjectAssetConflict - | { - status: 'rolled-back' | 'reconciliation-required'; - projectId: string; - projectRevision: number; - draftId: string; - draftRevision: number; - commitId: string; - idempotencyKey: string; - eventId: string; - asset: null; - manifest: GameCreationAppManifest; - }; -``` - -- 首次成功时 `projectRevision === committedProjectRevision === expectedRevision + 1`。重复查询发生在项目后来又被修改之后时,`committedProjectRevision` 保持原提交 revision,`projectRevision/manifest` 返回锁内读取的当前最新一致快照。 -- project identity 冲突不返回另一个项目的 manifest,相关字段为 null;revision 冲突返回当前项目最新完整 manifest;draft 冲突返回当前 draft revision,不写正式文件。 -- `asset` 精确等于返回 manifest 中 ID 为 `canvas-` 的条目。其 `kind=assetKind`、媒体类型来自 staging、localPath 使用固定路径、`source.kind='canvas'`、`source.canvasProjectId=null`、`source.resourceId='local-asset:canvas-'`、`source.referenceResourceIds` 使用规范血缘;其它 generation 字段按 `generationProvenance` 填充或为 null。 -- 相同 commitId/idempotencyKey 与相同指纹在已提交后返回 `already-committed`,不得再次写文件、递增 revision、追加 manifest、扣费或创建新事件身份。 -- 收到 typed conflict 证明 prepared 尚未建立且零正式副作用。调用方必须先接受最新 project/draft 状态并由用户显式再次保存,新的尝试生成新的 commitId/idempotencyKey;不得静默改写 expectedRevision 后沿用旧键。IPC 响应丢失或结果未知时则必须复用原始完整请求和原键调用恢复/查询,禁止生成新键。 -- `rolled-back` 表示 journal 已证明旧尝试安全回滚;再次保存必须生成新 commitId/key。`reconciliation-required` 表示结果不能自动证明,必须保留原身份并先运行 recovery/人工对账;在该项目事务解除前禁止创建第二笔替代提交。 - -## 9. 提交事务、锁和崩溃恢复 - -commit ledger schema 固定为 `game-creator-local-asset-commit.v1`,transaction journal schema 固定为 `game-creator-local-asset-transaction.v1`: - -```ts -type LocalAssetCommitLedger = { - schemaVersion: 'game-creator-local-asset-commit.v1'; - projectId: string; - draftId: string; - commitId: string; - idempotencyKey: string; - requestFingerprint: string; - status: 'prepared' | 'committed' | 'rolled-back' | 'reconciliation-required'; - expectedProjectRevision: number; - committedProjectRevision: number | null; - expectedDraftRevision: number; - committedDraftRevision: number | null; - assetId: string | null; - eventId: string; - eventPayload: Omit | null; - eventDelivery: 'pending' | 'attempted'; - createdAt: number; - updatedAt: number; -}; - -type LocalAssetTransactionStage = - | 'prepared' - | 'file-installed' - | 'manifest-installed' - | 'revision-installed' - | 'verified' - | 'committed' - | 'event-attempted' - | 'rolled-back' - | 'reconciliation-required'; - -type LocalAssetTransactionJournal = { - schemaVersion: 'game-creator-local-asset-transaction.v1'; - projectId: string; - draftId: string; - commitId: string; - idempotencyKey: string; - requestFingerprint: string; - stage: LocalAssetTransactionStage; - expectedProjectRevision: number; - targetProjectRevision: number; - expectedDraftRevision: number; - targetDraftRevision: number; - stagedImage: { - token: string; - mediaType: 'image/png' | 'image/jpeg' | 'image/webp'; - sha256: string; - byteLength: number; - pixelWidth: number; - pixelHeight: number; - }; - finalImage: { relativePath: string; sha256: string; existedBefore: false }; - manifestBeforeSha256: string; - manifestAfterSha256: string; - projectRevisionBeforeSha256: string | null; - projectRevisionAfterSha256: string; - assetId: string; - eventId: string; - occurredAt: number; - createdAt: number; - updatedAt: number; -}; -``` - -- 同一 `commitId` 与同一 `idempotencyKey` 必须唯一映射到同一 ledger;Tauri 检查两个索引,不能只按其中一个去重。ledger 最大 `16 MiB`,journal JSON 最大 `1 MiB`;预计的 after manifest、event payload 或 ledger 超限时必须在 prepared 前拒绝。 -- `eventDelivery='attempted'` 只表示至少调用过 emit,不表示跨崩溃 exactly-once。恢复可以再次 emit 同一 payload。ledger 只持久化不含 `projectPath` 的事件业务 payload,emit 时使用本次已授权并重新验证的当前项目根作为事件 envelope 路径。 - -### 9.1 锁顺序 - -- 正式提交和事务恢复统一按“项目 mutation write lock -> asset-canvas transaction/draft lock -> manifest store lock”的顺序取锁;任何路径不得反向取锁。 -- 资源布局专用锁不参与正式提交。布局协调发生在提交返回后的前端投影阶段。 -- 在项目 write lock 内重新读取 projectId、project revision、draft、commit ledger、manifest 和 staging 摘要;所有 expected 值都匹配后才允许产生 prepared journal。 - -### 9.2 固定事务顺序 - -```text -prepared journal / commit ledger -→ 最终图片文件原子落盘 -→ manifest 与项目 revision 在同一锁内完成可恢复的逻辑原子更新 -→ 回读最终图片、manifest、项目 revision 和血缘并逐项验证 -→ commit ledger 标记 committed,草稿标记 committed -→ 释放项目写锁 -→ 最后发布 Tauri event -``` - -具体要求: - -1. journal 先保存 before/after 摘要、固定目标路径、`expectedRevision/targetRevision`、草稿 revision、请求指纹和预生成 `eventId`;写入后回读验证才进入下一步。 -2. 最终图片只能安装到固定的新路径。路径已存在但没有同一 committed ledger 时失败关闭;不能覆盖用户已有文件。 -3. manifest 与 `.agent/runtime/project-revision.json` 是两个文件,不能假设文件系统提供跨文件物理原子性。实现必须在同一项目锁内分别原子替换,并依靠 journal 的 before/after 字节和摘要实现逻辑原子性;target project revision 固定为 `expectedRevision + 1`。 -4. manifest 更新只允许保留全部现有内容、必要时补齐 refine 源 `source.resourceId`,并追加唯一新 asset。正式版本数组的不可变前缀门禁继续生效。 -5. 回读必须验证最终图片摘要/魔数/尺寸、manifest projectId、唯一 asset、源资产保留、血缘、localPath,以及 project revision 精确等于 target。任一不符都不能发事件。 -6. committed ledger 和草稿 committed 状态都必须落盘并回读。草稿 revision 增加 `1`,`pendingCommit=null`,`lastCommit` 填入固定身份。 -7. event 只在项目锁释放后发布,避免监听器回调重新调用 Tauri 时死锁。事件投递结果可以写回 outbox/ledger 作为投递元数据;崩溃发生在 emit 与投递标记之间允许重复 emit。 - -### 9.3 失败与恢复 - -- final file 已安装而 manifest 尚未安装:只有 journal 证明目标提交前不存在、当前摘要等于 after 摘要且 manifest/revision 仍等于 before 时,恢复才可删除该新文件并标记 rolled-back;否则进入 reconciliation-required。 -- manifest 已安装而 revision 尚未安装:若 manifest 精确等于 after、revision 精确等于 before,恢复在同一项目锁内前向安装 target revision;任何后续项目变更都禁止猜测,转 reconciliation-required。 -- manifest/revision 已安装而回读、ledger、草稿或事件前崩溃:恢复重新逐项验证,补齐 committed ledger 和草稿状态,并重发同一 `eventId`。不得创建新 asset、新 commitId 或新 eventId。 -- ledger 已 committed 但事件状态未知:返回 `already-committed` 并重发同一事件。事件语义是“至少一次 + eventId 去重”,不承诺跨进程崩溃的严格 exactly-once。 -- journal、before/after 文件、ledger 或已安装文件相互矛盾时进入 `reconciliation-required`,保留证据并禁止普通重试/取消;不得只凭目标文件存在或错误字符串推断成功。 -- staging 尚未进入 prepared 的过期文件可在 24 小时后清理;prepared、reconciliation-required 或未完成事务引用的文件不得清理。committed ledger 是永久幂等事实,随项目一起保留。 - -## 10. Tauri 正式提交事件 - -事件名固定为: - -```text -game-creator-local-asset-committed -``` - -payload 固定为: - -```ts -type GameCreatorLocalAssetCommittedEvent = { - schemaVersion: 'game-creator-local-asset-committed.v1'; - eventId: string; - projectPath: string; - projectId: string; - committedProjectRevision: number; - draftId: string; - commitId: string; - idempotencyKey: string; - asset: GameCreationAppAssetManifestEntry; - manifest: GameCreationAppManifest; - occurredAt: number; -}; -``` - -- `eventId` 是 prepared 阶段生成并持久化的规范小写 UUID v4;同一 commit 永远复用同一 eventId 和不可变业务 payload。`projectPath` 不进入项目 sidecar,emit 时从当前已验证项目根注入;项目目录被用户移动后可以变化,但 `projectId/commitId/eventId/manifest` 不变。 -- payload 的 manifest 是提交 revision 的完整快照。监听方按 eventId 去重,并按 `projectPath + projectId + committedProjectRevision` 防止旧事件覆盖更新状态。 -- 事件不包含 staging token、绝对媒体路径、API Key、operationId 或私有生成账本正文。正式 manifest schema 已允许的 `source.prompt/model` 可以随完整 manifest 和 asset 出现,但不得额外附带未提交草稿 prompt、上游响应或请求头。 -- 本文所有 `createdAt/updatedAt/occurredAt/expiresAt` 都是非负 JavaScript 安全整数的 Unix 毫秒时间戳,由 Tauri 生成;前端不得提交权威时间。 - -## 11. 保存后即时投影与焦点竞态 - -### 11.1 成功投影顺序 - -commit command 成功返回后,当前项目仍匹配时必须执行: - -```text -用返回的完整 manifest 更新项目上下文 -→ 重建资源投影 -→ 以新投影更新依赖图输入 -→ 分别协调 dependency/type 布局中的新资源 -→ 当前模式布局 ready 后再决定是否选择和定位新资源 -``` - -- 不刷新页面,不关闭/重开项目,不额外重新拉一份旧 manifest。 -- 新资源卡身份固定为 `asset:`。布局使用现役确定性默认位置与 CAS/FIFO 合同;保存事务本身不直接写资源布局 sidecar。 -- manifest 投影可以在布局协调完成前显示加载占位,但不得用临时坐标持久化错误布局。 - -### 11.2 焦点守卫 - -保存开始时捕获: - -```ts -type AssetCanvasFocusGuard = { - projectPath: string; - projectId: string; - centerKind: 'asset-canvas'; - sessionId: string; - draftId: string; - intent: 'create' | 'refine'; - selectionEpoch: number; - queryEpoch: number; -}; -``` - -- 用户已切项目:结果只能更新原项目 key 下的后台缓存,不得写当前项目 manifest、切换中央状态或抢焦点。 -- 用户已切到 run、资源总览或另一个素材画布 session:可以按精确项目身份更新缓存/投影,但不得把中央主视窗切回本次流程。 -- 用户在等待期间选择了其它资源:保持用户当前选择;新资源仍进入投影和布局,但不得自动选中。 -- 搜索或筛选 epoch 已变化且新资源被隐藏:保持条件和当前选择,显示“新资源已保存,当前筛选条件下不可见”,并提供显式“清除筛选并定位”动作;不得自动清空条件。 -- 只有 project/session/draft/intent、selection epoch 和 query epoch 全部仍匹配,且新资源在当前条件下可见时,才在布局 ready 后自动选择、滚动并聚焦新资源。 -- 迟到结果不得依赖 React 闭包中的旧布尔值;必须用当前 ref/store 中的完整守卫身份复核。 - -## 12. 素材画布状态机与取消语义 - -```text -resource-overview - -> opening - -> editing.clean - -> editing.dirty - -editing.* - -> generating - -> saving.staging - -> cancelling - -generating - -> editing.dirty (成功) - -> failed.recoverable (确定失败) - -> cancelling - -saving.staging - -> saving.committing - -> failed.recoverable - -saving.committing - -> saving.projecting - -> failed.recoverable - -> failed.reconciliation-required - -saving.projecting - -> saved - -> resource-overview - -opening | failed.* - -> recovering - -> editing.clean | editing.dirty | saving.projecting - -> failed.reconciliation-required - -cancelling - -> cancelled - -> resource-overview -``` - -- `editing.clean` 指当前内存状态等于最近成功持久草稿 revision;任何已接受的图层、viewport、生成记录或背景修改使其变为 dirty。 -- clean 取消直接退出;dirty 取消必须弹独立确认面板,提供“保留草稿并退出”和“放弃草稿”两个动作,默认保留。 -- generating 取消先尝试宿主取消。宿主没有取消能力或远端已受理时,不换幂等键重提;把结果继续写入原 generation ledger。当前 session 已离开或 draft revision/身份已变化时,迟到生成结果不得自动插入当前画布。 -- `saving.staging` 在 commit command 尚未受理前可以停止;进入 `saving.committing` 后没有“假取消”。窗口关闭或用户离开只解除焦点意图,事务继续由 Tauri 完成或在恢复时对账。 -- 确定的 validation/staging/generation 失败回到 recoverable;文件/manifest/revision/ledger 之间结果不明只能进入 reconciliation-required,禁止普通“再试一次”制造第二份资产。 -- saved 只有在 manifest 投影和当前 mode 布局已进入 ready/failed 明确终态后成立;布局 failed 可以提示后进入资源总览,但不能回滚已经正式提交的资产。 - -## 13. 验收矩阵 - -| 编号 | 宿主/场景 | 前置或故障注入 | 必须结果 | -| ---- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A01 | Web + Tauri 共享源码 | 构建两个宿主 | 两者 import 同一 core/react;客户端无画布目录镜像 | -| A02 | 新增图片 | create,导入/编辑/保存 | 新 asset 落盘并进入 manifest、投影、依赖图和两种布局;无需刷新 | -| A03 | 精修图片 | refine 已有本地图片 | 原文件/asset 保留,新建 asset,source.resourceId 补齐且血缘包含源 | -| A04 | 基础编辑 | 平移、缩放、多选、移动/缩放、层序、显隐、锁定、翻转、分组 | 两宿主行为和序列化 fixture 一致,undo/redo 最多 60 步 | -| A05 | 生成成功 | 响应正常 | 只新增一次 generation 结果,草稿 CAS 递增且可继续编辑/保存 | -| A06 | 生成失败 | 上游确定失败 | 状态可恢复,不创建正式资产,不用新幂等键自动重试 | -| A07 | 生成响应丢失 | 上游已受理、客户端未收到结果 | 以原 operation/idempotency 对账,只产生一份结果/扣费 | -| A08 | 保存成功 | project/draft revision 匹配 | file -> manifest/revision -> 回读 -> ledger/draft -> event 顺序成立 | -| A09 | 重复保存 | 相同 commit/key/指纹 | 返回 already-committed,asset/revision/eventId 均不重复 | -| A10 | 幂等冲突 | 同 key 或 commitId、不同指纹 | 失败关闭,原 ledger/文件/manifest 不变 | -| A11 | 两窗口并发 | 相同 expectedRevision 同时提交 | 最多一笔 committed,另一笔 typed conflict,不覆盖成功方 | -| A12 | draft 并发 | 相同 expectedDraftRevision 更新 | 最多一笔 updated,另一笔返回最新完整 draft | -| A13 | 崩溃:首个/全部事务快照、journal 或 prepared 后 | 尚未装图片 | 无 ledger 的未发布 transaction 只在正式文件不存在且 manifest/revision 仍为 before 时清理;prepared 安全回滚或继续,不生成幽灵 asset | -| A14 | 崩溃:图片后 | manifest 前 | 仅在摘要/before 全匹配时删除新文件,否则 reconciliation-required | -| A15 | 崩溃:manifest 后 | revision 前 | before/after 匹配时前向补 revision,否则 reconciliation-required | -| A16 | 崩溃:revision 后 | ledger/event 前 | 回读验证后补 ledger/draft,并重发相同 eventId | -| A17 | 崩溃:emit 后 | 投递标记前 | 允许重复事件,前端 eventId 去重且不重复选中/布局 | -| A18 | 切项目后的迟到保存 | 提交在途时打开其它项目 | 当前项目 UI 不变;旧项目缓存可按精确身份更新 | -| A19 | 切模式/离开流程 | 提交在途时进入 run/overview/新 session | 不切回素材画布、不抢焦点,正式结果仍可投影到对应项目 | -| A20 | 改选择后的迟到保存 | 等待时选择其它资源 | 保持用户选择,新资源只进入投影和布局 | -| A21 | 搜索隐藏新资源 | query epoch 改变且不匹配新资源 | 不清搜索、不自动选中,提示并提供显式清除/定位动作 | -| A22 | 即时投影 | 提交后不刷新/不重开 | manifest、资源卡、依赖图输入、布局和允许时的选中全部完成 | -| A23 | 草稿损坏/身份错配 | 损坏 JSON、未知 schema、项目路径被重建 | 失败关闭,不用空草稿覆盖,不创建其它项目副作用 | -| A24 | 锁与恢复 | 活锁 mtime 很旧、进程退出、Windows/Unix | 不按时间/PID删锁;句柄释放后正常取得同一锁入口 | -| A25 | 容量边界 | 2 MiB/4096 层/64 MiB/像素上限边界及超限 | 边界内成功,超限零副作用且错误不泄露绝对路径/密钥 | -| A26 | 导出 | PNG/JPEG/WebP | Web 下载/云端、Tauri 保存对话框均成功;共享 UI 不接收绝对路径 | -| A27 | 取消 | clean、dirty、generating、staging、committing | 分别符合第 12 节;committing 不伪装成可取消 | -| A28 | 恢复草稿 | 主文件损坏但恢复副本可信/不可信 | 可信副本恢复到 clean history 基线;不可信进入对账,不猜测 | -| A29 | 登录刷新重放 | context、参考图准备或首次提交返回 401,刷新后以相同 generationId 和幂等身份重放 | 401 账本保持可恢复且第二次真实访问平台;403 直接失败且不刷新;远端最多受理一次 | -| A30 | 生成卡片拖动 | 生成中拖动占位卡片 | 卡片位置按画布坐标更新并持久化到 generation record,任务状态刷新不覆盖用户位置 | -| A31 | 精修最终图唯一性 | 选择另一候选图设为最终图 | 入口原图和所有其他候选保持各自快照,只有 `lastCommit.sourceLayerId` 标识唯一正式候选,更新正式 asset 不反向改写历史图层 | -| A32 | 精修默认比例 | 打开图片的快速编辑 | 按原图宽高映射到最接近的支持比例(1:1、2:3、3:2、9:16、16:9);尺寸无效时回退 1:1 | -| A33 | 旧提交被后继提交取代 | 同一 asset 的旧事务未收尾,且后继 committed 事务链、当前 manifest/revision 与最终文件全部可证明 | 旧事务进入 `superseded`,不回滚、不覆盖当前正式图、不重放旧事件;证据不完整仍进入对账 | -| A34 | 同资源并发正式提交 | 同一 project/draft/asset 存在 prepared 或 reconciliation 事务时再次提交 | 拒绝新提交并要求先安全恢复;已 committed/rolled-back/superseded 事务不阻塞后续提交 | -| A35 | 精修文件名包含历史提交后缀 | 后续精修重新打开当前 `localPath`,或再次生成 / 设为最终图 | 统一剥离文件名末尾一个或多个 `--` 后缀并规范化为合法 1..=80 字符显示名;生成与最终提交使用同一结果 | -| A36 | 确定性提交参数无效 | 候选提交名称或用途在校验阶段失败 | 在读取候选、staging、transaction 或 ledger 写入前零副作用失败;UI 作为输入校验错误允许继续编辑,不触发安全恢复 | -| A37 | 候选首次确认 | 生成完成后与旧 autosave 并发,或重复打开已确认候选 | 前端把候选确认排入草稿保存 FIFO,并在提交、导入、生成、归档和放弃草稿前等待确认屏障;Tauri 在草稿锁内只为当前权威草稿中仍存在且尚未确认的候选更新私有 ledger,不改写草稿或推进 revision。普通 update 在确认前继续把候选层合回旧保存,重复确认无写入,确认后的显式删除仍允许 | -| A38 | 稳定运行入口 | 精修替换已在游戏源码中引用的图片,或继续精修旧版本事务创建的资源 | manifest 指向不可变正式版本,同时原稳定入口路径不变并刷新为新版本字节;新事务可从旧事务 `manifest.before.json` 迁移稳定入口身份,幂等重放和事务恢复会修复缺失或不匹配入口,游戏源码不需要改路径 | - -阶段一至五最终审计只有在矩阵对应的纯模型、共享 React、Web adapter、Tauri adapter、Rust 持久化与 AppSurface 测试全部通过后,才可宣称图片素材创作正式闭环完成。 - -## 14. 2026-08-05 阶段五真实图片生成补丁 - -本节是 Tauri 真实图片生成的编码级补丁;与第 3、6、12 节的早期四态生成记录冲突时,以本节为准。网站继续复用现役生成工作流,客户端只通过共享 `ImageCanvasGenerationPort` 接入,不复制 `useImageCanvasGenerationWorkflow`,也不新建第二套服务端生成系统。 - -### 14.1 确认与身份 - -- 打开生成面板、修改 prompt / 比例 / 尺寸 / `assetKind`、选择参考资源和预览确认摘要都只能产生本地 UI 状态,确认前不得读取或创建服务端编辑器项目、素材目录、上传票据或生成 operation。 -- 只有用户点击独立确认面板中的“确认生成”才进入现有 `canvas.asset_generate` confirm 权限边界。项目 deny 继续优先;客户端不得调用 auto gate、模拟确认或在恢复时分配新逻辑请求。 -- 一次逻辑生成在本地意图建立时固定分配规范 UUID v4 `intentId`、`generationId`、远端 `idempotencyKey`、本地 `commitId` 和本地 commit idempotency key。用户取消确认可直接丢弃这组未使用身份且保持零远端调用;一旦点击“确认生成”,重复点击、IPC 响应丢失、进程重启和结果未知都复用原值与原始请求字节。只有远端明确 terminal failed 后,用户再次明确确认才可建立新 intent。 -- 远端 taskId 是平台生成事实,不是本地 manifest task 身份。阶段五本地正式资产的 `source.taskId` 固定为 `null`,不能把远端 taskId 填入并参与本地 producer/task graph。 - -### 14.2 私有账本与公开投影 - -私有账本固定放在: - -```text -.agent/workbench/asset-canvas/generations/.json -``` - -账本由每个 `projectId + generationId` 的进程内 single-flight、安全相对路径和原子 sidecar 写入保护,包含调用模式、精确相对 endpoint / 请求 JSON / SHA-256、prompt 私有正文、不依赖凭据的 API 身份指纹、远端 idempotency key、operationId、稳定 `objectKey/resourceId/assetObjectId`、本地 staging 与 commit 恢复身份。本地参考图上传恢复只允许暂存稳定 bucket/objectKey;direct-upload ticket 的 host、formFields、policy、signature 和临时 Authorization 只存在内存,任何阶段都不得序列化。阶段三正式资产事务继续由既有持久锁保护。登录 Access Token、API Key、Authorization、Provider URL、签名 URL 和绝对路径不得写入账本;登录 Token 刷新不得导致 `configuration-changed`。 - -草稿、manifest、Tauri 事件、Agent DB、Runtime observation、trace、普通错误和日志只允许出现 `intentId/generationId`、安全阶段、引用资源 ID、本地 asset/commit/event 身份和稳定错误码。草稿 generation record 不再保存 prompt、External idempotency key 或 operationId;阶段五读取旧空记录兼容,但下一次成功写入必须移除这些私有字段。 - -### 14.3 不可压缩状态机 - -生成与显现必须按下列阶段推进,不能用单个 `completed` 布尔值代替: - -```text -confirmation-required -→ generation-accepted -→ generation-running -→ remote-completed -→ media-downloaded -→ asset-durable-committed -→ manifest-projected -→ layout-ready -→ selected -``` - -- `generation-accepted` 只表示 External `202 + operationId` 已原子落账;`queued/running` 继续属于远端生成期。 -- `remote-completed` 必须已经取得自洽稳定引用;缺 `objectKey/resourceId/assetObjectId` 中可支持下载与身份核验的必要组合时进入 `reconciliation-required`,不得显示成功。 -- `media-downloaded` 表示媒体通过 `/assets/read-url` 换签、SSRF/重定向门禁、大小、签名、完整解码和尺寸检查,并已写入阶段三受控 staging;重复 completed 不再下载或重写。 -- `asset-durable-committed` 只来自阶段三 journal / ledger 的 `committed | already-committed`。正式资源事务成功后,私有生成账本必须先进入 `asset-commit-settlement-pending`,再幂等补齐公开 generation 投影和 staging revision,最后才发布 `asset-durable-committed`;任一持久化切点重启都不得重复推进草稿 revision。恢复投影前必须逐项绑定私有 commit 回执、阶段三 commit ledger、transaction journal、manifest 资产与事件 payload;任一项目、草稿、commit、幂等键、revision、资产、资源或事件身份错配都保持 pending 并失败关闭。下载成功但提交失败时保留 generation ledger、staging token 和原 commit 身份,恢复只继续本地事务,不重新生成或下载。 -- `manifest-projected / layout-ready / selected` 继续复用阶段四单向链路。只有当前项目、flow、session、draft、intent 和 focus generation 全匹配时才允许选择;manifest 已提交但事件丢失时以 command/recovery 的权威完整快照继续投影。 - -### 14.4 首版请求范围与恢复 - -- 普通模式图片生成和编辑支持相同 prompt、`1:1 | 2:3 | 3:2 | 9:16 | 16:9`、`0.5K | 1K | 2K`、合法 `assetKind` 与参考资源约束。refine 的 `sourceReferenceId` 必须是当前账号已登记的服务端项目 resourceId 或素材 assetId;`objectKey`、URL、本地 `local-asset:*` 与 `assetObjectId` 都不能冒充该业务引用。本地独有图片在用户确认后先走 `/api/assets/direct-upload-tickets` → OSS form → `/api/assets/objects/confirm`,再创建当前账号拥有的项目资源或素材记录,取得正式 ID 后才可进入编辑请求。额外参考最多 8 个。高级 External v1 模式使用同一业务引用约束,但走其独立外部路由与 Developer Key 鉴权。 -- 客户端参考媒体直传固定复用 `legacyPrefix=generated-character-drafts`,不得把内部用途目录作为新 legacy prefix,也不得扩大服务端白名单。图片画布的 `pathSegments` 固定为 `editor / asset-canvas-references / / / `;全类型资源编辑的本地视频、音频等源媒体固定为 `editor / resource-editor-references / / `。两条路径都只持久化 confirm 后的稳定 objectKey,不持久化 ticket 或签名 URL。 -- 图片参考资源准备失败按阶段投影安全错误码:本地读取/校验为 `reference-material-invalid`,票据为 `reference-ticket-failed`,OSS 表单上传为 `reference-object-upload-failed`,对象确认为 `reference-confirm-failed`;`401` 进入当前账号代际的单飞刷新,刷新失败或重试后仍未授权才返回 `authentication-required`,`403` 直接按权限不足处理。任一阶段失败都必须保持 `operationId=null`、生成 endpoint/request body 未建立,不得进入扣费或生成提交。普通错误不得包含 ticket host、formFields、policy、signature、Token、API Key、Provider 响应正文或本机绝对路径。 -- `accepted/running` 重启只查询原 operation;`prepared` 只以账本中的原 endpoint、原始字节和原键恢复同一 POST。轮询超时、临时鉴权/网关错误或结果未知保持可恢复,不自动转 failed 或换键扣费。 -- 用户“停止等待”只撤销当前 session 的聚焦意图,不伪装远端取消。后台 command 可以继续安全 reconcile;应用退出后由项目恢复扫描继续原 operation。迟到完成可以更新原项目权威缓存和 manifest,但不能切回旧画布、清搜索或抢焦点。 -- 普通模式缺少有效登录态、刷新失败或账号权限不足,以及余额不足、平台生成服务配置错误、远端 terminal failed、稳定引用缺失、下载失败、staging/正式提交失败,都不得返回资源创建成功。错误只引导用户重新登录或检查账号权限,不要求填写 Developer API Key,也不回显 Token、Authorization 或上游正文。高级 External v1 模式才可提示检查 Developer API Key。只有 terminal failed 才允许用户通过新的独立确认建立新 intent;其它未知或对账状态必须保留原身份。 -- 普通模式会话使用单调 `authGeneration` 做安装与清除 CAS。登录、刷新或换号只允许更高 generation 覆盖 GUI/Runner 内存快照;每个网络动作在发出前冻结 `ownerUserId + authGeneration`,响应解析、账本推进、媒体安装和正式提交前都必须复核仍属于当前账号。旧 generation 的迟到成功、失败或清除不得覆盖新账号状态,也不得把旧账号媒体提交到新账号项目。 -- 普通模式首次遇到 `401` 时,每个 `authGeneration` 全局只允许一个 refresh 请求;其它调用等待该 Promise,但各自仍受自己的 deadline/取消信号约束。refresh 成功后以更高 generation CAS 安装新 Token,并只用原 endpoint、原始请求字节和原 `Idempotency-Key` 重试一次;不能重建正文、创建新 operation 或重新扣费。refresh 失败、账号已切换或重试仍为 `401` 时停止网络动作并保留原账本;`403` 不触发 refresh。 -- 私有生成账本的普通模式服务身份固定为 `official-platform-v1 + 官方 origin + ownerUserId`,并保存创建该账本时的账号身份,不保存 Access Token、refresh Cookie 或 authGeneration。Access Token 轮换不改变 operation 身份;切号或退出会提升 generation、中止并脱离旧请求,旧账号的 `prepared/accepted/running` 账本在新账号下零网络动作、零安装、零归档。只有重新登录同一 owner 后才能继续原 POST/GET;不能把新账号下的 `401/403/404` 当作旧 operation 的终态证明。 - -## 15. 2026-08-06 画布 UI 对齐阶段三至五收口 - -- 阶段三没有新建平行画布。现有 `AssetCanvasSurface` 直接使用 `CanvasChromeButton`、`CanvasToolbar`、`CanvasToolbarGroup` 和 `CanvasToolbarDivider`。精修顶栏以紧凑尺寸覆盖返回、导入、定位当前最终图、撤销和重做;删除与“设为最终图”属于选中图片的上下文动作,通用 AI 生成与保存设置只保留给创建模式。生成确认、失败重试、停止等待和缩放继续复用共享 chrome。共享组件只接收图标节点、名称、事件和原生状态,不读取 Tauri、账号、钱包或 HTTP。 -- Tauri 样式只导入 `packages/shared/src/theme.css`、`@genarrative/image-canvas-react/styles.css` 与客户端宿主样式,没有导入主站 `src/index.css`。宿主删除了覆盖全部按钮的重复视觉规则,只保留保存/确认主操作、字段、弹窗、进度/失败卡、状态栏和 viewport 定位;颜色、边框、surface、文字、focus 和主次动作均使用平台 token。 -- 创建模式的通用生成设置继续通过独立 dialog 展示。图片精修从选中图片的快速编辑卡片进入生成确认,不在顶栏提供第二个通用生成入口;精修保存设置整排隐藏,候选图只通过上下文“设为最终图”提交。生成/失败卡和底部状态使用受限高度加内部滚动,创建模式保存设置在窄宽下换为两行并保持主按钮整行可见;工具组不拆字,工具栏需要时横向收纳。普通确认文案不显示 manifest、幂等实现、开发者 API 配置或本机绝对路径。 -- 阶段四保留左导航、中央主视窗、右侧 Project Supervisor 和底部 Agent Dock 四区 DOM。中央壳使用主面板层级;Supervisor 降为协作面板,其消息和 Runtime 状态内部滚动、composer 固定在栏底;Dock 使用较轻 surface 和可收缩状态卡,不与中央主要操作争抢层级。 -- 桌面 `>760px` 统一使用 `100dvh` 的两行工作台网格,第一行是内部滚动的主视窗/Supervisor,第二行是常驻 Dock。正式 `1280×800` 基线下页面级尺寸合同为 window/document/body `1280×800`;无登录会话的真实 Vite 入口按安全合同停在登录页,不绕过门禁,工作台内部由 AppSurface、共享 chrome 和 CSS 合同测试提供可重复证据。 -- 阶段五定向测试覆盖共享 chrome 的 pressed、Tauri 实际消费的工具组/分隔符、按钮名称、expanded/disabled、生成确认、失败重试、保存/取消,以及生成 dialog、进度/失败卡、状态栏、Supervisor composer 和 Agent Dock 的非裁剪/可见性合同。生成请求、登录态、泥点计费、Host Port、草稿 CAS、manifest/revision、Runtime、审批和保存事务均未改变。 -- 保存设置的产品语义固定为“名称可编辑、用途受控、格式枚举”。create 默认 `game-art`,普通用户只从 `game-art / icon-spec / ui-prototype / art-spritesheet` 四个权威用途选择;界面显示中文名称,不暴露自由 slug 输入。refine 必须继承源 manifest asset 的 `kind` 并锁定,未知历史 kind 原值透传但只显示“原资源用途”,不得借精修改变 subtype。PNG/JPEG/WebP 继续是有限格式选项。 -- refine 的继承名称从当前源 `localPath` 派生时必须使用唯一 canonical 规则:取文件名、去扩展名、循环剥离末尾 `--` 历史提交后缀,再执行 NFC、非法字符清理、1..=80 字符边界、Windows 保留名和尾随空格/点防护。生成请求和“设为最终图”不得各自解析。名称 / 用途校验失败属于确定性的输入错误,后端必须发生在读取候选与 staging 之前,前端不得把它呈现为事务恢复。 -- 工具动作与保存设置必须是显式上下两行,保存栅格把主按钮列固定为 `max-content` 且禁止换行;容器宽度不足时保存按钮独占整行。普通用户工作区状态只显示项目名称,不直接展示本机绝对项目路径;显式目录选择、权限确认或开发诊断不受该展示规则替代。 - -## 16. 2026-08-10 全类型现有资源非破坏性编辑阶段覆盖条款 - -本节是当前产品入口的覆盖条款;与第 1、2、4、13 节中要求同时开放 create/refine 入口或只允许图片编辑的文字冲突时,以本节为准。底层 create 草稿、序列化和恢复兼容继续保留,不作为当前普通用户入口。 - -- 资源总览的“新增资源”保留为明确禁用态,入口处理函数也必须拒绝 create,不能只依赖按钮外观阻止进入空白画布。 -- 资源聚焦态的所有现役资源均提供“编辑资源”,覆盖 manifest asset、已完成任务产物、已导入附件、Agent 文本回执和项目版本。后端必须按 manifest、任务完成态、上传登记或回执身份重新核验来源;没有唯一来源身份的本地媒体不得仅凭前端路径进入编辑。 -- 静态 PNG / JPEG / WebP 继续使用 `AssetCanvasSurface + intent=refine`,自动加载唯一源图片,并把源资源身份作为图片编辑请求的必选引用。任务产物或附件中的静态图片必须先正规化为正式 manifest asset,再进入现有图片画布。 -- refine 入口不能只在 WebView 进程内缓存 `draftId`。每次打开先在正式 draft sidecar 中按 `projectId + intent=refine + sourceAssetId + active status` 有界发现:唯一命中沿用原 `draftId` 并生成新 `sessionId`,零命中才创建,多命中进入 `reconciliation-required`;`committed/cancelled` 不属于 active 候选。 -- SVG、UTF-8 文档、代码和 Agent 文本回执使用文本差异派生:把源内容当作不可信数据交给当前客户端 LLM,响应必须是完整、唯一的结构化内容 envelope;JSON、SVG 等可校验格式必须在落盘前重新校验。结果写入新的本地路径和 manifest asset,不能直接写回源文件。Agent 回执原记录不转写、不删除,新 asset 以回执资源身份登记血缘。 -- 文本、SVG 与 Agent 回执在 Provider 调用前必须先持久化 request-issued;成功响应必须先原子安装到与原 operation、请求指纹和内容摘要绑定的私有 durable handoff,再做 envelope 解析、格式校验和 staging。issued 后缺少可信 handoff 只能对账;handoff 已存在且校验通过时恢复只消费该正文,两种情况都禁止再次调用 Provider。 -- 普通客户端视频使用 `POST /api/editor/videos/generations`。有稳定远端引用时直接作为 `referenceVideoSrcs`,只有本地文件时先走 `/api/assets/direct-upload-tickets`、OSS 表单上传和 `/api/assets/objects/confirm`,再提交同一逻辑生成;结果必须下载到新的本地文件并登记远端稳定身份。 -- 普通客户端音效和背景音乐分别使用 `POST /api/editor/audios/sound-effects/generations` 与 `POST /api/editor/audios/background-music/generations`。当前接口没有源音频引用字段,因此产品语义固定为“基于原资源语义的派生重制”,界面不得描述为对源波形的裁剪、变声或局部修改;结果仍必须引用源资源身份并保留原音频。 -- 项目版本编辑固定追加 `parentVersionId` 指向源版本的子版本,继承源版本资源绑定并记录本轮编辑提示;已有版本数组元素不可修改、删除或重排。 -- 所有编辑结果固定创建新的本地 asset、文件路径、版本 ID 或资源身份。源 asset、源文件、Agent 回执与源版本不得删除、覆盖或复用;新 asset 的 `referenceResourceIds` 必须包含源资源规范身份,资源总览同时保留新旧资源。 -- 各类型统一使用稳定 `operationId / Idempotency-Key`。普通站内响应的 queued operation 与高级 External v1 的 `202` 都只表示受理,必须轮询原 operation;inline 完成则直接消费同一请求的稳定结果。提交结果未知或登录失效时保留原身份供恢复,不能换键重提。签名 URL 不得写入 manifest,manifest 只保存稳定 objectKey 对应的资源 / 资产身份。 -- 图片 refine、视频、音效和背景音乐请求统一在 `generationInputs.source` 写入专用消费身份 `game-creator-resource-editor`;普通内部路由和高级 External 路由都必须实际读取并传递同一稳定 `Idempotency-Key`。队列完成态只向该消费身份返回经过裁剪的稳定 `objectKey / resource / asset` 引用和必要媒体元数据,不暴露 provider、worker、队列内部字段或临时签名 URL。 -- 生成公开状态每次写入草稿都必须推进草稿 revision,并把最新 revision 同步到私有生成账本、进度事件、staging 与正式 commit;旧 UI 快照不得覆盖 accepted/running/reconciliation 状态。正式 commit 已成功但公开终态尚未写入时,恢复扫描必须在无需远端凭据的情况下完成 `asset-commit-settlement-pending`,不能把私有 committed 账本当作已经完成公开投影。 -- operation 已受理或首次提交结果未知后遇到 `401`,不得写成 terminal failed 或不可逆 `reconciliation-required`。普通模式先按上述单飞流程刷新;刷新失败、换号或当前 owner 不匹配时保留原 remote phase、operation、原请求字节和原幂等身份,停止网络。重新登录同一 owner 后,已有 `operationId` 只继续 GET 同一 operation,未受理请求才允许用冻结正文和原幂等键重试 POST。高级 External v1 模式修正 Developer API Key 后遵守相同恢复规则。 -- 全类型资源编辑私有账本必须冻结 `sourceAssetId / sourceMediaType / sourceAssetKind / producerTaskId / sourceVersionId` 源快照;新账本不得依赖 UI 仍停留在原资源,旧账本则只允许从当前 manifest、已完成任务产物、版本数组和受控扩展名兼容恢复。恢复结果仍要重新校验唯一来源、项目归属与媒体类型,不能把账本快照当成越权凭据。 -- 普通模式私有账本绑定固定官方 origin 与 ownerUserId,不绑定可轮换的 Access Token;恢复命令不得让前端重新拼请求或把 Token 写入参数。高级 External v1 账本绑定显式服务 origin,同样不绑定可轮换的 Developer API Key。两种模式都不得因凭据轮换改变原 operation;`401/403/404`、轮询超时或网关暂时失败必须保留原 endpoint、请求字节、幂等键和 operation,并先按账本记录的模式与 owner 做隔离校验。 -- pending 恢复扫描的目录上限统计所有实际目录条目,包括未知文件、非 pending 账本和损坏条目;达到上限后停止扫描并返回安全错误,不能通过堆放无关文件绕过边界。已 committed 的账本不得出现在 pending 列表中。 -- 派生媒体提交必须以账本冻结的源摘要再次校验源文件;摘要、媒体身份或版本父子关系变化时失败关闭。远端派生视频二次编辑必须复用已提交账本中的稳定 `objectKey`,不能把仅用于本地登记的 `assetObjectId` 当成 generation reference。版本 journal 重放只能返回既有子版本,不得重复追加。 -- 返回资源总览时,clean 草稿直接保留并退出;dirty 草稿必须使用独立确认面板提供“保留草稿并退出”和“放弃草稿”,默认保留。保留前必须等待当前保存或主动 flush,放弃才允许调用 discard。 -- 图层选择与图层缩放必须同时提供指针和键盘路径;不得嵌套 button/role=button。Enter/Space 可选择图层,缩放手柄使用原生 button 并提供方向键离散缩放。 -- Tauri 画布普通滚轮按二维 delta 平移,Shift 加纵向滚轮作为横向平移回退,Ctrl/Command 加滚轮才围绕指针缩放;一次 Shift 指针事件序列只能切换一次选择。生成中必须把焦点移到“停止等待”,并阻止 wheel、指针拖拽、缩放快捷键和方向键继续改变画布,避免生成恢复期间写入新的 UI 快照。 -- `editing` 是唯一允许发起新生成的生命周期。`generating/committing/recovering/failed` 及独立 modal 打开时,画布与顶部工具栏必须同时 disabled/inert;生成事件 handler 还要读取当前权威 lifecycle 再次失败关闭,不能依赖遮罩层或渲染时闭包。 -- asset transaction journal 已精确证明目标 asset、正式媒体和 target revision 写入时,恢复应允许当前 manifest 包含该目标的同时已有后续合法追加,且只要求 `currentRevision >= targetRevision`;补齐同一 ledger/draft 后仍复用原 commit/event 身份,不能因为整个 manifest 不再等于历史 after 快照而永久对账。 -- 未收尾事务只有在同一 project/draft/asset 的后继事务为 `committed/event-attempted`,且旧事务 after 与后继 before、后继 after 与当前 manifest/revision、两份最终文件摘要和 manifest 唯一引用全部严格匹配时,才可标记为 `superseded`。该终态保留旧 journal、ledger、snapshots 与候选文件,不修改草稿 lastCommit,也不重放旧事件;任何证据缺失继续保持 `reconciliation-required`。正式提交入口发现同资源存在 `prepared/reconciliation-required` 事务时必须先拒绝并引导安全恢复,避免制造第二笔可覆盖旧结果的提交。 - -当前阶段不主动扩展 create 专属空画布导入、生成或保存体验;图片继续复用现有画布,其他类型使用同一资源编辑壳按能力分流,不能把视频、音频或版本强塞进图片图层模型。共享持久化、安全、幂等和可访问性缺陷仍必须修复,因为它们直接影响编辑链路。