From 35222858942848cf6d716ec716d0b1109da8355e Mon Sep 17 00:00:00 2001 From: menghao Date: Wed, 5 Aug 2026 12:45:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=BB=E7=BB=93=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E5=88=9B=E4=BD=9C=E6=97=A0=E9=99=90=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E9=98=B6=E6=AE=B5=E4=B8=80=E5=90=88=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增共享画布、宿主适配、草稿与正式资产提交的完整技术合同 同步工作台 PRD 与 AI 游戏创作 App 实施计划的产品范围 补充事务恢复、焦点竞态、状态机和验收矩阵 更新文档索引、决策记录与踩坑记忆 --- docs/README.md | 3 + ...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 62 +- .../shared-memory/decision-log.md | 12 + docs/project-memory/shared-memory/pitfalls.md | 16 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 26 +- ...客户端素材创作无限画布阶段一合同-2026-08-05.md | 884 ++++++++++++++++++ 6 files changed, 982 insertions(+), 21 deletions(-) create mode 100644 docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md diff --git a/docs/README.md b/docs/README.md index b9a0be0c9..9529a82cb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,8 @@ 4. [平台入口与玩法链路](./【玩法创作】平台入口与玩法链路-2026-05-15.md) 5. [本地开发验证与生产运维](./【开发运维】本地开发验证与生产运维-2026-05-15.md) 6. [AI 游戏创作项目开发工作台 PRD](./prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md) +7. [AI 游戏创作智能体 App 实施计划](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) +8. [客户端素材创作无限画布阶段一合同](./technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md) 团队长期约定、决策、流程和排障摘要统一从 [项目记忆入口](./project-memory/README.md) 读取。代码、当前融合文档与项目记忆冲突时,以代码和最新融合文档为准。 @@ -17,6 +19,7 @@ ### 图片编辑器与 Agent +- [客户端素材创作无限画布阶段一合同](./technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md) - [图片画布编辑器 MVP 接入方案](./technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md) - [图片画布编辑器前端拆分计划](./technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md) - [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md) diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index 2983be366..294d1372b 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -1,6 +1,6 @@ # AI 游戏创作项目开发工作台 PRD -更新时间:`2026-08-04`(依赖视图视觉口径调整) +更新时间:`2026-08-05`(冻结客户端素材创作无限画布阶段一合同) ## 1. 产品定位 @@ -13,7 +13,7 @@ 3. 右侧 Project Supervisor 对话与确认区。 4. 底部专业 Agent 状态栏。 -中央主视窗在“资源管理”和“运行测试”之间切换。正式预览始终在客户端当前窗口内展开,只允许载入当前项目启动的 `127.0.0.1:` 本地 HTTP 预览,不调用系统外部浏览器。 +中央主视窗明确区分“资源总览画布”“素材创作无限画布”和“运行测试”三个状态。资源总览与素材创作不是两套项目页:前者负责 manifest 资源投影、依赖关系和类型布局,后者负责单个图片素材的无限画布创作;两者共用当前工作台中央区域。正式预览始终在客户端当前窗口内展开,只允许载入当前项目启动的 `127.0.0.1:` 本地 HTTP 预览,不调用系统外部浏览器。 ## 2. 创作工具平台接入声明 @@ -88,20 +88,25 @@ ### 4.1 主视窗 ```text -resources +resource-overview + -> asset-canvas.create(点击“新增资源”) + -> asset-canvas.refine(在唯一图片资源上点击“精修资源”) -> run(存在 runnableVersion 且 loopback preview 可启动) +asset-canvas.create|refine + -> resource-overview(取消、保留草稿退出或保存投影完成) + run.playing -> run.paused(用户暂停或切片结束) - -> resources(先暂停当前预览表现,再切换视图) + -> resource-overview(先暂停当前预览表现,再切换视图) run.paused -> run.playing(继续当前切片) -> run.relaunching(数值或版本编辑态发生变化) - -> resources + -> resource-overview ``` -运行入口不可用时仍允许点击,显示“当前无可运行版本”,但不切换状态。 +运行入口不可用时仍允许点击,显示“当前无可运行版本”,但不切换状态。素材创作的完整 opening/editing/generating/saving/cancelling/failed/recovering 状态、草稿身份和迟到结果门禁以 [`【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`](../technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md) 为准。 ### 4.2 测试切片 @@ -127,10 +132,10 @@ idle -> focused(document|art|audio|version) -> idle - 美术:PNG、JPEG、WEBP 继续使用图片魔数与像素边界预览;GIF、SVG、AVIF、BMP、MP4、WebM、MOV 通过新增受控媒体读取链路按文件签名校验后在中央画布放大聚焦。SVG 额外拒绝脚本、事件处理器、外部资源引用和实体声明;视频使用内置播放控件。读取失败显示错误空态。 - 音频:只读取 manifest 已登记音频或已成功导入且登记到 manifest 的附件,按文件签名接受 MP3、WAV、OGG / Opus、M4A、AAC、FLAC;聚焦态展示实际格式、浏览器解码后的时长以及带播放进度和暂停能力的内置播放器。音频任务声明中的未登记路径继续不得读取或播放。 - 版本:只展示 manifest 中正式、不可变的迭代版本记录;版本卡展示项目修订、创建原因与父版本,聚焦态同时展示直接子版本和资源绑定。点击版本卡后高亮仍存在于当前资源投影中的引用资源;缺失历史资源只保留绑定身份,不生成幽灵资源卡。资源替换仍留给后续切片。 -- mentor 最新决定:资源聚焦不提供工具栏,也不提供工具侧边栏。 -- 点击资源后,中央主视窗从 `resources.list` 切换为 `resources.focused.document / art / audio / version`,左侧平台导航、右侧 Supervisor 对话和底部 Agent 状态栏保持原位;聚焦容器只包含标题、资源主体、必要元数据与右上角收起按钮,不使用页面级浮层或可拖动标题栏。 +- 资源聚焦不提供通用工具栏或工具侧边栏;图片聚焦态允许一个明确的“精修资源”业务动作进入素材创作无限画布,该动作不是在聚焦容器中内嵌编辑器或恢复通用工具栏。 +- 点击资源后,中央主视窗从 `resource-overview.list` 切换为 `resource-overview.focused.document / art / audio / version`,左侧平台导航、右侧 Supervisor 对话和底部 Agent 状态栏保持原位;聚焦容器只包含标题、资源主体、必要元数据与右上角收起按钮,不使用页面级浮层或可拖动标题栏。 - 退出聚焦后恢复进入前的搜索条件、dependency / type 布局模式、资源画布滚动位置和选中资源;这些只属于当前前端会话,不写入布局 sidecar。 -- 阶段四只新增上述受控读取与媒体展示;阶段六在同一聚焦容器内补齐正式版本只读展示和引用高亮,但不新增资源聚焦工具栏 / 工具侧边栏,不新增美术编辑、音频编辑 / 替换、资源重新生成、版本替换或运行模块。飞书原需求中“编辑并生成新资源”的条件项仍暂缓,不能只打开画板却缺少回写、`referenceResourceIds` 血缘登记、新资源自动选中与邻近布局的完整闭环。 +- 资源管理阶段四至阶段七只交付上述受控读取、媒体展示、正式版本只读展示和引用高亮;`2026-08-05` 起,后续素材创作切片已冻结完整图片闭环,不再把图片导入、基础编辑、生成、导出或本地回写列为非目标。入口必须与草稿 CAS、正式事务提交、`referenceResourceIds` 血缘、新资源即时投影、布局和焦点竞态一次实现,不能只打开一个没有回写的画板。 ### 4.4 历史成果与当前状态 @@ -144,23 +149,32 @@ idle -> focused(document|art|audio|version) -> idle 以下合同先冻结字段语义;P0 只实现标注为 P0 的部分。 -### 5.1 工作台视图状态(P0) +### 5.1 工作台中央状态(素材创作阶段一目标合同) ```ts type ProjectWorkbenchViewState = { - schemaVersion: 'game-creator-workbench-view.v1'; + schemaVersion: 'game-creator-workbench-view.v2'; projectId: string; - mode: 'resources' | 'run'; + centerState: + | { kind: 'resource-overview' } + | { + kind: 'asset-canvas'; + sessionId: string; + draftId: string; + intent: 'create' | 'refine'; + sourceAssetId: string | null; + } + | { kind: 'run' }; approvalMode: 'strict' | 'risk' | 'none'; expandedAgentGroups: Array<'balance' | 'audio' | 'publishing'>; }; ``` -P0 中 `approvalMode` 只能有效写入 `strict`;其它值只能作为不可用选项展示。 +旧 `game-creator-workbench-view.v1.mode='resources'` 读取时只映射到 `resource-overview`,`mode='run'` 映射到 `run`;旧状态不能合成 asset-canvas 草稿。`sessionId` 是本次进入流程的短生命周期 UUID,`draftId` 是本地可恢复草稿 UUID。`create` 必须没有源资产,`refine` 必须绑定当前 manifest 中唯一图片资产。`approvalMode` 仍只有 `strict` 可有效写入;其它值只能作为不可用选项展示。 ### 5.2 资源画布布局(P1) -实现状态(2026-08-03):dependency / type 双模式通过项目内 CAS sidecar 独立持久化;dependency 模式由 Tauri Rust 只读构建关系拓扑与确定性依赖深度、前端 SVG 派生几何,图结构和线段均不写入布局 sidecar。依赖图加载完成前设布局初始化屏障,避免以临时 `dependencyDepth=0` 生成并持久化错误坐标。当前用户入口只允许自动布局与资源卡点击;资源卡手动拖动已按 mentor 决定暂缓。历史 sidecar 坐标继续只读恢复,底层布局读写与 CAS 合同保留,但当前没有用户手动布局入口。资源替换、缩放 / 平移等其余 P1 能力仍按本文非目标保持未实现。 +实现状态(2026-08-03):dependency / type 双模式通过项目内 CAS sidecar 独立持久化;dependency 模式由 Tauri Rust 只读构建关系拓扑与确定性依赖深度、前端 SVG 派生几何,图结构和线段均不写入布局 sidecar。依赖图加载完成前设布局初始化屏障,避免以临时 `dependencyDepth=0` 生成并持久化错误坐标。资源总览当前只允许自动布局与资源卡点击,资源卡手动拖动继续暂缓;这不限制素材创作无限画布的 viewport 平移/缩放和图片图层移动/缩放,两套坐标及 sidecar 完全独立。 ```ts type ProjectResourceCanvasLayout = { @@ -393,7 +407,8 @@ type ProjectAgentMudPointAttribution = { - 已实施依赖/类型两套坐标持久化、首次默认不重叠布局、历史坐标跨重启恢复与自动协调 CAS 冲突处理;资源卡手动拖动暂缓。 - 资源关系线在布局持久化验收通过后单独实施,不与本切片捆绑伪造完成。 - 已实施正式版本只读模型、版本卡、父子关系与引用资源高亮;资源兼容性判断和不可变下一迭代版本创建仍待后续切片。 -- 美术/音频编辑状态接线。 +- 素材创作无限画布阶段一按权威专题一次交付图片导入、编辑、生成、导出、草稿恢复、正式本地回写、即时投影和焦点竞态闭环。 +- 高级抠图、图集、角色动画、视频编辑和音频编辑按后续切片实施。 ### P2 @@ -450,16 +465,27 @@ type ProjectAgentMudPointAttribution = { ### 7.5 阶段七完整验收 -1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核阶段零至阶段六;美术编辑生成新资源继续按本 PRD 已确认的闭环条件暂缓,不作为遗漏或伪完成。 +1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核资源管理阶段零至阶段六;该阶段本身保持只读收口。后续图片素材创作按 `2026-08-05` 阶段一合同实施,不得再引用阶段七的历史暂缓文字关闭新入口。 2. AppSurface 同时覆盖文档、图片、SVG、音频和视频聚焦;视频必须使用原生 `controls` 且 `preload="metadata"`,读取策略失败时中央主视窗显示安全空态,右侧对话和底部 Agent 状态栏继续存在。 3. `1280×800` 应用内浏览器实测 `window`、document 与 body 均无页面级横向或纵向溢出。浏览器开发页受真实登录门禁保护,不为验收绕过认证或伪造 Tauri;工作台内部结构由 AppSurface 集成测试与资源布局 CSS 合同测试复核。 4. 根目录全量 Vitest、前后端 typecheck / lint / build、Rust workspace test / check、SpacetimeDB schema、原生壳、内容 / 编码、生产运维与部署门禁全部通过后,阶段七才允许提交。 5. 本地 `.env`、`.env.local`、密钥、缓存、日志和构建产物不进入阶段七提交;提交前再次执行编码检查和 `git diff --check`。 +### 7.6 素材创作无限画布阶段一验收 + +1. 网站与 Tauri 实际 import 同一份 `@genarrative/image-canvas-core` 和 `@genarrative/image-canvas-react`,客户端没有复制的主站画布目录;宿主差异只位于 adapter。 +2. “新增资源”和“精修资源”分别进入 create/refine 素材画布;精修保留原资产、创建新资产,并用规范 `referenceResourceIds` 登记直接血缘。 +3. 草稿 schema、revision、容量、项目身份、OS 锁、CAS、恢复副本和媒体引用符合权威专题;损坏、未知 schema、身份错配和超限均失败关闭。 +4. 正式提交携带 `expectedProjectId + expectedRevision + expectedDraftRevision + commitId + idempotencyKey`,按文件、manifest/revision、回读、ledger/draft、事件顺序完成;两窗口并发、重复提交和各崩溃阶段均有确定结果。 +5. 保存成功后不刷新、不重开项目即可进入 manifest 投影、依赖图、dependency/type 布局和允许时的选中定位;切项目、切中央状态、改选择或改搜索后的迟到结果不得抢焦点。 +6. 搜索/筛选隐藏新资源时保留条件,明确提示“新资源已保存,当前筛选条件下不可见”,只通过显式动作清除条件并定位。 +7. 新增、精修、生成、保存、取消、失败和恢复必须覆盖权威专题 §13 的完整验收矩阵;只完成画布 UI 或只完成本地写文件都不能算正式闭环。 + ## 8. 非目标 -- 当前收口不实现资源卡手动拖动,也不实现资源聚焦工具栏、资源聚焦工具侧边栏、美术编辑、音频编辑 / 替换、资源重新生成、资源替换、下一迭代版本创建入口、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因。正式版本记录已经成为 manifest 业务真相,但当前只读取、校验和展示已有记录。 -- 本切片不持久化资源聚焦状态、画布缩放 / 平移、搜索条件、筛选条件或当前 mode;聚焦退出时的列表上下文恢复只限当前前端会话,这些状态如需跨重启保存必须另行扩展合同,不能塞入 `game-creator-resource-layout.v1`。 +- 资源总览当前不实现资源卡手动拖动,也不实现通用聚焦工具栏/工具侧边栏、下一迭代版本创建入口、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因。正式版本记录已经成为 manifest 业务真相,但当前只读取、校验和展示已有记录。 +- 素材创作阶段一不实现高级蒙版/毛发级抠图、图集、角色动画、视频编辑或音频编辑;图片画布平移/缩放、图层选择/移动/缩放、撤销重做、导入、基础编辑、生成、导出和本地回写明确不是非目标。 +- 资源总览不持久化资源聚焦状态、搜索条件、筛选条件或当前 dependency/type mode;其会话上下文不能塞入 `game-creator-resource-layout.v1`。素材创作 viewport 和图层状态按独立 `game-creator-asset-canvas-draft.v1` 保存,不能混用资源总览 sidecar。 - 不修改 SpacetimeDB schema。 - 不开放普通用户 Agent.md/Skill。 - 不自动确认 Agent 动作,不自动触发可能扣费的生成。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d01b3b7d8..bfd16db78 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,17 @@ # 决策记录 +## 2026-08-05 素材创作无限画布共享源码并以本地事务形成图片闭环 + +- 背景:资源管理阶段七已经完成 manifest 实时投影、dependency/type 布局、依赖图和中央只读聚焦,但“新增资源/精修资源”仍缺少草稿、生成、正式回写、血缘、布局和焦点竞态的完整合同。网站已有成熟图片画布,若直接复制进 Tauri 会形成两份长期分叉的画布内核和通用 UI。 +- 产品决策:项目工作台中央主视窗把 `resource-overview` 与 `asset-canvas(create|refine)` 建模为两个状态。“新增资源”和“精修资源”进入素材画布,资源总览卡片继续不可拖动,素材画布图片图层必须支持平移/缩放、选择/移动/缩放、撤销重做、导入、基础编辑、生成、导出和本地回写。首版只正式闭环图片;高级抠图、图集、角色动画、视频和音频编辑后续分期。 +- 架构决策:现役网站画布抽取到 `packages/image-canvas-core` 与 `packages/image-canvas-react`,网站和 Tauri 实际 import 同一源码,通过 Web/Tauri Host Port 注入差异。Web 保留账户、钱包、服务端 editor project 和云端素材库;Tauri 保留本地项目、受控文件、manifest、项目 revision 和 External Editor API。禁止复制整个 `src/components/image-editor/` 到客户端。 +- 持久化决策:Tauri 草稿使用 `.agent/workbench/asset-canvas/` 下的 `game-creator-asset-canvas-draft.v1`,以 `expectedProjectId + expectedDraftRevision` 在 OS 句柄锁内 CAS;JSON 最大 `2 MiB`、最多 `4096` 层,媒体只保存受控引用。正式 `commit_local_project_asset` 同时绑定项目 `expectedRevision`、草稿 revision、`commitId/idempotencyKey` 和 staging 摘要,按 prepared journal、最终图片、manifest/revision 可恢复更新、回读、ledger/草稿、最后 Tauri event 推进;事件 `game-creator-local-asset-committed` 至少一次并按固定 eventId 去重。 +- 血缘与并发:refine 永远保留源文件/asset 并创建 `canvas-`。源没有外部 resourceId 时补齐 `local-asset:`,新资产只通过规范 `referenceResourceIds` 引用,禁止混用裸 manifest asset ID。两个窗口基于同一 project/draft revision 最多一个成功;相同提交返回 already-committed,同键不同请求失败关闭。恢复只依据 journal、before/after 摘要和 ledger,不按文件存在、mtime 或 PID 猜测。 +- 投影与焦点:提交返回完整当前 manifest;同项目仍活动时立即更新 manifest 投影、依赖图输入和两种布局,不要求刷新或重开。自动选中还必须复核 project/path、asset-canvas session/draft/intent、selection epoch 和 search/filter epoch;用户已切项目、切模式、选其它资源或新资源被条件隐藏时不得抢焦点,隐藏时保留条件并提供显式清除/定位动作。 +- 影响范围:下一阶段的共享画布包、网站 adapter、`apps/ai-game-creator-shell` 前端与 Tauri Rust 本地持久化;不修改 SpacetimeDB schema,不把草稿或资源布局 sidecar 变成 manifest 业务真相。 +- 验证方式:按权威专题的 28 项矩阵覆盖 Web/Tauri 共用源码、新增/精修、生成响应丢失、重复提交、两窗口并发、事务各崩溃点、草稿恢复、切项目/切状态/改选择/改筛选迟到结果和不刷新即时投影。 +- 关联文档:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 + ## 2026-08-03 资源管理阶段七以完整 CI 与可重复界面合同收口 - 背景:飞书资源管理需求的阶段零至阶段六已经分别完成资源卡禁拖、固定资源投影、中央聚焦、安全文档 / 媒体预览、依赖深度与正式版本只读模型;最后需要统一复核需求边界并用当前主分支完整门禁排除集成回归。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 701031b5c..e58df155b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -14,6 +14,22 @@ - 关联:相关文件、文档、提交或 Issue ``` +## 正式素材提交不能把多文件写入或 Tauri 事件误当成一次原子动作 + +- 现象:图片已经落到 `assets/` 但 manifest 没有资产,或 manifest 已追加而 project revision/草稿仍是旧值;进程在 emit 前后退出后,用户重试又得到第二份图片、第二个 asset 或重复选中。 +- 原因:文件系统只保证单文件原子替换,不能让最终图片、manifest、`.agent/runtime/project-revision.json`、commit ledger 和草稿跨文件物理原子;Tauri event 也没有跨崩溃 exactly-once。若先写副作用再临时生成幂等身份,或只凭目标文件存在推断成功,就无法区分未提交、已提交未回包和部分提交。 +- 处理:第一次保存前冻结 `commitId + idempotencyKey + eventId + requestFingerprint`,在项目 write lock 内先写 prepared journal 和 before/after 摘要,再按最终图片、manifest/revision 逻辑原子更新、回读、ledger/草稿提交推进,释放锁后最后 emit。恢复只按 journal stage、精确字节摘要和 ledger 前向完成/安全回滚;矛盾状态进入 reconciliation-required。事件采用至少一次,监听方按 eventId 和 project revision 去重。 +- 验证:分别在 prepared、图片安装、manifest 安装、revision 安装、ledger 提交、emit 和投递标记后强杀;确认只有唯一 `canvas-`、revision 最多推进一次、源资产与血缘正确,响应丢失后返回 already-committed,矛盾 fixture 不自动重试。 +- 关联:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`。 + +## 素材保存成功不等于迟到结果仍有权抢占当前焦点 + +- 现象:用户等待生成/保存时切到另一个项目、run、另一份素材草稿,或主动选择其它资源、修改搜索条件;旧请求完成后界面却切回旧画布、清空筛选并自动选中新资源。 +- 原因:异步回调只检查“请求成功”或捕获的旧 `isMounted/projectId`,没有绑定中央状态 session、draft/intent、selection epoch 和 query epoch;manifest 投影这一数据事实又被错误地与“当前应自动聚焦”的用户意图合并处理。 +- 处理:保存开始捕获 `projectPath + projectId + centerKind + sessionId + draftId + intent + selectionEpoch + queryEpoch`,响应时从当前 ref/store 完整复核。manifest 可以按精确项目身份更新当前上下文或后台缓存,但自动切状态、选择、滚动和聚焦必须等当前 mode 布局 ready 且全部焦点守卫仍相等。新资源被搜索/筛选隐藏时保留条件与选择,提示“新资源已保存,当前筛选条件下不可见”,只提供显式清除/定位动作。 +- 验证:使用 deferred commit/layout Promise,依次在请求后切项目、切 run/overview、新开 session、改选择和改筛选;断言 manifest 只更新对应项目,新资源仍进入投影/布局,但所有失效守卫都不切中央状态、不改选择、不清查询。条件未变化且资源可见时才自动定位。 +- 关联:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`。 + ## Linux 生产脚本门禁不能假设本地也是 GNU userland - 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现 `mv: illegal option -- T`、`mapfile: command not found`、`/usr/bin/cp` / `/usr/bin/chmod` 不存在,以及 `.rlib` 明明含有 `.o` 却报告“没有可扫描成员”;安全修复计划还会把 `/var/folders` 到 `/private/var/folders` 的系统别名误判为用户符号链接。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 28023064d..bf85145b0 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -346,6 +346,8 @@ game-project/ 美术组和音乐组复用现有画板能力,不另建平行资产系统。 +`2026-08-05` 起,客户端图片精修不再通过“打开另一份主站画布副本”实现,而按 [`【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`](./【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md) 把现役网站图片画布抽取为共享 `@genarrative/image-canvas-core` 与 `@genarrative/image-canvas-react`。网站和 Tauri 实际 import 同一份 core/React/UI 源码,分别注入 Web 与 Tauri Host Port;禁止把 `src/components/image-editor/` 整目录复制到客户端。 + - 美术组通过画板链路生成角色、场景、UI、图标、动画和宣传素材。 - 音乐组复用画板已有 `audio`、`sound-effect`、`background-music` 能力。 - 默认后台生成,用户需要精修时打开画板继续编辑。 @@ -357,6 +359,9 @@ game-project/ - 项目工作台点击已登记图片时必须在中央主视窗的资源聚焦状态中直接渲染图片,而不是只展示路径与 MIME。图片通过受控 Tauri 命令从项目 `assets/` / `game/` 读取,只允许 manifest 已登记资产或已完成任务产物,并复用 `file.read` auto 权限、图片魔数、文件大小、像素尺寸、普通文件、路径漂移和符号链接校验后以 data URL 返回;首版只支持 PNG、JPEG、WEBP,不向 WebView 暴露任意本机文件协议或绝对路径。 - 2026-08-03 阶段四在上述图片链路外新增 `read_local_project_text_preview` 与 `read_local_project_media_preview`。前者只接收当前 manifest 已登记文档或已完成任务中的 Markdown / 文本 / JSON / YAML / TOML,限制 2 MiB 与 UTF-8;Agent 文本回执继续直接消费合法对话投影,不反查本地路径。后者的美术分支接收 GIF、安全 SVG、AVIF、BMP、MP4、WebM、MOV,音频分支只接收 manifest 已登记的 MP3、WAV、OGG / Opus、M4A、AAC、FLAC,二进制媒体限制 32 MiB。两条命令统一执行 `file.read` auto 权限、规范化相对路径、项目边界、敏感路径、普通文件、父目录链接、硬链接、读取漂移和重开身份复核;媒体按文件签名而非只按扩展名或 MIME 建立 data URL,SVG 额外拒绝活动内容与外部引用。 - `canvas.export_import` 复用 `/editor/canvas` 已有素材导出 ZIP 格式,读取根 `metadata.json`、复制 `images/` / `media/` / `sequences/` 到本地项目 `assets/canvas-imports/`,再按导出层登记为 `canvas` 来源资产;导出包不保存真实 resourceId 时,使用 `canvas-export:` 作为可追踪 assetObjectId,不伪造后端资源行。 +- 阶段一正式闭环只覆盖图片。网站 adapter 保留账户、钱包、服务端 editor project、云端素材库、OSS/asset object 与现有生成 API;Tauri adapter 使用本地项目、受控媒体、`game-creator-asset-canvas-draft.v1` 草稿、manifest、项目 mutation revision 和 External Editor API。高级抠图、图集、角色动画、视频和音频编辑后续分期。 +- Tauri 正式保存必须通过受控 staging 与 `commit_local_project_asset`,携带 `expectedProjectId + expectedRevision + expectedDraftRevision + commitId + idempotencyKey`。事务固定为 prepared journal、最终图片、manifest/revision 可恢复更新、回读验证、committed ledger/草稿、最后发布 `game-creator-local-asset-committed`;不能继续用先推进 revision 再分别登记资产的旧命令拼装正式闭环。 +- refine 默认保留源文件和源 manifest asset,新建 `canvas-` 资产。源资产没有外部 `source.resourceId` 时在同一 manifest 事务中补齐 `local-asset:`,新资产通过 `referenceResourceIds` 引用该规范身份;禁止把裸 manifest asset ID 冒充 External Editor resource ID。 ## GameAgent V1.0 项目开发工作台首版界面 @@ -365,9 +370,9 @@ game-project/ 2026-07-20 起,产品状态机、P0/P1/P2 范围与后续数据合同以 [`【AI游戏创作】项目开发工作台PRD-2026-07-20.md`](../prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md) 为准;本节只保留当前实现边界。 - 页面骨架固定为左侧现有全局导航、中间主视窗、右侧陶泥儿对话和底部子 Agent 状态栏;不新建第二套客户端或平行项目页。 -- 中间主视窗提供 `资源管理 / 运行` 切换。`code-prototype` 任务完成前运行入口保持视觉不可用,但仍可点击查看“当前无可运行版本”,不能使用会阻断说明交互的原生 `disabled` 或 `aria-disabled`;完成后才允许进入运行表现层。切回资源管理只修改前端展示态,不伪造后端预览暂停结果。 +- 中间主视窗提供 `resource-overview / asset-canvas / run` 三种状态。资源总览的“新增资源”和图片聚焦的“精修资源”进入 create/refine 素材创作无限画布;素材画布只替换中央区域,不覆盖右侧 Supervisor 或底部 Agent。`code-prototype` 任务完成前运行入口保持视觉不可用,但仍可点击查看“当前无可运行版本”,不能使用会阻断说明交互的原生 `disabled` 或 `aria-disabled`;完成后才允许进入运行表现层。切回资源总览只修改前端展示态,不伪造后端预览暂停结果。 - 资源管理从当前 `GameCreationAppManifest`(包含可选 `versions`)、合法 Agent 文本回执和已导入附件派生资源,固定按文档、项目版本、美术资源、音乐音效资源分区;未知任务产物不再兜底为版本,任务声明中的未登记音频也不冒充正式音频。`按依赖 / 按类型` 使用各自前端排列,dependency 模式额外绘制当前 manifest 与资源投影可证明的依赖关系。排列与图层都不写回 manifest,不能推断或伪造缺失依赖。 -- 资源卡支持点击聚焦、搜索和类型筛选。2026-07-28 起完成两套二维坐标与本地 CAS sidecar;2026-07-31 起 dependency 模式增加不持久化的原生 SVG 关系图层。2026-08-03 mentor 决定暂缓资源卡拖动,当前卡片不挂载 Pointer Down / Move / Up / Cancel 拖动入口,只允许自动布局和点击聚焦。聚焦态替换中央主视窗内容,保留左侧导航、右侧对话和底部 Agent 状态栏,退出后恢复搜索、布局模式、滚动位置与选中资源;不提供工具栏、工具侧边栏或可拖动标题栏。阶段四已补齐安全本地文档、扩展美术媒体与音频聚焦,正文独立滚动,视频 / 音频使用内置媒体控件,失败显示空态;美术编辑、音频编辑 / 替换、版本替换或运行模块仍不在本阶段。 +- 资源卡支持点击聚焦、搜索和类型筛选。2026-07-28 起完成两套二维坐标与本地 CAS sidecar;2026-07-31 起 dependency 模式增加不持久化的原生 SVG 关系图层。2026-08-03 mentor 决定暂缓资源总览卡片拖动,当前卡片不挂载 Pointer Down / Move / Up / Cancel 拖动入口,只允许自动布局和点击聚焦。聚焦态替换中央主视窗内容,保留左侧导航、右侧对话和底部 Agent 状态栏,退出后恢复搜索、布局模式、滚动位置与选中资源;不提供通用工具栏、工具侧边栏或可拖动标题栏。阶段四已补齐安全本地文档、扩展美术媒体与音频聚焦,正文独立滚动,视频 / 音频使用内置媒体控件,失败显示空态。该资源总览边界不限制后续素材创作无限画布内的图片图层移动/缩放、生成和正式回写。 - 运行表现层首版直接嵌入当前项目的 loopback 游戏画面,并展示上一项 / 暂停继续 / 下一项切片控制、素材信息和数值微调面板。`preview.start` 启动本地 server 后把真实 URL 回写工作台,`preview.open` 只激活客户端内运行视图,不再调用系统浏览器;切片、参数调整和自然语言新增调节项首版仍只保留本地 UI 草稿,不修改代码或 manifest。 - 右侧继续复用现有 Project Supervisor 会话、Runtime 澄清和确认链路;输入区展示 `严格审批 / 风险审批 / 无需审批` 独立面板。P0 只有严格审批可选;风险审批和无需审批保持视觉不可用但允许点击查看原因,不替代 Runtime 的逐动作权限、确认、sandbox 或 reconciliation 门禁。风险 Rank 算法记录在 `docs/project-memory/todos/【待解决】AI游戏创作高风险审批Rank-2026-07-20.md`,前端不得自行计算。 - 底部状态栏默认展示策划、美术、程序 3 组,并允许在同一栏展开数值、音频、发布组;状态来自 manifest 与当前 Supervisor run 的 Runtime,悬停显示当前任务与进度。累计泥点必须等待后端计费归因投影;Agent.md 编辑和自定义 Skill 在来源审核、版本、权限、sandbox 与回滚合同完备前不向普通用户开放。 @@ -391,7 +396,7 @@ game-project/ - 新资源只在第一次进入某个 mode 时计算默认不重叠位置;全部现存坐标保持不变。搜索、筛选、窗口 resize 和 mode 切换不得重排或回写已有坐标,窄视图通过 section 画布范围与滚动访问,不裁切持久坐标。 - type 默认布局固定按 `subtype -> mediaType -> label -> id` 排序。manifest 资产的 subtype 使用 `asset.kind`,任务产物、导入附件和 Agent 文本成果使用稳定的来源 fallback;subtype 必须进入资源协调签名,不能因 MIME 相同而退化成按名称混排。 - 自动协调保存失败时保留当前会话布局;CAS 冲突载入对方最新布局,需要继续协调时最多追加两次重试,持续跨窗口竞争时停止自旋。用户提示只说明“布局已在其他窗口更新”,不要求重新拖动。损坏、未知 schema、身份冲突、超限与链接文件失败关闭,不能用空布局覆盖原文件。 -- 本布局持久化切片不包含资源关系线、资源替换、聚焦态持久化、缩放 / 平移、搜索 / 筛选条件、当前 mode,也不修改 `api-server` 或 SpacetimeDB。资源关系线与当前会话内中央聚焦已在后续独立前端切片接入,不改变本段 sidecar 合同;其余 P1 能力继续独立实施。 +- 本资源总览布局 sidecar 不包含资源关系线、资源替换、聚焦态持久化、资源总览缩放 / 平移、搜索 / 筛选条件、当前 mode,也不修改 `api-server` 或 SpacetimeDB。资源关系线与当前会话内中央聚焦已在后续独立前端切片接入;素材创作 viewport 和图层使用独立草稿 schema,不能写入 `game-creator-resource-layout.v1`。 历史实施顺序已完成 TypeScript / Rust DTO、Tauri sidecar/CAS、前端纯模型与持久 Hook。二维手动拖动接线现已暂缓;重新开放前必须先更新 PRD 与验收合同。任何后续步骤不得用 `localStorage`、manifest 字段或只在当前 React 会话有效的状态冒充项目持久化。 @@ -429,11 +434,16 @@ game-project/ 3. 扩展 `apps/ai-game-creator-shell` 的本地能力:项目目录、文件写入、受限命令、本地 HTTP 预览。 4. 用户侧项目开发页提供资源 / 运行工作台、当前项目的 loopback 游戏画面、陶泥儿对话、上传结果和 Agent 状态栏;开发专用单 Agent 对话、原始任务 / 文件面板、预览诊断面板和命令日志只在开发构建的独立开发窗口展示。 5. 将美术组、音乐组接入现有画板与外部生成队列。 +6. 从现役网站画布抽取共享 `image-canvas-core/react`,先让网站改为消费共享源码,再由 Tauri adapter 接入;不得复制整个画布目录。 +7. 实现 Tauri 草稿/media/staging/commit/transaction/event 合同和跨窗口锁、CAS、幂等与崩溃恢复。 +8. 在项目工作台接入 create/refine 状态机、保存后的 manifest 即时投影、依赖图与两种布局协调,以及 project/session/selection/query 焦点守卫。 +9. 按权威专题验收矩阵同时完成 Web、Tauri、共享包、Rust 持久化和 AppSurface 证据后,才开放正式入口。 ## v1 验收 - 用户能创建本地 Web 游戏项目。 - 用户进入项目开发页后能看到资源管理主视窗、陶泥儿对话栏和底部策划 / 美术 / 程序 Agent 状态栏;`1280×800` 最小横屏窗口和更大桌面窗口均不得出现页面级横向 / 纵向溢出,对话输入与底部 Agent 状态栏始终位于视口内。 +- “新增资源”和“精修资源”进入共用中央区域中的素材创作无限画布;图片导入、画布平移/缩放、图层选择/移动/缩放、撤销重做、基础编辑、生成、导出和 Tauri 正式保存形成完整闭环。保存后不刷新、不重开即可进入 manifest、依赖图、布局和选中流程,迟到结果不能覆盖用户后来切换的项目、状态、选择或搜索意图。 - 资源管理可在按依赖 / 按类型之间切换、搜索资源并点击打开当前资源详情;dependency 模式展示可验证的资源引用和聚合任务流,搜索过滤端点、选择高亮直接上下游。资源卡不可拖动,Pointer Move 不更新坐标、线段或手动布局;所有展示数据来自当前 manifest、当前资源投影或当前项目导入附件。 - 首个 `code-prototype` 任务未完成时运行入口不可进入并给出可感知提示;完成后可进入运行表现层,真实预览直接加载到客户端内受限运行容器。 - 审批档位通过独立弹出面板切换,默认严格审批;界面选择不得绕过 Runtime 现有确认门禁。 @@ -898,3 +908,13 @@ game-project/ - 嵌入项目工作台的 Project Supervisor 在本地 manifest 状态变化时向启动器外传完整 manifest,并携带来源项目路径。启动器只更新仍为同一路径的活动项目上下文;资源列表、依赖图输入、任务状态、运行入口和正式版本卡必须在当前页面实时重投影,不要求关闭或重开项目。 - `.agent/agent.db` 有界尾部读取报告截断时,审计 producer 映射失败关闭,不生成基于不完整审计的 producer 或 task flow。前端收到截断 DTO 时再次清空 producer、task flow、任务环和依赖深度派生结果;只依赖 manifest 唯一外部资源 ID 的精确引用关系继续保留。 - 资源依赖 SVG 继续作为不可交互装饰层隐藏,但 dependency 画布通过 `aria-describedby` 提供当前可见精确引用和任务流的文本等价列表。中央资源聚焦关闭或按 Escape 退出后恢复触发卡片焦点;橙色引用线及箭头使用对 `#fffdfa` 画布达到至少 `3:1` 的颜色。 + +## 2026-08-05 客户端素材创作无限画布阶段一合同 + +- 资源总览画布与素材创作无限画布是项目工作台中央主视窗的两个状态。资源总览继续消费 manifest、dependency/type 布局与 Rust 依赖图;素材画布以 `sessionId + draftId + create/refine` 建立独立生命周期。“新增资源”和“精修资源”只切换中央区域,右侧 Supervisor 与底部 Agent 状态栏常驻。 +- 网站与 Tauri 共用 `packages/image-canvas-core` 和 `packages/image-canvas-react`;网站保留账户、钱包、服务端项目和云端素材库 adapter,Tauri 保留本地项目、受控文件、manifest、项目 revision 和 External Editor API adapter。现役网站画布是抽取来源,不允许整体复制到客户端。 +- 本地草稿固定保存到 `.agent/workbench/asset-canvas/`,schema 为 `game-creator-asset-canvas-draft.v1`,以 `expectedProjectId + expectedDraftRevision` 做 OS 锁内 CAS;单 JSON `2 MiB`、最多 `4096` 层,媒体正文只进入受控 media/staging 文件。损坏、未知 schema、身份错配、超限和恢复摘要不匹配全部失败关闭。 +- 正式资产命令固定为 `commit_local_project_asset`,同时绑定 `expectedProjectId`、项目 `expectedRevision`、`expectedDraftRevision`、`commitId`、`idempotencyKey` 和 staging 摘要。事务在项目锁内按 prepared、最终图片、manifest/revision 逻辑原子更新、回读、ledger/草稿提交推进,释放锁后最后发布 `game-creator-local-asset-committed`;事件至少一次并按固定 eventId 去重。 +- refine 不覆盖源文件或复用源 asset ID。源缺少外部 resourceId 时补齐 `local-asset:`,新 `canvas-` 资产用 `referenceResourceIds` 登记源和其它直接引用,避免混淆 manifest asset ID 与 External Editor resource ID。 +- 提交返回完整最新 manifest;当前项目仍匹配时立即更新项目上下文、资源投影、依赖图和两种布局,布局 ready 后才按焦点守卫决定选中。用户已切项目、切状态、开始新 session、选择其它资源或改变搜索/筛选时,迟到结果不得抢焦点;新资源被隐藏时保留条件并提供显式清除/定位动作。 +- 图片首版包含平移、缩放、图层选择/移动/缩放、撤销重做、导入、基础编辑、生成、导出和本地回写。高级抠图、图集、角色动画、视频与音频编辑后续分期;本阶段不修改 SpacetimeDB schema。 diff --git a/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md b/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md new file mode 100644 index 000000000..83ca8aedf --- /dev/null +++ b/docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md @@ -0,0 +1,884 @@ +# 客户端素材创作无限画布阶段一合同 + +更新时间:`2026-08-05` + +状态:阶段一产品与技术合同已冻结;当前提交只更新文档,不代表代码已经实现。 + +本文是网站与 AI 游戏创作 Tauri 客户端共享图片画布能力的下一阶段编码依据。若本文与资源管理阶段七的“美术编辑暂缓”口径冲突,以本文对后续素材创作切片的更新决定为准;资源总览既有布局、依赖图和只读聚焦合同继续有效。 + +## 1. 冻结结论与范围 + +1. 项目工作台中央主视窗必须把“资源总览画布”和“素材创作无限画布”建模为两个不同状态;两者复用同一个工作台壳,不创建平行项目页。 +2. 资源总览顶部的“新增资源”和图片资源聚焦态的“精修资源”进入素材创作无限画布。资源总览卡片仍不可拖动;素材创作无限画布中的图片图层必须可选择、移动和缩放,二者不是同一种交互。 +3. 阶段一正式闭环只覆盖图片。PNG、JPEG、WebP 的导入、画布平移与缩放、单选与多选、图层移动与缩放、层序、显隐、锁定、翻转、分组、撤销与重做、裁剪/扩图等基础编辑、图片生成、导出和 Tauri 本地正式回写均属于目标,不得再列为非目标。 +4. 现有一键去背景能力可以通过共享 Host Port 接入;毛发级抠图、可编辑蒙版和高级边缘修复后续分期。图集、角色动画、视频编辑和音频编辑不进入本阶段正式闭环。 +5. 网站和 Tauri 必须实际 import 同一份画布 core 与 React/UI 源码。现役 `src/components/image-editor/` 是抽取来源,不得把整个目录复制到客户端,也不得形成网站版和 Tauri 版两份长期分叉的画布实现。 +6. 网站继续负责账户、钱包、服务端编辑器项目和云端素材库;Tauri 继续负责本地项目、受控文件读写、manifest、项目 mutation revision 和 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。 +- Tauri adapter 可以依赖 `invoke/listen`、本地项目上下文、受控媒体命令、manifest、项目 revision、草稿 sidecar 和 External Editor 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< + ImageCanvasHostResult + >; + createDraft(input: ImageCanvasHostScope): Promise< + ImageCanvasHostResult + >; + 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、云端素材库及账户/钱包链路。 +- Tauri adapter 把草稿、导入、导出和提交映射到本文第 7 至 11 节的本地合同;生成通过 External Editor API,API Key 只从 Tauri 应用配置读取,不能进入项目 sidecar、manifest、事件或错误正文。 + +## 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 | 崩溃:prepared 后 | 尚未装图片 | 恢复安全回滚 staging/transaction 或继续,不生成幽灵 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 基线;不可信进入对账,不猜测 | + +下一阶段只有在矩阵对应的纯模型、共享 React、Web adapter、Tauri adapter、Rust 持久化与 AppSurface 测试全部通过后,才可宣称图片素材创作正式闭环完成。