合并master并保留Spine序列帧修复

合并master当前工程、后端、前端和文档更新

按已确认方案解决Spine序列帧多模态、帧数和快速编辑冲突

保留当前分支底部工具栏宽度与隐藏滚动条样式
This commit is contained in:
2026-08-14 21:33:26 +08:00
338 changed files with 69729 additions and 8828 deletions
+3
View File
@@ -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)
- [图片画布游戏场景生成链路](./technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md)
@@ -3,7 +3,7 @@
"info": {
"title": "陶泥儿外部编辑器 OpenAPI",
"version": "1.0.0",
"description": "外部系统调用陶泥儿图片画布项目、画布布局、素材库,以及图片、视频、音效、音乐等编辑器素材生成/编辑能力的 v1 契约。全部生成 POST 都是异步提交:必须携带 Idempotency-Key,收到 202 后使用 operationId 查询统一生成状态。支持远程 MCP 的 Agent 可连接 /api/external/v1/mcp;不支持 MCP 的 Agent 可从 /api/external/v1/skill.zip 下载完整 Skill 包。新建 projectId 使用 proj- 前缀,新建 taskId / operationId 使用 task- 前缀;历史 editor-project-*、aitask_*、extgen-* ID 仍可作为既有资源标识传入。\n\n兼容性说明:v1 当前处于无外部存量调用方阶段,正式对外发放 API Key 之前,契约可能在不升 info.version、不设弃用期的情况下发生包含字段移除在内的破坏性变更。生成客户端时请勿假定本文档已冻结。"
"description": "外部系统调用陶泥儿图片画布项目、画布布局、素材库,以及图片、视频、音效、音乐等编辑器素材生成/编辑能力的 v1 契约。全部生成 POST 都是异步提交:必须携带 Idempotency-Key,收到 202 后使用 operationId 查询统一生成状态。支持远程 MCP 的 Agent 可连接 /api/external/v1/mcp;不支持 MCP 的 Agent 可从 /api/external/v1/skill.zip 下载完整 Skill 包。新建 projectId 使用 proj- 前缀,新建 taskId / operationId 使用 task- 前缀;历史 editor-project-*、aitask_*、extgen-* ID 仍可作为既有资源标识传入。\n\n兼容性说明:截至 2026-08-08v1 当前线上 API Key 与调用方状态确认仍无外部存量调用方正式对外发放 API Key 之前,契约可能依据明确决策在不升 info.version、不设弃用期的情况下发生包含字段移除在内的破坏性变更。生成客户端时请勿假定本文档已冻结。"
},
"servers": [
{
@@ -1088,6 +1088,17 @@
],
"operationId": "editExternalEditorImage",
"summary": "重绘/调整编辑器图片素材",
"description": "主来源只接受当前账号已登记的项目资源 ID 或素材 IDsourceReferenceId);objectKey、URL、Data URL 与 Blob URL 即使归属当前账号也返回 400。服务端从命中的业务记录派生 canonical objectKey、assetObjectId 与权威类型;快速编辑的完整有效类型白名单为普通静态图片(类型为 null)、spec、character、icon-spritesheet、icon-spec、publication-material、ui-design 和 scene,其他及未知类型返回 400。提供 targetLayerId 时必须同时提供 projectId,目标图层必须关联有效项目资源;双方都有 assetObjectId 时必须相同,否则回退比较 canonical bucket/objectKey。同一对象的来源默认类型与目标资源默认类型冲突、目标有效类型或媒体类型不允许、来源或目标不存在/越权/缺少对象时均返回 400,任务不会入队。referenceImageSrcs 仍只作为辅助参考图。",
"x-genarrative-allowed-effective-asset-kinds": [
null,
"spec",
"character",
"icon-spritesheet",
"icon-spec",
"publication-material",
"ui-design",
"scene"
],
"security": [
{
"ExternalApiKey": []
@@ -3450,28 +3461,24 @@
"type": "object",
"required": [
"prompt",
"sourceImageSrc"
"sourceReferenceId"
],
"properties": {
"prompt": {
"type": "string",
"minLength": 1
},
"sourceImageSrc": {
"sourceReferenceId": {
"type": "string",
"description": "待重绘/调整图片的稳定引用:当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。"
"minLength": 1,
"description": "待编辑主来源的业务 ID,只接受当前账号已登记的项目资源 ID 或素材 ID。objectKey、普通 URL、签名 URL、Data URL、Blob URL 和未登记上传对象均返回 400;上传对象必须先登记为项目资源或素材。服务端从命中记录派生 canonical objectKey、assetObjectId 与权威类型。"
},
"projectId": {
"type": [
"string",
"null"
]
},
"assetKind": {
"type": [
"string",
"null"
]
],
"description": "项目上下文。提供 targetLayerId 时必须同时提供非空 projectId,否则返回 400。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
@@ -3488,18 +3495,12 @@
"null"
]
},
"sourceResourceId": {
"type": [
"string",
"null"
]
},
"targetLayerId": {
"type": [
"string",
"null"
],
"description": "带 projectId 且未提供 canvasCompletion 时,服务端用生成结果替换该画布图层。"
"description": "目标画布图层。提供时必须同时提供 projectId,且图层必须关联有效项目资源。来源与目标都有 assetObjectId 时按 ID 比较;任一缺失时回退比较 canonical bucket/objectKey。来源记录默认类型必须与目标资源默认类型一致,最终类型取 assetKindOverride 或目标资源类型,且媒体类型必须为图片;违反任一条件返回 400。未提供 canvasCompletion 时,生成结果替换该图层。"
},
"size": {
"type": "string"
@@ -3531,7 +3532,7 @@
"type": "array",
"items": {
"type": "string",
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceImageSrc 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
"description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceReferenceId 对应的主来源原图占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。"
},
"maxItems": 8
},
@@ -1,6 +1,6 @@
# AI 游戏创作项目开发工作台 PRD
更新时间:`2026-08-10`(现有 Godot 项目入口与项目根绑定)
更新时间:`2026-08-11`资源卡预览、分区布局与依赖聚类,图片素材无限画布及全类型非破坏性资源编辑收口;现有 Godot 项目入口与项目根绑定)
## 1. 产品定位
@@ -13,7 +13,7 @@
3. 右侧 Project Supervisor 对话与确认区。
4. 底部专业 Agent 状态栏。
中央主视窗在“资源管理”和“运行测试”之间切换。正式预览始终在客户端当前窗口内展开,只允许载入当前项目启动的 `127.0.0.1:<port>` 本地 HTTP 预览,不调用系统外部浏览器。
中央主视窗明确区分“资源总览画布”“素材创作无限画布”和“运行测试”三个状态。资源总览与素材创作不是两套项目页:前者负责 manifest 资源投影、依赖关系和类型布局,后者负责单个图片素材的无限画布创作;两者共用当前工作台中央区域。正式预览始终在客户端当前窗口内展开,只允许载入当前项目启动的 `127.0.0.1:<port>` 本地 HTTP 预览,不调用系统外部浏览器。
## 2. 创作工具平台接入声明
@@ -50,6 +50,22 @@
- 资源分区由资源投影固定,前端交互不能改变分类。
- 资源卡手动拖动、拖动持久化、拖动性能与冲突后的重新拖动提示全部暂缓,不作为当前产品合同或验收条件。
#### 3.3.1 资源管理三个串行阶段
1. 阶段一“资源卡本体化”:`dependency / type` 两种布局共用同一个资源卡组件和预览调度器。图片与安全 SVG 直接显示主体并保留透明棋盘底;视频显示首个可解码画面并可在卡内播放;音频使用清晰的音频视觉和独立播放键;文档显示安全纯文本摘要或稳定类型占位;项目版本不伪造媒体,只显示稳定版本视觉和必要的父子 / 绑定状态。中央详情以元数据、Rust 权威依赖关系和版本信息为主,不重复放大图片、SVG 或视频;文档正文和按用户意图读取的音频控件位于元数据之后。
2. 阶段二“分区独立缩放”(已完成)同时包含互不替代的分区可视高度和内容倍率。文档、项目版本、美术资源、音乐音效资源的标题栏分别保留高度减小 / 恢复 / 增大,并新增 `50%..200%` 内容缩放减小 / 百分比复位 / 增大;触摸板捏合、`Ctrl/Cmd + wheel` 与按钮使用同一倍率模型,普通双指滚动仍只滚动。高度与倍率都以 `projectId + layoutMode + category` 隔离并只保留在当前会话内存;倍率以分区 viewport 中的手势中心为锚点,逻辑 `x / y``180×128` 本体尺寸和媒体比例不变。最小高度固定容纳标题操作与至少一排卡片,最大高度以中央资源画布当前可用高度为上限。分区保持正常文档流,后续分区由外层资源画布滚动访问,单区内容在缩放后仍只在该区内部滚动。两类表现态都不修改 manifest、`game-creator-resource-layout.v1`、布局 CAS 或资源身份,不重建媒体控件、不抢走焦点,也不恢复卡片拖动。
3. 阶段三“依赖布局聚类”(已完成):仅在 `dependency` 模式中,按固定资源分区对当前 Rust 只读图返回的同类型精确引用边和同类型聚合任务流做弱连通分组;任务流以流节点连接其成员,绝不展开成资源笛卡尔积,但只作为布局邻近提示,不绘制虚线、不进入画布关系说明。`dependencyDepth` 是唯一横向层级权威;每个相关簇先按“最小深度 -> 最小稳定资源 ID”排序,再在同层进行固定两轮由左至右、由右至左的中位数扫描,平局回退稳定资源 ID;同 SCC/环成员保持连续,环后资源继续按 Rust 深度推进。孤立资源是稳定单例组,紧凑排在相关簇之后,簇之间保持固定留白。跨分类 read model 关系不改变 Rust 业务真相,但不得进入前端布局邻接、关系说明或 SVG;搜索只隐藏卡片和橙色精确引用边,不重算组或压缩坐标。历史 `manuallyPlaced=true` 坐标继续原样保留并参与自动坐标避让。聚类是可派生表现,不新增 manifest 字段、sidecar schema、SpacetimeDB 表或第二套依赖真相。
#### 3.3.2 本体化资源卡交互与性能合同
- 卡片默认可视区域不再显示文件名、资源名称、来源、路径、任务和媒体类型等详细文本。这些字段继续进入搜索索引和中央详情;卡片“打开详情”入口的可访问名称必须包含稳定可辨识的资源名与类别。
- 卡片外层是非交互容器;“打开详情”与“播放 / 暂停”必须是可分别键盘聚焦的同级按钮,禁止在 `<button>` 中嵌套 `<button>`。播放键不选择资源、不打开详情。
- 同一时间最多播放一个卡片音频或视频。打开详情、切换布局模式、切换项目、进入运行视图、搜索 / 筛选隐藏当前资源、当前资源被删除或资源身份 / 路径变化时,必须暂停旧媒体并收口待播放意图;卡片卸载还必须执行防御性暂停。同资源 ID 的无关 manifest 更新不得重建控件或抢走焦点。
- 图片、视频首帧和项目文档只在卡片进入资源画布可见区及小幅预取边界后读取;音频只在用户点击播放后读取。前端逻辑调度器必须使用有界并发、同身份去重、有界 LRU 淘汰,不允许按全部投影同步启动读取。`play > detail > visible` 是固定调度优先级;可见性预取不得占满全部队列容量或静默丢弃播放 / 详情请求,主动请求进入硬上限队列时必须替换最低优先级的排队预取并优先执行。终态预览缓存同时受 `48` 项和 `64 MiB` 总载荷双重上限约束,媒体按解码后的 Blob 字节、文档按 UTF-8 字节计入;Tauri 返回的 data URL 只允许作为 IPC 临时载体,进入 React 状态前必须转换为可撤销的 Blob URL。LRU 淘汰、资源删除、项目 / mode 切换和卸载都必须 `revokeObjectURL`,不能让 base64 字符串或失效 Blob 长期占用 WebView。
- 卡片读取继续调用 `read_local_project_image_preview``read_local_project_media_preview``read_local_project_text_preview`;项目边界、manifest / 任务登记、`file.read` 策略、普通文件 / 链接、签名、尺寸和读取漂移门禁不变。每个 Hook 挂载及每次 `projectPath + projectId + mode` 变化必须生成不复用的 `scopeId`,每个实际读取生成唯一 `requestId`;每个队列任务和结果继续绑定完整的 `projectPath + projectId + mode + resourceId + category + path + mediaType`。前端 scope epoch 只隔离逻辑队列、缓存和异步回调;三个读取命令在 Tauri 进程中共用唯一的全局 `3` 槽物理读取管理器,不能因项目、mode、Hook 或窗口不同获得额外物理槽。scope 切换或卸载必须调用窄职责 `cancel_local_project_resource_preview_scope`,使等待 permit 和分块读取中的旧任务协作取消;旧 epoch 的 `then / catch / finally` 仍不得写入、释放或扣减新 scope 的前端状态,`A → B → A` 也不得复用第一轮 A 的 scope、请求或迟到结果。
- 原生读取必须在命令入口、权限 / 登记复核后、打开文件后、每个固定上限读取块之间、签名 / 图片结构校验前、漂移复核前和 base64 编码前检查取消。取消的排队任务不得打开文件,取消的在途任务不得生成 data URL 或 Blob URL;全局 permit 只能在对应原生任务结束后释放。重复 `requestId` 失败关闭,取消未知或已完成 scope 幂等成功;成功、失败和取消均必须清理活动 request / scope registry。为防止近期 request ID 重放和已取消 scope 复活,原生管理器继续保留有界 tombstoneseen request 最多 `8192` 项;非活动 cancelled scope 的保留预算为 `1024` 项,仍有请求的已取消 scope 必须临时钉住,最后一个请求结束后立即重新淘汰到预算内。明确取消只静默收口旧 scope,当前 scope 的真实 transient / permanent 失败语义不变。
- 安全读取失败、图片解码失败或视频无可解码画面时,卡片展示稳定类型占位,不挂载破图,不降级为项目外 URL 或裸路径读取。滚动可见性不得自动重试已失败预取;读取漂移、文件替换和通用暂时失败标记为 transient,用户再次打开详情或点击播放时可以显式重试。超尺寸、损坏、类型不支持和不安全 SVG 等永久失败继续缓存且不得用“关闭后重试”误导用户。
### 3.4 数值微调
- 数值修改立即写入当前项目的编辑态配置。
@@ -83,7 +99,20 @@
- 未开放选项使用“视觉不可用但可点击说明原因”,不使用无法触发说明的原生 `disabled`
- 普通用户暂不开放 Agent.md 编辑和自定义 Skill 安装;后续必须先定义来源审核、版本、权限、沙箱和回滚合同。
### 3.7 现有 Godot 项目
### 3.7 主站 UI 对齐与共享视觉边界
实现状态(2026-08-10):主站与 Tauri 已完成同源 chrome 接入。Tauri 现有中央素材画布直接消费共享动作按钮、工具栏、工具组和分隔符;工作台外围继续保留四区结构,并以平台 token 统一中央壳、Supervisor、Agent Dock、状态提示和主要操作。当前普通用户入口禁用“新增资源”,现有资源“编辑资源”按图片、SVG、视频、音频、文档/代码、Agent 回执和项目版本分流,所有结果均以新 asset 或子版本保存。生成、保存、登录、计费、草稿、manifest、Runtime 和审批语义不因入口分流而改变。
- 项目工作台继续保留左侧平台导航、中央主视窗、右侧 Project Supervisor 和底部专业 Agent 状态栏四区结构;主站图片编辑器只作为视觉语言和共享画布组件的事实源,不把其素材库侧栏、账号业务或云端项目外壳整体搬入客户端。
- 平台主题事实源固定为 `packages/shared/src/theme.css`。画布通用 chrome 固定落在 `@genarrative/image-canvas-react`,主站与 Tauri 必须直接 import 同一组件和作用域样式;客户端不得复制 `src/components/image-editor/`,也不得导入主站完整 `src/index.css`
- 第一批共享 chrome 固定覆盖画布动作按钮、工具栏、工具分组和分隔符。按钮的默认、悬停、键盘焦点、选中、禁用和主次色语义由共享层表达;宿主只提供图标、文案、事件与业务禁用条件。
- 主站账号、钱包、服务端项目、云端素材库和生成面板仍留在网站宿主;客户端本地项目、manifest、Runtime、审批、草稿、生成与正式提交仍留在 Tauri 宿主。共享视觉组件不得读取这些业务事实。
- 客户端 UI 对齐采用“同视觉、同组件、保留四区布局”,不复制主站整页布局。资源总览、运行视图和 Supervisor 对话继续是 Game Agent 工作台独有语义。
- 客户端正式产品仍只按 `1280×800` 横屏合同交付;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。
- 普通用户界面不默认展示内部错误码或本机绝对路径;External Editor 配置只进入独立“运行时配置”对话框,不与素材画布主要动作并列,确需诊断的信息进入受控详情或开发模式。
- 素材画布的“素材名称”是用户可编辑的正式输出名称;“资源用途”是 manifest subtype,不向普通用户开放自由文本。新增资源默认“普通游戏美术”,可从普通游戏美术、统一视觉规范、游戏界面原型、核心美术图集四项中选择;精修资源继承源用途且不可改。导出格式继续限定 PNG/JPEG/WebP。工具动作与保存设置分层展示,“保存到项目”在 `1280×800` 和窄容器中都必须完整可见。
### 3.8 现有 Godot 项目
- 首页和项目组复用同一个“打开 Godot 项目”动作,用户选择的目录必须包含普通文件 `project.godot`
- 该目录直接成为当前项目根;后续文件读取、修改、命令 cwd、对话、Runtime 和最近项目记录都绑定这个根目录,不复制工程,也不建立 `game/``assets/``memory/``exports/` 平行目录。
@@ -95,20 +124,27 @@
### 4.1 主视窗
```text
resources
resource-overview
-> asset-canvas.create(当前临时禁用,不向普通用户开放)
-> asset-canvas.refine(静态图片“编辑资源”)
-> resource-editor.deriveSVG、视频、音频、文档/代码、Agent 回执“编辑资源”)
-> resource-editor.version-branch(项目版本“编辑资源”)
-> 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 测试切片
@@ -131,14 +167,17 @@ idle -> focused(document|art|audio|version) -> idle
```
- 文档:合法 Agent 文本回执直接使用对话投影内容;项目文件只允许读取当前 manifest 已登记资产或已完成任务产物中的 Markdown、文本、JSON、YAML、TOML,必须经过 `file.read` auto 权限、相对路径、项目边界、普通文件、符号链接 / 硬链接、读取漂移、2 MiB、UTF-8 与扩展名白名单校验。正文使用不执行 HTML、不加载远程图片、不产生可点击外链的安全 Markdown 渲染,并在中央画布内独立滚动;读取失败显示错误空态。
- 美术:PNG、JPEG、WEBP 继续使用图片魔数与像素边界预览;GIF、SVG、AVIF、BMP、MP4、WebM、MOV 通过新增受控媒体读取链路按文件签名校验后在中央画布放大聚焦。SVG 额外拒绝脚本、事件处理器、外部资源引用和实体声明;视频使用内置播放控件。读取失败显示错误空态
- 美术:PNG、JPEG、WEBPGIF、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 状态栏保持原位;聚焦容器只包含标题、资源主体、必要元数据与右上角收起按钮,不使用页面级浮层或可拖动标题栏
- 音频:只读取 manifest 已登记音频或已成功导入且登记到 manifest 的附件,按文件签名接受 MP3、WAV、OGG / Opus、M4A、AAC、FLAC;聚焦态展示实际格式、浏览器解码后的时长以及带播放进度和暂停能力的内置播放器。音频任务声明中的未登记路径继续不得读取或播放
- 版本:只展示 manifest 中正式、不可变的迭代版本记录;版本卡展示项目修订、创建原因与父版本,聚焦态同时展示直接子版本和资源绑定。点击版本卡后高亮仍存在于当前资源投影中的引用资源;缺失历史资源只保留绑定身份,不生成幽灵资源卡。资源编辑只允许追加继承源绑定并记录提示词的子版本,不允许原地替换或修改源版本。
- 资源聚焦不提供通用工具栏或工具侧边栏;图片聚焦态允许一个明确的“精修资源”业务动作进入素材创作无限画布,该动作不是在聚焦容器中内嵌编辑器或恢复通用工具栏。
- 点击资源后,中央主视窗从 `resource-overview.list` 切换为 `resource-overview.focused.document / art / audio / version`,左侧平台导航、右侧 Supervisor 对话和底部 Agent 状态栏保持原位;聚焦容器以路径、类型、来源任务、依赖层级、同类型上下游和版本字段为首屏主体,不使用页面级浮层或可拖动标题栏。文档正文与按意图加载的音频控制位于元数据之后。
- 焦点转换以稳定资源 ID 为准。只有从资源列表进入详情或从一个资源 ID 切换到另一个 ID 时聚焦详情 region;同一资源 ID 因 manifest 更新而重新投影时,不得抢走详情内音频 / 视频控件、文档链接或收起按钮的当前焦点。
- 显式收起或按 Escape 后恢复进入前的搜索条件、dependency / type 布局模式、资源画布滚动位置和选中资源,并优先把键盘焦点还给原触发资源卡;这些只属于当前前端会话,不写入布局 sidecar。若资源已经被后台删除,必须清理 stale focused / selected ID、关闭详情并把焦点落到“搜索项目资源”,不得落到 `body`。项目切换和进入运行视图必须取消旧项目的焦点恢复意图。
- 阶段四只新增上述受控读取媒体展示;阶段六在同一聚焦容器内补齐正式版本只读展示和引用高亮,但不新增资源聚焦工具栏 / 工具侧边栏,不新增美术编辑、音频编辑 / 替换、资源重新生成、版本替换或运行模块。飞书原需求中“编辑并生成新资源”的条件项仍暂缓,不能只打开画板却缺少回写`referenceResourceIds` 血缘登记、新资源自动选中与邻近布局的完整闭环
- 资源管理阶段四至阶段七交付上述受控读取媒体展示正式版本展示和引用高亮`2026-08-05` 起图片编辑闭环生效,`2026-08-10` 起全类型非破坏性派生与版本子分支覆盖旧的只读限制。入口必须与草稿或 operation 恢复、正式事务提交`referenceResourceIds` 血缘、新资源即时投影、两份布局和焦点竞态一次实现,不能只打开一个没有回写的面板
### 4.4 历史成果与当前状态
@@ -152,23 +191,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-11):dependency / type 双模式通过项目内 CAS sidecar 独立持久化;dependency 模式由 Tauri Rust 只读构建关系拓扑与确定性依赖深度、前端 SVG 派生几何,图结构和线段均不写入布局 sidecar。依赖图加载完成前设布局初始化屏障,避免以临时 `dependencyDepth=0` 生成并持久化错误坐标。资源总览当前只允许自动布局与资源卡点击资源卡手动拖动继续暂缓;素材创作无限画布独立支持 viewport 与图层变换,分区高度/倍率与 sidecar 完全分离
```ts
type ProjectResourceCanvasLayout = {
@@ -251,10 +299,11 @@ type UpdateProjectResourceCanvasLayoutResult =
- 资源卡是可点击按钮,只负责选择资源并让中央主视窗进入当前唯一资源聚焦状态;不得绑定卡片级 `pointerdown / pointermove / pointerup / pointercancel` 拖动处理器。
- 指针移动不得修改卡片 `x / y`、不得产生拖动预览、不得更新依赖线几何,也不得提交手动布局 CAS。卡片 title、cursor、`touch-action` 和 class 不得暗示可拖动。
- dependency 默认布局按 `dependencyDepth` 形成横向层级同层资源纵向寻找第一个不重叠位置;type 默认布局固定按“资源子类型 -> 媒体类型 -> 名称 -> 资源 ID”稳定排序,在分区内从左到右、从上到下寻找第一个空位。布局模型的 `subtype` 必填:manifest 资产使用 `asset.kind`,任务产物、导入附件与 Agent 文本成果分别使用稳定的 `task-artifact``attachment``agent-result`,不得以缺失值或显示文案兜底;资源协调签名必须包含 subtype。卡片尺寸间距由单一前端布局模型常量维护
- type 模式资源集合变化时保留全部仍存在的坐标,只为新 ID 计算默认位置,并删除已确认失效的旧 ID。dependency 模式只永久保留 `manuallyPlaced=true`用户坐标;`manuallyPlaced=false` 属于可派生自动位置,在 Rust 关系图首次就绪或可信 producer / dependency depth 变化后按最终拓扑确定性重算。自动重算不得移动手动坐标,协调结果与持久布局逐项一致时不得产生 CAS 写入。
- dependency 默认布局按 Rust 返回的 `dependencyDepth` 形成横向层级同层仅在所属固定分区内按精确引用、聚合 task-flow 的稳定邻接、连通簇和固定两轮中位数扫描确定纵向次序,随后寻找第一个不重叠位置。task-flow 只作为聚合超边参与分组和排序,不能生成资源两两边。dependency 自动布局使用专用 `48px` 列间走线区和 `40px` 行间走线区;相关簇以本簇最大层行数为高度,资源较少的层在该高度中居中,避免菱形 / 分叉关系一侧极短、另一侧过长。type 默认布局继续使用原有 `16px` 行列间距,并固定按“资源子类型 -> 媒体类型 -> 名称 -> 资源 ID”稳定排序。布局模型的 `subtype` 必填:manifest 资产使用 `asset.kind`,任务产物、导入附件与 Agent 文本成果分别使用稳定的 `task-artifact``attachment``agent-result`,不得以缺失值或显示文案兜底;资源协调签名必须包含 subtype。卡片尺寸间距`0..=1_000_000` 坐标上限由前后端同名合同维护。超深依赖仍保留原始 `dependencyDepth` 业务真相,但显示坐标在上限列确定性饱和并纵向避让;自动布局在 IPC 前必须保证全部 `x / y` 合法,不能向 Tauri 永久重放必然失败的坐标
- type 模式资源集合变化时保留全部仍存在的坐标,只为新 ID 计算默认位置,并删除已确认失效的旧 ID。dependency 模式只永久保留 `manuallyPlaced=true`历史坐标;`manuallyPlaced=false` 属于可派生自动位置,在 Rust 关系图首次就绪`dependencyDepth` 或资源拓扑身份签名(精确引用端点和聚合 task-flow 成员)变化后按最终拓扑确定性重算。签名以稳定资源 ID 的规范端点 / 成员序列生成固定大小摘要,不使用显示名称或浏览器测量值;自动重算不得移动手动坐标,协调结果与持久布局逐项一致时不得产生 CAS 写入。
- 搜索或筛选只隐藏卡片,不删除、压缩或重排其坐标;清空搜索后恢复原位置。
- 窗口尺寸变化只改变可视范围和分区滚动边界,不回写裁切或缩放持久坐标。当前客户端继续以 `1280×800` 横屏合同验收。
- 窗口尺寸变化只改变可视范围和分区滚动边界,不回写裁切持久坐标。当前客户端继续以 `1280×800` 横屏合同验收。
- 分区高度和内容倍率都不属于逻辑布局几何。资源卡始终使用原 `x / y``180×128` 逻辑尺寸;内容 plane 只在显示层按 `50%..200%` 变换,并用同比例 frame 形成真实滚动范围。标题栏后的分区 viewport 使用内部滚动暴露缩放后的卡片,整个分区继续参与外层正常文档流。触摸板捏合、`Ctrl/Cmd + wheel` 与缩放按钮共享倍率状态,普通 wheel 不缩放;`Ctrl/Cmd + wheel` 必须由可取消的原生 `{ passive: false }` 监听处理并真实取消 WebView 默认缩放。搜索、项目或 mode 切换均不得把倍率写回布局。分区内部滚动按 `projectId + mode + category` 隔离,外层资源画布按 `projectId + mode` 隔离,二者在详情开关、模式与项目切换后分别恢复。
- 打开项目、切换 mode 或当前 mode 首次出现新资源时执行“读取 -> 协调 -> 必要时 CAS 写入”;dependency 模式必须先等待与当前 `projectPath + projectId + resource inputs` 匹配的 Rust 图进入 `ready``failed` 终态,等待期间不得创建 fallback、读取 sidecar、协调资源或入队保存。`failed` 只允许以空图降级初始化一次。项目或 mode 已切换后返回的旧异步结果必须丢弃。
- 同一 `projectPath + projectId + mode` 的首次读取与资源集合协调必须分开:资源集合变化不得取消已经发出的读取或保存。当前 scope 内资源自动协调写入使用单写者 FIFO,任一时刻最多一个 CAS 在途,后一笔必须使用前一笔成功返回的 revision。切换项目或 mode 后,旧 scope 的在途请求不能阻塞新 scope 队列;前端放弃旧请求槽位并丢弃其迟到响应,后端继续依靠 `expectedProjectId + expectedRevision + 系统锁` 仲裁已发出的请求。
- 自动协调 CAS 冲突时直接载入返回的最新布局;仍需协调时可以基于权威 revision 最多追加 `2` 次重试,持续跨窗口写入时不得无限自旋。当前提示只说明“布局已在其他窗口更新”,不得要求用户重新拖动。
@@ -271,21 +320,22 @@ type UpdateProjectResourceCanvasLayoutResult =
- 阶段五实现状态(2026-08-03):dependency 自动排列同时消费任务 DAG 与精确资源引用。Rust 把可信 producer 的任务深度作为资源深度下限,再对 `asset-reference` 图做迭代式 SCC 压缩与确定性层级传播;被引用资源位于引用资源之前,同一引用环共享稳定深度,环后资源继续递增,没有引用关系的资源保持默认不重叠位置。布局深度通过独立 `dependencyDepths` 返回,不能把 producer assignment 冒充全部资源的布局结果。
- `producerMappingTruncated=true` 只关闭依赖有界 Agent DB 审计的 `producerAssignments``taskFlows``cyclicTaskIds`。Rust 返回的 `dependencyDepths` 仍是 manifest / 精确引用 read model 的权威结果,前端必须过滤未知资源、负数、非整数和非安全整数后继续消费;不得因 producer 截断清空全部深度,也不得在前端重算替代深度。`referenceEdges`、connection index 中的 reference 关系、`cyclicResourceIds` 和 unresolved reference 继续保持可信。
- 图层只在 dependency 模式挂载;type 模式不得渲染 SVG、连线或 marker。切换 mode、切换项目或卸载工作台时必须销毁旧图层,并清理尺寸观察和窗口事件监听。
- 输入固定为当前资源投影的全部卡片身份 / 坐标与 Tauri Rust 返回的 `ProjectResourceGraph` 只读 DTO;Rust 负责资源过滤、去重、迭代式环检测、SCC 压缩后的确定性依赖深度、任务流聚合和一跳连接索引,前端只负责 DTO 防御归一化、浏览器几何与原生 SVG path / marker。SVG 叠加在资源卡底层并设置 `pointer-events: none`,不得引入 D3、React Flow 等图表库,也不得阻断卡片点击。
- `asset-reference` 表示精确资源引用,使用明亮橙色实线与连续贝塞尔曲线`GameCreationAppAssetManifestEntry.source.referenceResourceIds` 中的外部资源 ID 必须先唯一匹配另一项资产的 `source.resourceId`,再映射为当前资源卡 ID;缺失、重复或已删除的目标均不得渲染幽灵连线。
- `task-flow` 表示同一资源类型内的任务产物流转,使用灰色圆头虚线;文档、项目版本、美术、音频之间不得绘制跨分区虚线任务依赖`sourceTaskId -> targetTaskId + section` 分区聚合为一条主线,两端只保留同分区资源并绘制平滑曲线分支,不得出现直角折线;禁止对上下游资源生成笛卡尔积连线。任务主线与分支可以使用不同线宽和透明度表达聚合层级,但不能改变端点或方向语义
- 输入固定为当前资源投影的全部卡片身份 / 坐标与 Tauri Rust 返回的 `ProjectResourceGraph` 只读 DTO;Rust 负责资源过滤、去重、迭代式环检测、SCC 压缩后的确定性依赖深度、任务流聚合和一跳连接索引,前端只负责 DTO 防御归一化、逻辑 viewport 裁剪与原生 SVG path / marker。每个固定分区在自己的 `.game-resource-plane` 内持有一个 SVG,SVG 与卡片直接消费同一逻辑坐标、卡片尺寸、CSS scale 和 viewport scroll;端点主路径不得再通过 `getBoundingClientRect` 拼接全局屏幕坐标。SVG 位于资源卡底层并设置 `pointer-events: none`,不得引入 D3、React Flow 等图表库,也不得阻断卡片点击。
- `asset-reference` 表示精确资源引用;只有两端属于同一固定资源分类时才进入前端布局邻接、无障碍关系说明和 SVG,使用明亮橙色实线与实心箭头。跨分类引用仍可存在于 Rust read model,但资源管理画布不得绘线、不得以它形成布局簇或边界偏置`GameCreationAppAssetManifestEntry.source.referenceResourceIds` 中的外部资源 ID 必须先唯一匹配另一项资产的 `source.resourceId`,再映射为当前资源卡 ID;缺失、重复或已删除的目标均不得渲染幽灵连线。
- `task-flow` 表示同一资源类型内的任务产物流转,但资源画布不再绘制灰色虚线任务流 marker,也不把它加入 `aria-describedby` 关系说明。它只`sourceTaskId -> targetTaskId + section` 作为聚合超边参与 dependency 连通簇和中位数排序,禁止对上下游资源生成笛卡尔积邻接;详情仍可展示 Rust read model 已证明的任务信息
- 画布资产 producer 只能来自 `agent.runtime.canvas.asset_generate``assetId -> agentId` 审计且 `agentId` 必须存在于当前 manifestExternal Editor `source.taskId` 属于平台生成任务命名空间,禁止当作 manifest task ID。证据缺失、冲突或有界审计读取未覆盖时不生成对应 task flow,不猜测归属。
- 图模型必须对资源引用图和完整任务依赖图做迭代式环检测,不得用无界递归遍历;参与环的可见边保留渲染并标记 cyclic,环本身不能造成重复生成或死循环。
- 资源自引用的起点与终点为同一张卡片时,必须绘制在卡片外侧的可见闭环并保留箭头,不得让路径穿过卡片后被底层 SVG 层级遮挡。
- 搜索只允许为当前可见端点生成几何;任一精确引用端点隐藏时该线隐藏,聚合任务流只保留仍可见的两端分支,任一侧没有可见资源时整条任务流隐藏
- 资源点击只进入中央聚焦并保留当前选中卡片,不改变依赖卡片或连线的颜色、线宽与透明度;关系线始终直接展示,不提供点击后的上下游高亮或无关线弱化。
- 搜索只允许为当前可见端点生成几何;任一精确引用端点隐藏时该线隐藏。task-flow 不产生显示几何,搜索也不因此重排布局
- 同类型精确引用按稳定边 ID 和对端次序为同一卡片同侧的多条边分配独立端口;横向层级可用时优先左右连接,同列或横向间隙不足时才上下连接。同轴端点直接用直线,需转向时使用正交线段与最大 `10px` 的小圆角,不使用大范围贝塞尔控制柄。端口顺序不使用显示名称、随机数或浏览器枚举顺序,相同输入必须产生相同路径。资源点击只进入中央聚焦并保留当前选中卡片,不改变依赖卡片或连线的颜色、线宽与透明度;关系线始终直接展示,不提供点击后的上下游高亮或无关线弱化。
- 资源卡 Pointer Move 不改变基础 positions 或 SVG 几何。连线只随布局读取、资源自动协调、搜索、项目切换或 section origin 变化而更新。
- `ResizeObserver` 在单个图层生命周期只允许构造一次。dependency section 额外提供至少 `64px` 右侧视觉 gutter,确保最右侧自环和箭头可完整滚动显示,但不得修改卡片坐标或布局 sidecar。
- 阶段五不改变手动位置边界:已有 `manuallyPlaced=true` 坐标原样保留,资源引用新增或变化只允许重新派生 `manuallyPlaced=false` 的自动坐标;任务流继续按任务对与资源分区聚合,禁止为了计算深度或绘线生成资源笛卡尔积
- 每个已挂载 dependency 分区最多构造一个 `ResizeObserver`,四区总数最多四个;observer 只维护该分区的逻辑 viewport,不测量或重建卡片屏幕端点。dependency section 额外提供至少 `64px` 右侧视觉 gutter,确保最右侧自环和箭头可完整滚动显示,但不得修改卡片坐标或布局 sidecar。
- 分区高度、分区内部滚动和窗口 resize 在各分区内通过单一 `requestAnimationFrame` 调度器合并逻辑 viewport 更新,不得在每个 scroll event 中同步重算。浏览器以分区 viewport 原生裁剪同属该 plane 的 SVG 和卡片,因此线段不能穿过标题栏或泄漏到其它分区;一端可见时绘制入向 / 出向边界继续线,两端都离屏时不渲染。项目 / mode 切换或卸载时必须清理对应 observer、scroll 与 window resize 监听
- 阶段五不改变手动位置边界:已有 `manuallyPlaced=true` 坐标原样保留,资源引用新增或变化只允许重新派生 `manuallyPlaced=false` 的自动坐标;任务流继续按任务对与资源分区聚合,禁止为了布局分组生成资源笛卡尔积,且不进入 SVG。
### 5.3 资源类型与替换兼容性(P1)
实现状态(2026-08-03):当前资源投影已收口到固定的“文档 -> 项目版本 -> 美术资源 -> 音乐音效资源”四区。文档接收 Markdown / 文本 / JSON / YAML 等正式项目文档和合法 Agent 文本回执;项目版本只接收显式 `ProjectVersionResourceSummary` read model,未知任务产物不得兜底为版本;美术接收图片、SVG、动画和视频类产物;音频接收 manifest 已登记音频资产或已成功导入并登记到 manifest 的音频附件,任务声明中的未登记音频路径不冒充正式音频资源。无法识别的二进制任务产物和附件不进入资源画布。阶段四已为本地文档、安全 SVG / 扩展图片 / 视频和音频补齐受控读取、中央聚焦、失败空态与媒体播放;这些都是只读表现层,不改变资源投影或 manifest 真相
实现状态(2026-08-10):当前资源投影已收口到固定的“文档 -> 项目版本 -> 美术资源 -> 音乐音效资源”四区。文档接收受支持的 UTF-8 文档/代码和合法 Agent 文本回执;项目版本只接收显式 `ProjectVersionResourceSummary` read model,未知任务产物不得兜底为版本;美术接收图片、SVG、动画和视频类产物;音频接收 manifest 资产、上传登记资产和已完成任务 `artifacts` 明确声明的音频产物。无法识别的二进制任务产物和附件不进入资源画布。受控读取、中央聚焦、失败空态与媒体播放不改变 manifest 真相;编辑成功后只追加新的 asset 或版本子记录
资源身份固定使用 manifest asset ID、正式 version ID、Agent ID + run ID 或已导入资源稳定路径;显示标题、来源文案变化不得改变 `resourceId`,从而避免布局、依赖边、选择和聚焦状态因改名失效。
@@ -394,14 +444,15 @@ type ProjectAgentMudPointAttribution = {
- 当前 run 专业状态与项目历史成果分离。
- 默认三专业组,并可展开另外三组。
- 严格审批有效;风险/无需审批可点击查看未开放原因。
- 橙色低保真视觉`1280×800` 横屏边界。
- 与主站图片编辑器一致的平台轻色主题、共享画布 chrome `1280×800` 横屏边界。
### P1
- 已实施依赖/类型两套坐标持久化、首次默认不重叠布局、历史坐标跨重启恢复与自动协调 CAS 冲突处理;资源卡手动拖动暂缓。
- 资源关系线在布局持久化验收通过后单独实施,不与本切片捆绑伪造完成。
- 已实施正式版本只读模型、版本卡、父子关系与引用资源高亮;资源兼容性判断和不可变下一迭代版本创建仍待后续切片。
- 美术/音频编辑状态接线
- 已实施正式版本不可变模型、版本卡、父子关系与引用资源高亮;“编辑资源”可追加继承源绑定并记录提示词的子版本,资源直接替换、运行版本切换和兼容性迁移仍待后续切片。
- 素材创作无限画布阶段一按权威专题一次交付图片导入、编辑、生成、导出、草稿恢复、正式本地回写、即时投影和焦点竞态闭环
- 高级抠图、图集、角色动画、视频时间线编辑和音频波形级编辑按后续切片实施;当前视频走源引用派生,音频只做语义重制。
### P2
@@ -439,37 +490,81 @@ type ProjectAgentMudPointAttribution = {
### 7.3 P1 资源依赖关系图验收
1. dependency 模式显示对画布背景至少 `3:1` 对比度的橙色实线资源引用,并只在同一资源类型分区内显示灰色虚线任务流;跨类型不显示虚线,type 模式没有图层或连线。
1. dependency 模式显示同一资源类型分区内的精确引用,使用对画布背景至少 `3:1` 对比度的橙色实线箭头;聚合任务流只参与布局,不绘制灰色虚线或 marker。type 模式没有图层或连线。
2. 精确引用只接受唯一有效的外部资源 ID 映射,删除或不存在的资源不产生幽灵连线。
3. 多资源任务依赖按资源类型分区后,各分区只形成一条聚合主线与 `O(S+T)` 条端点分支,不产生 `S×T` 连线或跨分区虚线。
3. 多资源任务依赖按资源类型分区后只形成布局超边,以 `O(S+T)` 成员关系参与聚类,不产生 `S×T` 邻接、SVG 主线、端点分支或跨分区虚线。
4. 资源引用环和无资源产物参与的任务环都可被有限遍历识别,界面不死循环。
5. 搜索触发端点过滤;资源点击不改变上下游卡片或任何连线的视觉状态,资源卡指针移动不更新线段,点击与中央聚焦行为不回归。
6. 切换布局模式或项目后旧 SVG、ResizeObserver 与窗口监听全部清理;图层从不写入 layout sidecar、manifest 或其它持久化。
7. 4096 资源链式 fixture 继续验证拓扑、聚合复杂度和自动布局性能;拖动局部更新与真实 Chromium 拖动帧预算暂缓,不作为当前验收条件。最右侧自环与箭头仍需完整显示。
8. Rust 图读取延迟时,dependency sidecar 在图进入 `ready / failed` 前没有读取或写入;首次布局直接使用 Rust 返回的最终 producer 与 dependency depth。Agent DB 有界读取截断时 producer、task flow 与 `cyclicTaskIds` 失败关闭,精确 manifest 引用及 Rust 返回的合法 `dependencyDepths` 继续到达布局层。重新打开包含深度 `0 / 1 / 2` 自动坐标的旧布局时不得降成全 `0` 或持久化扁平布局;手动位置逐项不变,自动位置按最终拓扑协调且相同结果不增加 revision。
9. 依赖 SVG 作为装饰层不可聚焦并对辅助技术隐藏;画布通过关联的视觉隐藏文本逐条说明当前可见资源引用和任务流,搜索过滤或模式切换后文本与可见关系同步变化。
9. 依赖 SVG 作为装饰层不可聚焦并对辅助技术隐藏;画布通过关联的视觉隐藏文本逐条说明当前可见的橙色精确引用,搜索过滤或模式切换后文本与可见关系同步变化。task-flow 不进入该关系说明。
10. 精确引用使用橙色实线和可见箭头;dependency 自动卡片之间保留横向 `48px`、纵向 `40px` 走线区,相关簇的窄层按本簇最大层高度居中。不同横向层级且卡片间有空间时使用左右端口优先的横线,同列、同深度或横向空间不足时才使用上下端口纵向降级。同行 / 同列直接连线,需要转向时只使用正交线段与最大 `10px` 小圆角。同一卡片同侧的多条精确引用按稳定边 ID 与对端位置分配不同端口;自引用继续在卡片右侧外绕,箭头端点与卡片保留固定显示间隙。
11. 每个分区 SVG 必须位于拥有相应资源卡的 `.game-resource-plane` 内,以同一逻辑坐标随原生滚动和 CSS scale 同步移动;滚动 / 缩放不得通过异步 DOM 屏幕测量重新绑定端点。分区 viewport 原生裁剪本区 SVG,不使用四区 viewport 并集裁剪;一端离屏时只显示对应方向、带同语义箭头的边界继续线,不保留指向已裁剪卡片的悬空主路径,两端离屏时隐藏。搜索隐藏精确引用任一端点时整边隐藏。
### 7.4 P1 正式项目版本阶段六验收
1. manifest 缺少 `versions` 时旧项目正常打开且不显示伪造版本;存在合法记录时,固定“项目版本”分区按追加顺序显示稳定版本卡。
2. 根版本、父版本和直接子版本关系在卡片或聚焦态可见;悬空父版本、自引用、重复 ID、非递增修订、倒退时间、重复 slot 和超限数字均失败关闭。
3. 点击版本卡后,当前 manifest 中仍存在的绑定资产卡被高亮;历史已删除资产只在版本详情保留 ID,不创建幽灵卡,也不把 External Editor resource ID 猜成 manifest asset ID。
4. 版本聚焦态只读展示身份、修订、创建原因、父子关系、创建时间和 slot 绑定,不提供编辑、替换、切换、回滚或运行按钮。
4. 版本聚焦态展示身份、修订、创建原因、父子关系、创建时间和 slot 绑定;“编辑资源”只追加继承源绑定并记录提示词的子版本,不提供原地替换、切换、回滚或运行按钮。
5. 任意现有 manifest 写入只能保留磁盘版本前缀并追加新记录;存储边界以跨进程专用锁串行覆盖旧状态读取、前缀校验、安装和回读,修改、删除、重排或并发旧快照覆盖已有版本时写入失败。
6. 版本选择和高亮不写 manifest、布局 sidecar 或 project revisiondependency / type 两种布局都可显示绑定高亮,既有依赖关系 SVG 语义不变。
### 7.5 阶段七完整验收
### 7.5 资源分区独立高度验收
1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核阶段零至阶段六;美术编辑生成新资源继续按本 PRD 已确认的闭环条件暂缓,不作为遗漏或伪完成
2. AppSurface 同时覆盖文档、图片、SVG、音频和视频聚焦;视频必须使用原生 `controls``preload="metadata"`,读取策略失败时中央主视窗显示安全空态,右侧对话和底部 Agent 状态栏继续存在
3. `1280×800` 应用内浏览器实测 `window`、document 与 body 均无页面级横向或纵向溢出。浏览器开发页受真实登录门禁保护,不为验收绕过认证或伪造 Tauri;工作台内部结构由 AppSurface 集成测试与资源布局 CSS 合同测试复核
1. 四个分区均可独立缩小、放大和恢复默认高度;任一分区变化时其它分区高度、资源卡尺寸和媒体比例不变。到达上下限时对应按钮同时具备正确的 disabled 视觉与辅助技术语义
2. 高度、倍率和分区内部滚动按 `projectId + dependency|type + document|version|art|audio` 隔离,外层资源画布位置按 `projectId + dependency|type` 隔离;切换模式、切换项目和打开 / 关闭资源详情后,分别恢复当前会话中的内外滚动位置,且键盘焦点不被无关重置
3. 最小高度可操作标题控制并完整容纳至少一排卡片;最大高度不超过中央资源画布当前可用高度。窗口变小后超界尺寸被永久夹取到新上限,后续放大窗口不自动恢复旧超界值
4. 分区放大只在正常文档流中下推后续分区,不使用浮层、绝对定位、负 margin 或 z-index 覆盖。分区内容超出时内部可滚动,外层画布仍可滚动访问其它分区,`1280×800` 无页面级溢出。
5. dependency 线在高度、分区内部滚动、外层滚动和窗口 resize 后仍与可见卡片端点对齐;单端卡片被分区 viewport 裁掉时只保留边界继续线,不保留悬空主路径,两端都被裁掉时隐藏,线段不穿过标题栏。滚动与 resize 在每个分区内使用单一 `requestAnimationFrame` 合帧,每个分区 plane 最多创建一个 `ResizeObserver` 并在卸载时清理全部监听。
6. 高度操作不调用 `update_local_project_resource_canvas_layout`,不改 manifest、不更新 sidecar revision;阶段一的图片 / 视频 / 音频 / 文档 / 版本卡片、单媒体播放与中央详情回归全部通过。
### 7.6 阶段七完整验收
1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核资源管理阶段零至阶段六及素材创作阶段;图片、SVG 和视频主体只在资源卡中展示,中央详情以元数据和依赖信息为主,文档正文与按意图读取的音频控件保留。
2. AppSurface 覆盖资源卡媒体预览、资源聚焦、素材创作无限画布、全类型非破坏性编辑、恢复 modal、右侧对话和底部 Agent 状态栏;视频使用 `controls``preload="metadata"`,读取失败显示安全空态。
3. 预览调度、布局 scope、CAS、项目切换和 `1280×800` 无页面溢出均须通过对应定向测试、真实登录门禁和编码/diff 门禁验证。
4. 根目录全量 Vitest、前后端 typecheck / lint / build、Rust workspace test / check、SpacetimeDB schema、原生壳、内容 / 编码、生产运维与部署门禁全部通过后,阶段七才允许提交。
5. 本地 `.env``.env.local`、密钥、缓存、日志和构建产物不进入阶段七提交;提交前再次执行编码检查和 `git diff --check`
### 7.6 素材创作无限画布阶段一至五最终验收
实现状态(2026-08-12):当前产品切片禁用“新增资源”,只从现有资源进入非破坏性编辑。图片进入中央 refine 画布,其他现役类型进入统一派生编辑壳;取消恢复、正式 manifest/revision 实时合并、依赖图重建、dependency/type 双布局协调和三阶段自动定位已经接通。command/event 任意顺序按项目、commit、event 与 revision 去重;低 revision、旧 graph/layout 和失效 focus generation 均不能倒灌。Tauri 远端媒体编辑固定使用 AppData 私有 `editorApi.baseUrl/apiKey` 访问 `/api/external/v1/*`,普通 Launcher、开发工作台和独立 game-chat 的“运行时配置”都可编辑这两个字段;不把 Key 传入 WebView、项目事实、账本、日志或普通错误,素材画布工作区自身不提供凭据输入。重启恢复只继续原 operation,不以新请求、新幂等键或新 operationId 替代结果未知的旧任务。
1. 网站与 Tauri 实际 import 同一份 `@genarrative/image-canvas-core``@genarrative/image-canvas-react`,客户端没有复制的主站画布目录;viewport、selection、变换、renderer 与 history 算法位于共享层,宿主只保留事件接线与 adapter 副作用。
2. “新增资源”在当前产品切片中保持禁用;“编辑资源”只接受现有资源。图片 refine 保留原资产与原文件、创建新资产,并用规范 `referenceResourceIds` 登记直接血缘;其他类型同样只追加派生文件/asset 或子版本,不覆盖、删除或重排源记录。
3. 草稿 schema、revision、容量、项目身份、OS 锁、CAS、恢复副本和媒体引用符合权威专题;损坏、未知 schema、身份错配和超限均失败关闭。应用重启后必须按 `projectId + intent + sourceAssetId + active status` 从正式 sidecar 唯一发现原 refine 草稿并保留 `draftId`、生成新 `sessionId`;零条才允许创建,多条必须进入对账,不得依赖进程内 Map 或按时间猜测。
4. 正式提交携带 `expectedProjectId + expectedRevision + expectedDraftRevision + commitId + idempotencyKey`,按事务快照/journal/ledger、文件、manifest/revision、回读、ledger/draft、事件顺序完成;首个快照、全部快照、journal 写入、文件、manifest、revision、验证和 ledger 各崩溃阶段均有确定结果,未发布快照残留只在证明正式文件、manifest 与 revision 均未变化时清理。
5. 保存成功后不刷新、不重开项目即可进入 manifest 投影、依赖图、dependency/type 布局和允许时的选中定位;切项目、切中央状态、改选择或改搜索后的迟到结果不得抢焦点。
6. 搜索/筛选隐藏新资源时保留条件,明确提示“新资源已保存,当前筛选条件下不可见”,只通过显式动作清除条件并定位。
7. 现有资源编辑、生成、保存、取消、失败和恢复必须覆盖权威专题 §13 中与当前非破坏性编辑切片对应的验收矩阵;只完成画布 UI 或只完成本地写文件都不能算正式闭环。
8. 自动定位必须分别证明资源已投影、dependency/type 两份布局都 settled 且存在目标位置、目标卡 DOM 已提交;搜索隐藏走显式清除/定位,任何 commit 最多自动聚焦一次。
9. Tauri 远端媒体编辑只使用发布 AppData 私有 External v1 配置;后端按 owner 预扣/退款泥点并返回可轮询 operation。凭据失效、余额不足、平台生成配置故障和远端失败必须在画布内可见;生成状态不得因固定高度或 `overflow` 裁剪而消失,终态后刷新钱包余额。
10. 素材画布生成账本的服务身份固定为 `service-origin-v1`,绑定规范化 External base URL 的服务指纹,不绑定 Developer API Key;确认面板只展示去除路径与凭据的服务 origin。无法用当前 Key 验证的升级前 Key-bound 账本在任何网络动作前显示当前服务 origin 并要求用户确认;确认挑战过期、账本变化或 base URL 变化均失败关闭。确认后 `accepted/running` 只恢复原 GET`prepared` 只可精确重放冻结的原 POST。全类型资源编辑复用相同显式确认迁移:旧 Key-bound 指纹无法验证时确认前保持零网络动作,确认后已存在 `operationId` 的任务只 GET 原 operation,尚未受理的冻结请求才可复用原幂等键精确 POST。
11. 资源编辑恢复面板必须为独立 modal,展示后端权威队列的全部 operation。用户可继续任意可恢复项;`remote-failed` 只允许显式移出活动队列,并保留私有账本审计;`reconciliation-required` 只读展示对账。读取失败必须提供重试,不得伪装空队列;操作后必须重读后端。
12. `remote-failed` 已是远端明确终态,重启后不再 POST、不再轮询、不再扣费;`archived` 仅表示用户已将它移出活动恢复队列,不等于 `committed``result-unknown`、鉴权临时失败和 `reconciliation-required` 均不允许归档或重新生成。
13. 派生资产提交必须通过 durable asset transaction journal 串起最终文件、manifest 和 project revision。任一崩溃阶段恢复后只有一份派生文件、一条 manifest assetrevision 精确推进一次;manifest 已写而 revision 未写时只前向补 revision,无法证明的组合进入人工对账。journal 已证明目标 asset、媒体和 target revision 写入后,即使后续合法提交继续推进 manifest/revision,也应按目标 asset 精确身份与 `currentRevision >= targetRevision` 补齐 ledger/draft,不得要求整个 manifest 永远等于历史 after 快照。durable committed 后遗留的 staging 只有在 staging/正式媒体摘要一致,且 manifest 中按 asset ID 或路径唯一命中并与 journal asset 精确相等时才尽力删除;删除失败不降级已提交结果,身份或媒体漂移则保留 staging 并进入对账。
14. generation progress、草稿保存队列、生成/提交回包与延迟 `loadDraft` 必须共用单调 revision 门禁,低 revision 不得覆盖已落地的新草稿。Shift 指针与键盘选择必须与共享 core 一致;单选自身 Shift 不能清空选择,已选多图层普通指针拖动应保持并同步移动选择集。零位移不得产生 undo、documentVersion 或草稿保存。
15. 失败 UI 必须按 `generation / draft-save / asset-commit / recovery / cancellation` 五类 operation 显示可访问名称与安全动作。只有生成失败可以保留参数并“返回修改/重新确认”;草稿、CAS、提交、恢复和取消故障不得出现会发起新生成的按钮。任何非 `editing` 生命周期都必须同时禁用或 inert 背景画布/顶部工具栏,并由生成 handler 再次校验当前状态;modal 遮罩、视觉 disabled 或旧闭包都不能充当业务门禁。
16. 派生子版本 journal 必须冻结 project revision before/after 身份和目标 after 记录。manifest 已有目标子版本但 journal 缺失时失败关闭;旧 journal 缺少 revision 身份且当前 revision 已推进、无法证明是同一事务写入时进入人工对账,不得把现状猜测为已提交。
17. 文本、SVG 与 Agent 回执编辑必须在调用 Provider 前持久化 request-issuedProvider 成功正文必须在解析、格式校验和 staging 之前原子写入私有 durable handoff,并绑定原 operation、请求指纹和内容摘要。issued 后缺少可信 handoff 只能对账,已有可信 handoff 则只消费原正文,两者都不得再次调用 Provider。401/403 只表示当前凭据不能继续授权,不能把已受理 operation 改写为永久对账;修正 Key 后仍只查询原 operation。
### 7.7 主站 UI 对齐验收
1. `packages/image-canvas-react` 暴露共享画布动作按钮、工具栏、工具分组和分隔符;主站图片编辑器直接消费这些组件,旧网站组件只允许保留薄适配,不得继续维护另一份按钮可访问性或选中态实现。
2. 共享样式全部使用 `.genarrative-image-canvas*` 作用域并消费 `--platform-*` / `--image-canvas-brand-*` tokenTauri 不导入主站完整 `src/index.css`,主站也不复制共享样式回业务 CSS。
3. 客户端接入完成后,`1280×800` 下中央画布工具栏保持单行或受控横向收纳,中文动作不得逐字换行;聊天输入、主要保存动作和底部 Agent Dock 始终可见,document/body 不产生页面级溢出。
4. 主站现有图片编辑器的按钮名称、tool selection、hover/focus/pressed/disabled 行为与工具栏位置不因共享抽取回归;网站账号、钱包、素材库、生成和导出逻辑不下沉共享层。
5. 客户端生成、保存、取消、恢复、登录失效与余额不足只更换视觉承载,不改变 Host Port 调用、幂等身份、草稿 CAS、manifest/revision、Runtime 审批或钱包刷新语义。
6. 阶段一、二冻结合同并让主站消费共享 chrome;2026-08-06 的阶段三至五已经完成客户端中央区、Supervisor 和 Agent Dock 的正式换肤、定向测试与 `1280×800` 验收。独立 Vite 没有登录会话时继续显示真实登录门禁,不为视觉测试增加绕过入口。
## 8. 非目标
- 当前收口不实现资源卡手动拖动,也不实现资源聚焦工具栏、资源聚焦工具侧边栏、美术编辑、音频编辑 / 替换、资源重新生成、资源替换、下一迭代版本创建入口、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因。正式版本记录已经成为 manifest 业务真相,但当前只读取、校验和展示已有记录
- 本切片不持久化资源聚焦状态、画布缩放 / 平移、搜索条件、筛选条件或当前 mode;聚焦退出时的列表上下文恢复只限当前前端会话,这些状态如需跨重启保存必须另行扩展合同,不能塞入 `game-creator-resource-layout.v1`
- 当前不开放“新增资源”产品入口;现有资源只允许非破坏性编辑并追加新文件、asset 或子版本,源记录不覆盖、删除或重排
- 资源总览不实现资源卡手动拖动、通用聚焦工具栏/侧边栏、资源直接替换、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因
- 素材创作不实现高级蒙版/毛发级抠图、图集、角色动画、视频时间线或音频波形级编辑;图片画布提供现役编辑闭环,其他类型使用统一非破坏性派生面板。
- 资源聚焦、搜索、筛选、dependency/type mode 与分区滚动/倍率均为当前会话态;素材创作 viewport 和图层状态使用独立草稿 schema,不能混用资源总览 sidecar。
- 不修改 SpacetimeDB schema。
- 不开放普通用户 Agent.md/Skill。
- 不自动确认 Agent 动作,不自动触发可能扣费的生成。
File diff suppressed because one or more lines are too long
@@ -611,6 +611,16 @@ npm run check:server-rs-ddd
- 页面交互 smoke
- 移动端视口检查
### 提交与 master 推送前自动门禁
仓库级 Git `pre-commit` hook 通过 `lint-staged`,只对当前已暂存的 `*.js``*.mjs``*.cjs``*.ts``*.tsx` 文件依次运行 ESLint autofix 和 Prettier,并把修复结果更新到本次提交的暂存区;ESLint wrapper 会按仓库配置过滤 ignored 文件,避免 ignored warning 与 `--max-warnings 0` 组合造成误阻塞。未暂存的其他文件不进入处理范围,修复或暂存恢复失败时提交会中止,应先处理失败原因并重新检查 staged diff,不能等 CI 再暴露 import 排序或格式问题。
部分暂存同一 JS / TS 文件时,`lint-staged` 会临时隐藏该文件未暂存的改动,以暂存快照执行修复和格式化,随后恢复未暂存内容。因此提交前后都应分别检查 `git diff --cached``git diff`,确认修复后的暂存内容属于本次提交,未暂存工作没有被误带入;若恢复产生冲突,先人工整理暂存边界再重新提交。
`Repository checks` 的唯一仓库入口是 `npm run check:repository-ci`,依次运行 `npm run lint`、生产构建、内容检查和基线到候选提交的空白差异检查。Gitea `Repository checks` job 和本地 master `pre-push` 必须共同调用该入口,禁止各自复制或删减子命令;本地 hook 还会确认待推 master SHA 等于当前 `HEAD` 且已跟踪工作树干净,无法确认时失败关闭。普通 feature 分支 push 不运行这条重门禁,进入 master 前仍以 PR required checks 为权威。
`git commit --no-verify``git push --no-verify` 都会绕过本地 hook,只允许在已明确原因的紧急场景使用;绕过不代表可以跳过等价门禁。真正阻止红提交进入 master 依赖 Gitea 分支保护:禁止日常直接 push,统一经 PR,并要求 `Repository checks``Frontend tests``Backend tests``Native shell tests` 四个当前 head context 全部成功后合并。本地 hook 只负责提前反馈,不能替代服务端分支保护。
前端原则:
- 移动端优先,再兼容网页端。
+229 -13
View File
@@ -14,6 +14,77 @@
- 关联:相关文件、文档、提交或 Issue
```
## 资源管理第二轮修复后不能继续用第一轮文档和弱测试作为验收合同
- 现象:代码已经改成分区内 SVG plane 和卡内媒体,文档仍要求单全局 Overlay 或中央大图;CSS 正则和浅层 AppSurface 测试保持绿色,但真实 Tauri WebView 仍会默认缩放、主动预览请求饥饿、过滤后媒体继续播放或超深布局反复提交非法坐标。
- 原因:第一轮编码时同步编写的 PRD / 技术方案被后续代码修复绕过,第二轮只改实现和局部测试,没有把新验证结论回写正式合同。React 合成 wheel 事件、单例滚动 ref、只按数量限制的 base64 缓存、跨 scope 共用的活动读取计数、无优先级有界队列和前后端不同坐标边界又分别跨越浏览器、会话状态与 IPC 边界,浅层文本断言无法证明运行时行为。
- 处理:每轮验证后按“当前代码 + 最新决策 + 真实运行证据”同步修订 PRD、技术方案、决策记录和回归测试。原生可取消事件要直接断言 `defaultPrevented`;队列验证主动请求替换预取;预览 data URL 只作临时传输并转为可撤销 Blob URL,LRU 同时限制项目数和总字节;项目 / mode 切换推进 epoch,旧 `finally` 不得扣减新 scope;媒体状态同时核对可见集合;滚动分别按内外 scope 保存;坐标合同由共享 TS 与 Rust 同边界维护;暂时 / 永久错误在模型中显式分类。
- 验证:运行定向 hook / AppSurface / 纯布局 / Rust 边界测试,再执行类型检查、编码检查和 `git diff --check`
- 关联:`apps/ai-game-creator-shell/src/view/project-development/index.tsx``apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts``apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts``apps/ai-game-creator-shell/src-tauri/src/project/resource_layout.rs``docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`
## 预览读取不能像放弃旧 CAS 回调一样直接重置并发槽
- 现象:A 项目的 3 个预览 IPC 仍挂起时切到 B,前端把活动计数归零并立即再发 3 个;界面不会被 A 的迟到结果污染,但原生同时保留 6 个读取。连续项目 / mode / Hook / 窗口切换会继续叠加整文件缓冲、base64 临时字符串和 WebView IPC 载荷,绕过 `64 MiB` 终态缓存预算。
- 原因:布局 CAS 已经发出后只能依靠 revision / 系统锁仲裁,前端放弃回调是正确语义;只读预览却有明确的物理内存和文件读取成本,可以协作取消。把 scope epoch 的逻辑隔离误当成底层取消,又让每个 scope 自行拥有 3 个物理槽,实际并发就不再全局有界。
- 处理:前端继续用 epoch、优先级队列、LRU 和完整资源身份隔离展示状态,但每次挂载 / scope 变化生成不复用的 `scopeId`,每次 IPC 生成唯一 `requestId`。三个安全读取命令共用 Tauri 进程级 3 permit 管理器;切换和卸载调用窄 scope 取消命令,等待 permit 与固定块读取都检查取消。permit 和 request/scope 清理 guard 必须移入真正的 `spawn_blocking` 读取闭包;WebView 卸载或调用方 abort 只会丢弃外层等待,不能让仍在运行的 blocking 读取提前释放物理槽或从取消 registry 消失。取消任务在 base64 前退出,所有终态清理活动 request / scope registry。seen request tombstone 最多保留 `8192` 项;非活动 cancelled scope tombstone 的预算为 `1024` 项,活动取消 scope 为防复活必须临时钉住并在结束后重新收敛。不得为了追求整个 registry 字面清零而删除防重放 / 防复活记录,也不得让已经结束的 scope 长期占用预算外记录。不得放宽原有项目边界、登记、权限、链接、签名、大小、漂移或安全 SVG 门禁。
- 验证:先让旧 scope 占满 3 个 permit,再连续执行 A → B → A、mode、Hook 和多窗口切换;断言原生活动峰值始终 `<= 3`,旧等待任务不打开文件,旧在途任务在最近检查点释放,新 scope 随后启动,取消任务不编码 data URL / 不创建 Blob URL,最终活动 request / scope registry 为零。阻塞读取进入后主动 abort 外层 future,必须证明旧 blocking 任务仍占 permit、仍可按 scope 取消且新请求不能提前启动。另分别证明 seen request `8192` 项的硬上限、cancelled scope 超预算时不淘汰活动记录,以及任一活动 scope 结束后非活动 tombstone 立即收敛到 `1024` 项预算。前端再断言旧 `then / catch / finally` 和取消 ACK 均不写新 scope;内部取消类别只允许精确匹配,不能因真实错误正文恰好包含该标识而静默吞错。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts``apps/ai-game-creator-shell/src-tauri/src/resource_preview_scheduler.rs``apps/ai-game-creator-shell/src-tauri/src/resource_inspect.rs``apps/ai-game-creator-shell/src-tauri/src/image_inspect.rs`
## 依赖线与资源卡不在同一 transform 层时会在滚动和缩放中分离
- 现象:静止时依赖线似乎对齐,触摸板缩放或连续滚动后线段会追赶、漂移或忽隐忽现;某一资源分区的长线还可能出现在相邻分区。
- 原因:资源卡位于各自可滚动、可缩放的 section plane,全局 SVG 却是外层兄弟节点;通过 `getBoundingClientRect`、RAF 和 React state 重建屏幕端点无法与浏览器合成层 transform 原子同步。把四个 viewport 做成一个 clipPath 并集也不具备“每条线属于哪个分区”的所有权语义。
- 处理:让每个固定分区在自己的 `.game-resource-plane` 中拥有独立 SVG,卡片与路径都直接使用布局逻辑坐标并共享父级 CSS scale / 原生 scrollviewport 原生 overflow 负责本区裁剪。DOM 测量只换算本区逻辑 viewport,用于完整路径、incoming / outgoing 继续线和两端离屏隐藏,不参与端点身份或主路径坐标。每区 observer 和 RAF 各至多一个,卸载时清理。
- 验证:同时挂载至少两个分区和各自同类型关系,断言每条边只存在于对应分区 SVG;只滚动其中一分区,另一分区的逻辑 viewport 与 path 不变。另覆盖缩放后 SVG / 卡片仍在同一 plane、双向离屏继续线、两端离屏隐藏、marker、自环以及 mode / 项目切换清理。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/ResourceDependencyOverlay.tsx``apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts``docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`
## 正式保存不能等待 React state 才取得草稿 CAS 的新 revision
- 现象:用户刚完成编辑就点击“保存到项目”,自动草稿保存已经成功,但正式提交仍携带旧 `expectedDraftRevision`,于是单窗口也得到 draft revision conflict;快速连续保存时还可能使用不同 commitId 重复 staging。
- 原因:`setDraft(result.value)` 的 React state 提交晚于当前 Promise 链,正式保存若从闭包或下一次 render 读取 revision,会把 UI 调度时序误当成持久化顺序。相同问题也会出现在选择变化未标脏、父组件每次 render 新建 scope 对象而重复恢复、项目切换后旧 generation 回调继续写 notice。
- 处理:草稿保存队列在 CAS 成功后同步更新 `draftRef.current` 并直接返回权威 draft;正式提交继续使用该返回值的 revision。scope 按 project/draft/intent/source 原始字段稳定化,所有导入、保存、生成和事件回调捕获当前 epoch,选择变化属于草稿合同并必须标脏。首次正式保存冻结 commitId/idempotencyKey,未知结果只重放原请求。
- 验证:用 deferred Promise 证明草稿 CAS 完成后正式 commit 使用新 revision;相同 scope 值重渲染不重复 recover/load;锁定图层选择进入草稿更新;项目切换后迟到 generation/commit/event 均不改变新会话。
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx``apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx`
## 共享画布不能用全局 DOM 查询或宿主整包 CSS 作为隐式依赖
- 现象:页面挂两个画布时,第二个画布点击小地图会移动第一个画布;反复挂载后 wheel 触发多次。Tauri 单独引入网站 `index.css` 时还会带入账号、项目页和历史业务样式,或因仓库外源码解析到第二份 React 而出现 Hook 错误。
- 原因:`document.querySelector`、body 级 portal、未清理的 listener/observer/animation frame 和不受作用域约束的 CSS 都把组件实例与网站宿主当成全局单例;Tauri Vite 默认根目录又不等于仓库根,React 解析路径可能分叉。
- 处理:共享小地图先从当前 viewport 查询,只有文档中唯一候选时才兼容旧单实例形态;wheel、ResizeObserver 和 animation frame 在 effect cleanup 中逐项释放。portal 支持实例 root,默认 body 只作兼容。共享 CSS 全部限定在 `.genarrative-image-canvas`,两宿主 alias 同一 `packages/` 源码并 dedupe React/ReactDOMTauri `fs.allow` 覆盖 repo root,禁止导入主站完整 `index.css`
- 验证:共享 React 测试重复 mount/unmount 后 wheel add/remove 数量相等、ResizeObserver 精确 disconnect,并挂两个含各自小地图的 viewport,确认只更新目标实例;网站与 Tauri 壳分别 typecheck/build。
- 关联:`packages/image-canvas-react/src/useImageCanvasViewportControls.ts``packages/image-canvas-react/src/CanvasPortal.tsx`、根目录与 `apps/ai-game-creator-shell` 的 Vite 配置。
## 桌面工作台不要让 Supervisor 内容高度挤掉输入区和 Agent Dock
- 现象:`1280×800` 或更矮窗口中,Supervisor 的消息、Runtime 状态和错误正文共同按内容高度增长,聊天输入被推到栏外;底部 Dock 使用固定宽度卡片时还会在中等宽度造成页面级横向溢出。中央画布的生成卡和状态栏若同时固定高度并隐藏 overflow,进度、失败、重试、保存或取消动作会被裁掉。
- 原因:四区工作台没有把“主区内部滚动”和“页面级滚动”分开;Supervisor 的长状态没有独立上限,Dock 卡片不能收缩,宿主又用全局按钮/固定行高样式覆盖共享 chrome。
- 处理:桌面工作台使用 `100dvh` 两行网格,第一行 `minmax(0, 1fr)` 承载中央区与 Supervisor,第二行承载 Dock;消息和 Runtime 分别内部滚动,composer 保持最后一行并设置明确层级。Dock 卡片使用可收缩 flex 与文本省略。画布 dialog、进度/失败卡和状态栏设置 `min-height: 0`、受限最大高度与内部滚动;宿主不再覆盖所有按钮,只为主要动作和布局提供 token 化薄样式。
- 验证:AppSurface CSS 合同检查 `100dvh`、Supervisor composer、Runtime 内滚动、Dock 常驻/可收缩;素材画布测试检查生成 dialog、operation card、状态栏和窄屏保存动作不会被裁剪。真实入口在 `1280×800` 测量 document/body client 与 scroll 一致;登录门禁不可为视觉测试绕过。
- 关联:`apps/ai-game-creator-shell/src/styles.css``apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css``apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts``apps/ai-game-creator-shell/tests/assetCanvasSurface.test.tsx`
## 素材保存区不要把机器 subtype 当普通文本框(2026-08-06
- 现象:保存区同时显示“画布素材”和裸 `asset` 文本框,普通用户无法判断两者用途;自由修改 kind 会形成无法稳定参与类型布局、Agent 合同和替换兼容性的 subtype。工具栏与保存设置挤在同一行时,主要保存按钮还会被压缩或裁切。
- 原因:把 Host Port 的 `name / assetKind / mediaType` DTO 直接映射成同层输入控件,没有区分用户命名、机器分类和编码格式,也没有为中央区域的真实容器宽度保留主操作列。
- 处理:名称保留编辑;create kind 使用 Runtime 权威四项目录和中文标签,refine 从源 manifest 继承并锁定,未知历史值只透传;格式继续使用有限枚举。工具动作和保存设置显式上下分行,保存列使用 `max-content + nowrap`,窄容器时按钮独占整行。普通工作区状态只显示项目名称,不把绝对路径作为默认辅助文案。
- 验证:Surface 测试断言四项用途、默认值、精修未知 kind 锁定、最终 commit 参数和保存按钮 CSSAppSurface 断言普通界面找不到绝对路径,内部 Tauri 调用仍使用原完整路径。
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx``apps/ai-game-creator-shell/src/features/asset-canvas/assetCanvasSurface.css``apps/ai-game-creator-shell/src/features/project-workspace/`
## 正式素材提交不能把多文件写入或 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-<commitId>`、revision 最多推进一次、源资产与血缘正确,响应丢失后返回 already-committed,矛盾 fixture 不自动重试。
- 关联:`docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`
## 素材保存成功不等于迟到结果仍有权抢占当前焦点
- 现象:用户等待生成/保存时切到另一个项目、run、另一份素材草稿,或主动选择其它资源、修改搜索条件;旧请求完成后界面却切回旧画布、清空筛选并自动选中新资源。
- 原因:异步回调只检查“请求成功”或捕获的旧 `isMounted/projectId`,没有绑定中央状态 session、draft/intent、selection epoch 和 query epochmanifest 投影这一数据事实又被错误地与“当前应自动聚焦”的用户意图合并处理。
- 处理:保存开始捕获 `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`
## 派生 Debug 会让完整配置经应用状态递归进入日志
- 现象:配置和状态当前没有直接日志调用,但新增一行 `debug!(?state, ...)``format!("{config:?}")` 就能把 JWT、后台口令、支付私钥、OSS / provider key 与 SpacetimeDB token 一次性写入日志及 OTel 留存面。
@@ -71,6 +142,14 @@
- 验证:用真实 scheduler 恢复确定性 v1 主 Run;把旧 scheduler 美术 child 置为 running,断言 `canvas.asset_generate``memory.write``task.create``task.update``agent.run_status` 均被拒绝;再持久化其历史 `game/**` writeScope isolated 后代,断言恢复执行写操作仍失败且项目未变。对当前合法美术 child 同样验证 memory/manifest 零写入,再让它带在途外部生成命中硬截止,断言状态进入 `needs-reconciliation` 且 pending/batch/外部生成账本原样保留。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs``runtime_driver/game_chat_fast_path.rs``runtime_driver/main_loop.rs``runtime_tools/file_ops.rs`
## 委派幂等与 child 写入测试不能和真实后台 worker 抢状态
- 现象:测试刚建立 static delivery,自动 parent-wake 就抢先恢复并终结父 Run,使随后同 action 重放被“当前 durable task 仍为 running”拒绝;受限美术 child 测试也可能在后台 worker 抢先终态后,让本应允许的 `assets/**` 写入误报 verification failure。
- 原因:测试 fixture 同时手工推进 journal/manifest,又允许真实后台 future 执行同一父子 Run;单测运行时序决定谁最后写入。若为让测试通过而把 active durable task 门禁整体移动到 existing-child 分支之后,该分支仍可能补建 delivery 或投影 Ready,反而允许终态/过期父 Run 发生修复性写入。
- 处理:保留生产门禁顺序,父 Run 终态后的迟到 delivery 继续只允许 suppressed。需要断言回执、claim 与同 action 重放时,测试持有父 Agent execution lane,断言结束后释放再执行 wake;直接测试 child 写工具时持有目标 Agent lane,再把 child 持久推进到 `running`。所有 lane 均由 RAII 释放,不能依赖后台 future 的调度时机。
- 验证:覆盖父 Run active 时 claimed delivery 的同 action 重放返回 existing、不同 action 的重复缺口仍被拒绝、父终态后的新委派/迟到 delivery 继续失败关闭或 suppressed、合法运行中 child 只可写 `assets/**`。macOS 直接拼接 `std::env::temp_dir()` 的仓库安全测试还应先规范化临时根,避免 `/var -> /private/var` 被误当成项目内符号链接。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delegation.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs``apps/ai-game-creator-shell/src-tauri/src/repository_context.rs`
## 执行锁移交给未确认启动的异步 future 会制造永久 queued
- 现象:父 Supervisor 与 Runner 一直显示运行中、heartbeat 正常,专业 Agent 已有 `background_task.queued``autonomous_ready_task.scheduled`,对应执行锁也被 Runner 持有,但该 child 永远没有 running journal、`turn.started` 或后续 Runtime event;其它同批 Agent 可能已经完成。
@@ -111,6 +190,30 @@
- 验证:单测覆盖 CAS 边界(满了返回失败且计数不越界、上限为 0 时任何进入都失败),并由独立用例覆盖 guard 离开作用域后的计数归还。预算耗尽路径只断言 `504`,不得通过另一个测试也会修改的进程级 static before/after 来推断“未入队”,也不得用串行锁或 `--test-threads=1` 掩盖隔离问题。
- 关联:`server-rs/crates/api-server/src/editor_project.rs``try_enter_bounded_queue``EditorPixelArtSnapQueueGuard`)。
## dependency 不能复用 type 的紧凑间距或让窄层始终顶部对齐
- 现象:相邻卡片间的橙色引用只剩一个箭头,看起来像长度异常;同一个菱形 / 分叉关系中,上半组线很短而下半组线绕很远。
- 原因:type 模式的 `16px` 紧凑行列间距不足以同时容纳 marker 安全距离和可辨认线身;分层布局若只按每层 index 从簇顶向下排,单节点层无法与多节点层的垂直中心对齐。
- 处理:dependency 自动坐标使用独立 `48px` 列间距和 `40px` 行间距,type 继续使用 `16px`。相关簇记录最大层行数,每层起始 y 增加 `(maxRows - layerRows) * dependencySlotHeight / 2` 的确定性偏移;平局仍用稳定资源 ID,手动坐标仍原样占位。不能通过裁短长线、偏移真实端点或压缩 Rust depth 伪造一致长度。
- 验证:纯模型锁定 type / dependency 间距隔离、四节点菱形的首尾单节点层居中、同层稳定顺序、历史手动坐标和 4096 项性能;Overlay 使用相邻 dependency 槽位证明箭头前保留可辨认线身。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts``ResourceDependencyOverlay.tsx``resourceCanvasLayoutModel.test.ts`
## 依赖聚类不能把聚合 task-flow 展开为资源两两边
- 现象:为了让 task-flow 的两端资源靠近,若对每个 source × target 构造边,资源多的任务流会迅速放大内存、排序工作和虚假关系;同一图输入还可能随着成员枚举顺序出现不稳定排列。
- 原因:task-flow 的业务语义是任务对的聚合流,不是资源间的完整笛卡尔依赖;dependency depth 也已经由 Rust SCC read model 权威计算,前端不能用布局边重建业务方向。
- 处理:布局分组把每条 flow 作为一个临时流节点,仅与其 source / target 成员相连;中位数扫描读取流另一端成员现有 rank 的中位值。遍历保持迭代式,扫描轮数固定,所有初始序和最终平局都以稳定资源 ID 收口。用于自动重派生的拓扑签名先按固定分类过滤并规范化稳定 ID,再生成固定大小摘要,不能把显示名、卡片大小或浏览器几何加入签名。跨分类 reference 与跨分类-only task-flow 不进入前端布局;所有 task-flow 都不进入 SVG 或画布关系说明。`producerMappingTruncated` 时继续只消费现有同类型精确引用,不重建 task-flow。
- 验证:纯模型覆盖多入、多出、聚合 task-flow、环、4096 链和重复输入坐标一致;Hook 覆盖仅改邻接、深度不变仍重派生自动坐标。不得把搜索后的可见集传入聚类。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts``useProjectResourceCanvasLayout.ts``resourceDependencyGraphModel.ts`
## 四分区 SVG 不能把跨类型业务关系当成可绘制几何
- 现象:跨分类资源位于彼此独立滚动和裁剪的 viewport;若仍绘制一条全局 SVG 路径,只会在两个分区中留下没有完整上下文的断线,滚动时还会看似随机出现或消失。同一卡片多边若都锚在中心点,也会让合法的同类型线叠成一束。
- 原因:Rust read model 的业务关系范围大于资源管理画布的展示合同;四分区视图没有跨标题栏的合法连线走廊。几何层直接遍历全部 reference edge 等于把业务真相误当成全部可视关系;单中心端口又忽略了边的稳定身份与对端顺序。
- 处理:保留 Rust 图与权威深度;布局拓扑按资源分类过滤 reference 和 task-flow 超边切片,关系说明与 SVG 则只消费同类型精确引用。相同分区内按对端坐标、稳定边 ID 为同侧精确边分配有界端口。不要通过改变端点、隐藏同类型合法精确边或生成资源笛卡尔积来换取整洁。
- 验证:同时覆盖跨分类精确引用与跨分类-only flow 不聚类 / 不绘制、全部 task-flow 零 SVG / 零画布关系说明、同侧多边端口不重合且重复输入路径一致、同类环 / 自环和 4096 项回归。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/index.tsx``ResourceDependencyOverlay.tsx``resourceCanvasLayoutModel.ts`
## 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` 的系统别名误判为用户符号链接。
@@ -119,12 +222,20 @@
- 验证:运行 `npm run check:maintenance-page``npm run check:production-api-deploy``npm run check:server-rs-ddd``npm run test -- scripts/spacetime-repair-editor-canvas-resources.test.ts`,并在 Linux CI 保留同一生产脚本语义。
- 关联:`scripts/deploy/maintenance-on.sh``scripts/check-maintenance-page.mjs``scripts/check-production-api-deploy.mjs``scripts/deploy/production-api-deploy.sh``scripts/check-module-runtime-artifact.mjs``scripts/spacetime-repair-editor-canvas-resources.mjs`
## 分区内部滚动不能只重测分区原点
- 现象:若滚动时只重测分区原点,线会停在旧位置或穿过标题栏;若进一步把“卡片完整位于 viewport”当作关系挂载条件,同一合法关系会在卡片刚触边时突然消失、滚回又出现,箭头也可能恰好落在 clip 外而只剩一截线。
- 原因:全局 SVG 与 section plane 不共享 transform / scroll`getBoundingClientRect + RAF + state` 重建屏幕端点只能异步追赶浏览器合成层;卡片可见性又是显示裁剪状态,不是关系身份。SVG marker 贴卡或贴裁剪边界时还可能只剩主 path。
- 处理:每个 section plane 自己持有 SVG,让路径和卡片直接使用同一逻辑坐标与父级 scale / scroll;每区只以一个 Observer 和 RAF 维护逻辑 viewport。精确引用源端或目标端单独离屏时分别绘制 outgoing / incoming 边界继续线,两端离屏才隐藏;目标锚点预留固定箭头间隙,marker 使用 `userSpaceOnUse` 且允许 overflow;自环整体外移避免箭头压卡。搜索隐藏端点仍属于业务可见性过滤,不能与 viewport 裁剪混用。
- 验证:覆盖同帧多次 scroll 只调度一次 RAF、部分离屏时 outgoing / incoming 正确切换、两端离屏隐藏、缩放后路径与卡片仍处于同一 plane、箭头可见、分区互不串线,以及每区单 observer 与卸载清理。搜索隐藏任一精确端点时整条橙线隐藏;task-flow 始终不渲染。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/ResourceDependencyOverlay.tsx``apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`
## External Editor taskId 不能当作本地 manifest taskId
- 现象:画布资产之间已有橙色精确引用线,但依赖任务之间没有灰色 task flow;测试用 `design-foundation` 之类字符串时正常,真实生成返回 `task-1` 后失败。
- 现象:Rust read model、资源详情或 dependency 聚类中缺少本应存在的 task-flow;测试用 `design-foundation` 之类字符串时正常,真实生成返回 `task-1` 后失败。画布不显示灰色 task-flow 是当前产品决定,不能再用是否出现虚线判断 producer 映射是否正确。
- 原因:`GameCreationAppAssetSource.taskId` 保存的是 External Editor 生成任务身份,命名空间与本地 `.agent/manifest.json` 的 Agent/task 身份不同;前端用 `taskById.get(source.taskId)` 会让真实画布资产全部失去 producer。
- 处理:资源依赖图的 Tauri Rust read model 从有界 `.agent/agent.db` 读取 `agent.runtime.canvas.asset_generate`,以 `assetId -> agentId` 映射 producer,并要求 `agentId` 存在于当前 manifest。记录缺失、多个不同有效 Agent 冲突或读取已截断时失败关闭 producer assignment、task flow 与对应 `cyclicTaskIds`,不回退 `source.taskId`。精确 `asset-reference` 仍只依赖 manifest 中外部 resourceId 的唯一匹配;Rust 独立返回的 `dependencyDepths` 继续作为 manifest / reference read model 权威结果,前端只过滤未知资源、负数、非整数和非安全整数,不得因 producer 截断把它整体清空。
- 验证:Rust fixture 把 `source.taskId` 固定为 `task-1 / task-2`,只有审计提供 `art-director / design-foundation` 后才生成 task flow;移除或截断审计后橙色引用保留、灰色任务流消失,合法深度仍为 `asset:spec=0 / asset:ui=1`。AppSurface 使用截断生产数据形状证明深度 `0 / 1 / 2` 真实到达卡片布局,并且不会把已有自动坐标持久化成扁平布局。
- 验证:Rust fixture 把 `source.taskId` 固定为 `task-1 / task-2`,只有审计提供 `art-director / design-foundation` 后才生成 task-flow read model;移除或截断审计后该 flow 消失但橙色引用保留,合法深度仍为 `asset:spec=0 / asset:ui=1`前端始终断言 task-flow 零 SVGAppSurface 使用截断生产数据形状证明深度 `0 / 1 / 2` 真实到达卡片布局,并且不会把已有自动坐标持久化成扁平布局。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs``apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts``docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 依赖图未就绪时不能先初始化资源布局
@@ -739,7 +850,7 @@
- 现象:用户点击图片素材的“快速编辑”后,画布上额外出现 `Quick Edit Generator` 占位,像是新建了一个生成器;但用户预期是在原图下方框选区域、填写一个提示词和模型,然后直接修改当前图。
- 原因:快速编辑入口和提交链路误用了 `createQuickEditGenerationDialogDraft(...)` / `CanvasGenerationDialogState`,把“覆盖源图”的快速编辑伪装成会产出新图层的生成器占位。
- 处理:图片快速编辑必须走 `QuickEditPanelState`,打开时归档当前 active generation dialog 但不创建新的 `mode="quick-edit"` dialog;提交时调用 `/api/editor/images/edits`把当前图片或带编号标注的图片作为 `sourceImageSrc`,成功后覆盖源图,失败时保留快速编辑面板。快速编辑任务进入 `generating` 后必须移除框选工具和覆盖层,禁止继续新增框选;失败恢复面板后可继续调整框选再重试。图片重绘、去背景、视频快速编辑等会产出新图层或异步占位的入口仍可走 generation dialog / placement 链路。
- 处理:图片快速编辑必须走 `QuickEditPanelState`,打开时归档当前 active generation dialog 但不创建新的 `mode="quick-edit"` dialog;提交时调用 `/api/editor/images/edits`主来源始终使用当前图片已登记的 `resourceId``sourceAssetId`。带编号标注的图片上传后只作为辅助 `referenceImageSrcs`不能替换主来源身份;成功后覆盖源图,失败时保留快速编辑面板。快速编辑任务进入 `generating` 后必须移除框选工具和覆盖层,禁止继续新增框选;失败恢复面板后可继续调整框选再重试。图片重绘、去背景、视频快速编辑等会产出新图层或异步占位的入口仍可走 generation dialog / placement 链路。
- 验证:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx -- --runInBand`,以及按需运行 `npm run test -- src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx -t "快速编辑|quick edit" -- --runInBand`
- 关联:`src/components/image-editor/ImageCanvasEditorView.tsx``src/components/image-editor/useImageCanvasGenerationWorkflow.ts``src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts``src/services/image-editor/editorImageReference.ts`
@@ -786,11 +897,19 @@
## 图片画布快速编辑元数据必须记录原图引用
- 现象:快速编辑生成的新图可以替换画布,但打开图片信息时“生成输入”里看不到被修改的原图。
- 原因:信息面板直接渲染 `generationInputs.references`;快速编辑虽然把原图作为 `sourceImageSrc` 传给 provider,但如果 `buildQuickEditGenerationInputs(...)` 不把源图写成引用,后端资源和画布层都没有可展示的原图引用。
- 原因:信息面板直接渲染 `generationInputs.references`;快速编辑虽然 `sourceReferenceId` 指定原图,但如果 `buildQuickEditGenerationInputs(...)` 不把该业务 ID 写成引用,后端资源和画布层都没有可展示的原图引用。
- 处理:快速编辑的 `generationInputs.references` 必须始终包含 `原图`,再追加用户额外参考图;关闭额外参考图入口时也不能删除这条源图引用。
- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx -- --runInBand`
- 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts``src/components/image-editor/ImageCanvasMetadataModalView.tsx``src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`
## 图片编辑主来源不能接受 objectKey 或请求类型
- 现象:调用方可把 objectKey、URL 或 Data URL 当作主来源,再用请求 `assetKind` 或另一个允许编辑的目标图层为禁止类型“借壳”;无目标图层时,后端还会扫描账号全部项目和素材库。
- 原因:HTTP DTO 同时承担外部请求与队列载荷,来源身份、存储定位和类型真相混在 `sourceImageSrc/sourceResourceId/assetKind` 中;worker 没有按业务 ID 复核入队后的身份漂移。
- 处理:站内与 External v1 API 调用方只提交必填 `sourceReferenceId`,且只接受当前账号项目资源 ID 或素材 ID;上传对象必须先登记。后端按两张表主键分别窄查,双表同 ID 时失败关闭,objectKey 仅作为服务端解析结果。目标绑定优先比较双方 `assetObjectId`,缺失才比较 canonical `(bucket, objectKey)`,并校验双方默认类型一致。队列保存版本化解析快照,worker 执行前再次定点解析;旧任务只把既有资源 ID 或旧来源字符串本身作为业务 ID 尝试迁移,禁止 objectKey 反查和旧 `assetKind` 真相回退。
- 验证:覆盖资源 ID、素材 ID、双表冲突、跨账号、raw objectKey/URL/Data URL/Blob URL、旧字段、禁止类型、目标对象与类型冲突、快照漂移、旧任务迁移、红框图辅助引用,以及 Canvas Agent 缺少 `reference_id`
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs``server-rs/crates/api-server/src/editor_project.rs``server-rs/crates/api-server/src/external_generation_worker.rs``src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts``docs/openapi/genarrative-external-v1.openapi.json`
## 图片画布生成完成应用项目快照后也要刷新素材库
- 现象:部分素材生成成功后画布上已经出现结果,但左侧素材库没有立刻出现新素材,刷新页面后才显示。
@@ -4211,15 +4330,13 @@
- 处理:先以 CAS 单独 commit `queued -> executing`,成功后才调 ToolHost;调用返回后再 commit observation。恢复见到 executing 或 ToolHost 返回 Unknown 时只能进入 reconciliation,不得自动重执行。重复 resume 不得继续增 revision 或重复 event。
- 验证:在“ToolHost 已调用、observation commit 失败”处注入故障,序列化快照并用新 engine 重载;断言重复 resume 后 ToolHost 计数仍为 1,且只有显式 reconcile observation 才恢复 running。
## Runtime pending 恢复不能让大型 async frame 共用默认 worker 栈(2026-08-03
## Runtime 后台执行不能让大型 async frame 共用默认 worker 栈(2026-08-03
- 现象:Supervisor collaboration durable isolated spawn 恢复测试在默认 Tokio worker 栈下稳定 `stack overflow`;单独运行同样失败,提高 `RUST_MIN_STACK` 后通过。
- 原因:不是业务递归。debug 构建中 pending action continuation、后台 task queueAgent 主循环各自形成大型 async poll frame;恢复路径在同一次 poll 调用链直接进入下一层状态机,累计超过 worker 默认栈。
- 处理:整个 pending continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,使上层 poll 先退栈后再轮询下一层状态机。传入边界的 future 必须先装箱;若泛型 helper 直接持有大型 future,即使随后 `spawn`,调用方 async frame 仍会把它保留在默认 worker 栈上。边界必须保留结构化取消语义;当前使用 boxed future 与 `JoinSet`,父 continuation 被丢弃时同步 abort 子任务。不得增大 CI 的 `RUST_MIN_STACK`,否则生产默认栈仍可能崩溃。
- 验证:失败用例必须在未设置 `RUST_MIN_STACK` 时通过;同时覆盖 policy batch 全组、拒绝 pending 后重规划并 drain 下一任务,以及 pending/cancellation 回归,证明恢复不重复生成 isolated spawn、队列继续推进且父任务取消不遗留后台子任务。
- 2026-08-10 补充:Provider、Codex CLI 与 Codex app-server 合并到同一个模式分发后,即使本轮实际选择普通 Provider,未装箱的组合 future 仍携带最大分支状态;委派子任务完成后回流父 Agent 的既有回归会在默认 Tokio worker 栈稳定溢出,单独运行同样失败,扩大 `RUST_MIN_STACK` 才通过。持久重试 helper 与非持久压缩路径都必须在构造完整物理 Provider request 后、进入下层泛型 control/lifecycle helper 前装箱;不要逐个扩大 queue worker 栈,也不要等到底层 helper 内部再装箱已经进入调用方 frame 的泛型 future
- 2026-08-10 验证:未设置 `RUST_MIN_STACK` 时运行 `background_agent_runtime_can_delegate_task_to_other_agent`,并追加 `provider_retry_``provider_handoff_``response_stream_` 与 Native shell 完整门禁;测试只能以默认 worker 栈通过,不能把 CI 环境变量当修复。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs`
- 现象:Supervisor collaboration durable isolated spawn 恢复测试或普通 `agent.delegate` 后台委派测试在默认 Tokio worker 栈下稳定 `stack overflow`;单独运行同样失败,提高 `RUST_MIN_STACK` 后通过。
- 原因:不是业务递归。debug 构建中 pending action continuation、后台 task queueAgent 主循环,以及 Provider、Codex CLI、Codex app-server 组合模式分发的最大分支状态都会形成大型 async poll frame;恢复路径直接进入下一层状态机、普通后台任务把完整主循环放回默认 worker,或组合 future 进入泛型 helper,都会超过默认栈。
- 处理:整个 pending continuation、它进入的后台主循环,以及完成、取消或失败后 drain 同 Agent 后续队列时,都必须跨越独立 Tokio task 轮询边界,使上层 poll 先退栈后再轮询下一层状态机。传入边界的 future 必须先装箱;若泛型 helper 直接持有大型 future,即使随后 `spawn`,调用方 async frame 仍会把它保留在默认 worker 栈上。普通后台任务、静态委派子任务和 manifest ready-task 的首次执行统一复用 16 MiB 专用 Runtime worker,并在 worker 已启动后交接 Agent 任务锁;worker 创建或交接失败要持久化当前 run 失败。Provider 物理请求必须在持久重试 helper 与非持久压缩路径构造完整请求后、进入下层泛型 control/lifecycle helper 前装箱,不能等到底层 helper 才装箱。pending 边界继续保留结构化取消语义,父 continuation 被丢弃时同步 abort 子任务。不得逐个扩大 queue worker 栈,也不得增大 CI 的 `RUST_MIN_STACK` 掩盖问题,否则生产路径仍可能崩溃。
- 验证:失败用例必须在未设置 `RUST_MIN_STACK` 时通过;同时覆盖普通后台委派、policy batch 全组、拒绝 pending 后重规划并 drain 下一任务,以及 pending/cancellation 回归,证明任务锁只交接一次、恢复不重复生成 isolated spawn、队列继续推进且父任务取消不遗留后台子任务。另需运行 `background_agent_runtime_can_delegate_task_to_other_agent``provider_retry_``provider_handoff_``response_stream_` 与 Native shell 完整门禁,全部以默认 worker 栈通过。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_queue.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_retry.rs`
## Provider 可扩展不能用一个全局 protocol 枚举代替实例隔离
@@ -4289,7 +4406,7 @@
- 原因:把“客户端没有收到结果”误判为“服务端没有受理”,又没有持久保留逻辑请求的幂等键和服务端返回的 `operationId`。托管 MCP 若绕过 External REST router 直接调用 worker 或 SpacetimeDB,也会形成第二套去重与状态语义。
- 处理:一次逻辑生成只分配一个稳定幂等键。桌面 Runtime 在 POST 前先把 endpoint、精确请求体字节、SHA-256 和幂等键原子写入私有生成账本并回读一致;收到 `202 + operationId` 后先把账本升级为 `accepted` 再轮询。`accepted` 只恢复 GET`prepared` 或提交响应丢失时,只允许校验账本身份、配置指纹和请求 SHA 后,以账本保存的原 endpoint、原始正文与同一键恢复同一逻辑 POST,不得重建画布上下文、重组正文或换键。恢复 `202` 后继续 GET,恢复再次 transport 失败仍保留原账本;轮询超时只保留既有 operation 并恢复 GET。game-chat 的 4500 秒硬截止可以结束本轮、关闭预览和客户端,但 executing 的 `canvas.asset_generate` 必须保留 pending action、provider batch 与生成账本;旧 `200` 图集的 `spritesheetResource` 允许为空,此时只在顶层 `spritesheetImageSrc` 是有效下载引用时优先使用,否则回退可用 `objectKey``202` 缺 operationId、状态损坏与 `postprocess-failed-source-preserved` 仍进入对账边界;其它 non-blocking warning 继续消费成功结果并单独展示。旧 `200` 兼容不改变权威 External v1 的异步契约。MCP 生成工具必须把 `idempotencyKey` 映射到同一 REST header,并复用同一 External router、owner 和任务账本。这是 External v1 的专用幂等恢复,不是通用副作用自动重放。
- 补充:不能把“accepted 分支里没有生成 POST”误当成 GET-only 恢复。若读取账本前仍重做项目/素材目录准备、输出路径预检或请求正文构造,恢复仍可能创建远端资源或在查询 operation 前失败。恢复必须直接使用 durable snapshot;清理必须最后删除 pending 身份锚点,活动 orphan 不得自动删除。完整恢复 future 还要在默认 Tokio worker 栈下验证,不能靠测试环境调大 `RUST_MIN_STACK` 掩盖栈溢出。
- 加固:durable snapshot 必须绑定不含明文凭据的 base URL/API Key 配置指纹,配置漂移时恢复 POST 和 GET 都必须阻断。accepted operation 明确 failed 也不能在 observation 持久化前删账本。旧 `200` durable result 只保留允许字段与安全 objectKey/相对路径,签名 URL、query/fragment 和未知字段不落盘。只有首次提交直接返回契约明确的 `400 / 401 / 403` 才可证明未入队并清理 prepared 账本;首次结果已经未知后,恢复请求的临时鉴权错误、超时、冲突、限流、网关错误及其它意外状态均保留同一账本。账本根目录、扫描和删除必须通过受控路径解析逐级拒绝符号链接,不能让项目内链接把清理目标指向项目外。
- 加固:durable snapshot 必须绑定不含明文凭据的规范 base URL 服务身份指纹;服务地址漂移时恢复 POST 和 GET 都必须阻断Developer API Key 轮换则必须继续原 operation。accepted operation 明确 failed 也不能在 observation 持久化前删账本。旧 `200` durable result 只保留允许字段与安全 objectKey/相对路径,签名 URL、query/fragment 和未知字段不落盘。只有首次提交直接返回契约明确的 `400 / 401 / 403` 才可证明未入队并清理 prepared 账本;首次结果已经未知后,恢复请求的临时鉴权错误、超时、冲突、限流、网关错误及其它意外状态均保留同一账本。账本根目录、扫描和删除必须通过受控路径解析逐级拒绝符号链接,不能让项目内链接把清理目标指向项目外。
- 代理 DNS:Clash 等透明代理可能把公网对象存储域名解析到 RFC 2544 的 `198.18.0.0/15` fake-IP。下载器只对已通过鉴权 `objectKey` 或受控 legacy path 换签得到的 URL 接受“全部地址均位于该 benchmark 段”的窄例外;直接 URL、其它本机/私网地址、公私混合解析和重定向仍必须失败关闭,不能为了兼容代理整体移除 SSRF 校验。
- 验证:覆盖“服务端已入队但提交响应丢失”后两次 POST 的 endpoint、正文 bytes 与 `Idempotency-Key` 完全相同,原键重试仍返回同一 operation,最终只出现一份 completed result 和一次计费 / 写回;恢复再次 transport 失败或临时鉴权失败仍保留同一账本;换 owner 不可见;MCP 与 REST 对同一 owner、同一请求和同一键必须命中同一 operation。
- 关联:`server-rs/crates/api-server/src/external_generation.rs``server-rs/crates/api-server/src/external_mcp.rs``docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`
@@ -4362,6 +4479,20 @@
- 处理:焦点状态机只比较稳定 `resourceId``null -> id``idA -> idB` 聚焦详情,`idA -> idA` 保持当前 active element。显式收起 / Escape 才恢复原卡片与滚动;后台删除清理 focused / matching selected ID 并聚焦搜索框;项目或运行视图切换清空 trigger / restore。媒体预览副作用依赖稳定 ID、路径和类别,不因同 ID 对象重建先卸载控件。
- 验证:媒体控件获得焦点后用同 ID 新 manifest 重渲染并断言 active element 不变;删除资源后断言详情关闭、选中清理且搜索框获得焦点;既有收起、Escape、项目切换和运行切换测试继续通过。
## manifest 与 revision 必须作为同一一致快照发布(2026-08-05)
- 现象:旧 manifest 的 React effect 在正式素材提交后才读取项目 revision,可能把“旧内容 + 新 revision”发给父级;若它先到,真正的 commit manifest 会被误判为同 revision 分叉并失败关闭。
- 原因:manifest 和 mutation revision 分开读取,却把其中任意时刻的两个值拼成一个权威快照;单独比较 callback 到达顺序无法修复这种身份错配。
- 处理:普通 Supervisor 投影固定执行“revision 前读 -> manifest -> revision 后读”,两次 revision 相同才发布,漂移时有界重试。素材 command/event 直接使用事务返回的完整 manifest 与对应 revision。父级按 `projectPath + projectId` 单调接受更高 revision,同 revision 只允许内容一致的重复,低 revision 和分叉都不覆盖。
- 验证:分别覆盖 command/event 两种先后、成功后旧轮询和同 revision 不同 manifest;不能只用 eventId 去重而跳过 revision 防倒灌。
## 新资源自动聚焦不能把投影、布局和 DOM 当成同一时刻(2026-08-05
- 现象:保存回调已经带回 manifest,但新卡片可能尚无 dependency/type 坐标或尚未提交 DOM;立即选择会得到空画布、错误滚动,迟到回调还会抢走用户后来选择的资源。
- 原因:把 durable commit、资源投影、关系图 ready、两份布局协调和 React DOM commit 压成一个“保存成功”布尔值,缺少保存尝试身份和用户意图 generation。
- 处理:保存开始记录 `saveAttemptId + sessionId + draftId + commitId + focusGeneration`。自动定位依次等待资源投影存在、dependency/type 两份布局 settled 且都有位置、搜索条件可见和稳定 `data-resource-id` DOM 存在;按 commitId 只执行一次。切项目、切 mode、改选择/搜索、取消或开始新 flow 都推进 generation;迟到结果仍可合并权威 manifest,但不能改变选择。隐藏时保留搜索,只由显式“清除搜索并定位”建立新 generation。
- 验证:覆盖 manifest 已更新但布局未完成、DOM 后只聚焦一次、搜索隐藏、保存中切项目/改选择和连续保存;测试不得用 reload 或重开项目绕过阶段边界。
## 不要用自然语言精确 `.replace()` 维护 Runtime Prompt
- 现象:Prompt 文案稍作改写、增删空格或调整段落后,替换静默失效,代码中出现难以审阅的链式 `.replace()`
@@ -4452,6 +4583,52 @@
- 处理:全部全局 sink 测试共用一把 test-only 串行锁,并由 RAII guard 在 `Drop` 中无条件清空;测试统一使用 `manifest_invalidation_sink_isolation_` 前缀。relay fixture 对 accept 和 payload 分别使用非阻塞轮询与总 deadline,不使用固定 sleep;生产 loopback、token、连接 / 写入超时和 payload 大小校验保持不变。
- 验证:用 `--test-threads=2` 重复运行统一 filter,覆盖正常 relay、无事件 accept 超时、不完整 payload 超时、panic 展开清理,以及 GUI owner attach 配置与 guard 清理。
## 远端图片 completed 不能冒充本地资源创建成功(2026-08-05)
- 现象:External operation 已返回 completed,但稳定引用缺失、下载失败、正式资产事务中断或 manifest 已提交而 UI 事件丢失时,界面仍可能提前显示“资源创建成功”,重复回调还可能再次下载、写文件或登记资源。
- 原因:把远端生成、媒体传输、本地 durability、manifest 投影、布局和选择压成一个 completed 布尔值;同时把 External idempotencyKey、operationId 或 taskId 暴露到公开草稿,导致恢复逻辑从非权威状态重建请求或误绑本地 task graph。
- 处理:使用私有 generation ledger 保存原请求、External 身份、稳定远端引用、固定 staging token 与本地 commit 身份;公开面只投影不可逆阶段。启动时先恢复阶段三事务,再恢复原 generation;重复 completed 先检查远端引用、staging 和 committed ledger,只有 `committed | already-committed` 才进入 manifest 投影。用户取消等待只推进 focus generation,不删除账本或伪装远端取消。
- 精修补充:`sourceImageSrc` 是可下载的稳定媒体引用,`sourceResourceId` 是资源身份,二者不能因为都可表现为字符串就填同一个 objectKey。本地 `local-asset:*` 只保留在本地 manifest 血缘;没有真实 External resourceId 时省略 `sourceResourceId`
- 验证:覆盖确认前零调用、同 key 连点、accepted 重启 GET-only、重复 completed、取消后迟到、下载后本地事务恢复、事件丢失、切项目/改选择、旧轮询隔离、实时布局与选择、精修血缘及敏感字段零泄漏。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs``apps/ai-game-creator-shell/src/features/asset-canvas/AssetCanvasSurface.tsx``docs/technical/【技术方案】客户端素材创作无限画布阶段一合同-2026-08-05.md`
## 可恢复生成账本不能持久化 direct-upload ticket2026-08-05
- 现象:为支持参考图上传中断恢复,把完整 upload ticket 放进 generation ledger;账本随之包含 Provider host、formFields、policy、signature 或临时 Authorization,项目目录泄露即可复用临时凭证。
- 原因:把“恢复所需的稳定远端身份”和“仅供一次上传的临时授权材料”当成同一种持久状态。原子 sidecar 只能保证写入完整,不能让敏感字段变安全。
- 处理:ticket 结构不实现 Serialize/Deserializehost/formFields 只在本次内存调用中使用。账本在上传前只保存稳定 bucket/objectKey;重启先用这组身份调用 object confirm,确认成功后只保留 objectKey/assetObjectId 并清掉上传中间态。账本测试必须直接序列化完整 ledger,扫描 Provider URL、Authorization、policy、signature、API Key 和 ticket 字段名。
- 验证:运行 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml private_generation_ledger_never_serializes_upload_credentials_or_provider_url`,并继续检查公开草稿、manifest、事件和普通错误不含 prompt、operationId、Key、绝对路径或媒体正文。
## 客户端内部用途目录不能直接作为 legacyPrefix2026-08-10
- 现象:本地图片精修或视频、音频等全类型资源编辑点击生成后立即提示参考资源上传失败;私有账本的 `uploadBucket/uploadObjectKey/operationId/requestBodyJson` 全为空,服务端也没有 OSS、confirm、生成或扣费记录。
- 原因:direct-upload ticket 的 `legacyPrefix` 不是任意业务目录,而是 `platform-oss::LegacyAssetPrefix` 的权威白名单值。把 `asset-canvas-references``resource-editor-references` 直接放在该字段会被 api-server 在签名之前以 `400` 拒绝;客户端若把票据、OSS 和 confirm 全折叠成一个错误码,还会掩盖真正失败阶段。
- 处理:客户端编辑器统一使用合法私有 `legacyPrefix=generated-character-drafts`,把业务用途放入 `pathSegments`:图片画布为 `editor/asset-canvas-references/<projectId>/<draftId>/<generationId>`,全类型资源编辑为 `editor/resource-editor-references/<projectId>/<operationId>`。仍严格执行 ticket → OSS form POST → object confirm,只有 confirm 返回自洽稳定 `objectKey/assetObjectId` 后才允许提交生成;不要为内部目录扩白名单或新建上传接口。图片路径按本地校验、票据、对象上传、对象确认分别使用安全错误码,票据材料继续只驻留内存。
- 验证:客户端端到端测试必须断言 confirm 早于生成 POST、请求使用精确前缀与 pathSegments、账本只持久化稳定对象身份;票据失败时断言 `operationId/requestBodyJson` 为空且 manifest 只有源资产。api-server 测试应断言生成的 key 位于 `generated-character-drafts/editor/...` 且 access 为 private。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs``apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs``server-rs/crates/api-server/src/assets.rs`
## Tauri 有平台登录 Token 不代表应调用主站画布 API2026-08-10
- 现象:客户端素材画布和全类型资源编辑从 WebView 读取平台 Access Token,把它传给 Tauri command,再调用 `/api/editor/*``/api/assets/*``/api/runtime/external-generation/jobs/*`;代码同时保留 External 分支,导致真实 UI、Runtime 和测试使用不同路径,发布客户端还错误依赖网页画布登录态。
- 原因:把“客户端壳有账号登录能力”误当成“客户端画布属于主站网页宿主”。主站和 Tauri 虽复用相同请求 DTO 与后端生成服务,但对外边界不同:主站使用站内认证路由,Tauri 远端媒体能力使用 Developer API Key 和 External v1 路由。
- 处理:Tauri 前端不读取、透传或持久化站内 Access TokenRust 只从发布 AppData 私有 `editorApi.baseUrl/apiKey` 解析 External 凭据。图片、视频、音效、BGM 的项目/素材库、上传、确认、生成、轮询与换签全部留在 `/api/external/v1`,不访问内部 job 查询或账号/profile 接口。账本只绑定 External 配置身份指纹,升级前遗留的站内 endpoint 必须进入待对账状态,不能拿 External Key 自动重放。Key 缺失或无权限只返回安全配置错误,不打印 Key、Authorization、Provider 正文或私有路径。
- 验证:前端测试断言 command input 不含 `accessToken/apiKey`Rust mock 服务器拒绝任何 `/api/editor/*``/api/assets/*``/api/runtime/external-generation/jobs/*` 请求,并覆盖 External `202`、原 operation 轮询、换签、非破坏性本地提交和账本零凭据。主站路由与 OpenAPI 未发生契约变化时不得为了客户端切换修改后端接口。
- 关联:`apps/ai-game-creator-shell/src/features/asset-canvas/tauriImageCanvasHostAdapter.ts``apps/ai-game-creator-shell/src/view/project-development/index.tsx``apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs``apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`
## prepared journal 之前同样存在正式事务崩溃窗口(2026-08-05)
- 现象:事务依次安装 before/after 快照后才写 journal;若进程在首个快照、全部快照或 journal 已写但 ledger 未写时退出,重启扫描看到 transaction 目录却无法进入原先只覆盖 prepared 之后的恢复状态机,可能留下孤儿目录或阻塞项目后续提交。
- 原因:把 `prepared` 当成事务的第一个可观察持久阶段,忽略了构造 prepared 证据本身也由多次原子文件安装组成。
- 处理:把首个快照、全部快照和 journal 后/ledger 前加入故障矩阵。无 ledger 时只允许清理受控快照与本模块临时文件;若 journal 已存在,还必须证明正式目标不存在、manifest 和 project revision 精确等于 before。未知文件、正式文件存在或权威状态漂移全部失败关闭,不能递归猜测清理。清理后同步 transaction 父目录,并允许同 commit/idempotency 身份安全重放。
- 验证:故障矩阵逐阶段恢复;额外用同一幂等身份在快照残留清理后提交两次,必须得到一次 committed、一次 already-committedmanifest 仍只有一个 canvas asset。
## 宿主事件接线不能顺手复制共享 history 栈(2026-08-05
- 现象:Tauri Surface 已复用共享 viewport/transform/renderer 数学,却另外维护 undo/redo refs、快照克隆和恢复逻辑;网站共享 hook 后续增加内容安全或字段恢复时,两端会静默分叉。
- 原因:把 Pointer 事件接线、宿主生命周期胶水和可复用 history 算法放在同一组件中,误以为没有复制整个画布目录就已经满足共享源码边界。
- 处理:两宿主直接消费共享 `useCanvasHistory`;共享 snapshot 统一覆盖 viewport、selection、图层位置和 width/height,宿主只声明本地媒体是否允许安全移除/重做。Tauri 仍可保留 Pointer capture/epoch/host callback 接线,但选择、平移、缩放、变换、renderer 和 history 状态机不得在宿主重写。
- 验证:主站 history 定向测试覆盖 resize undo/redoTauri 新建、导入、编辑、撤销重做和 durable commit 用例必须在同一共享 hook 下通过。
## 编辑器生成不能把传输重试、参考图截断和客户端 provenance 当成独立小问题(2026-08-05
- 现象:生成 POST 首次已经入队但响应丢失时,客户端自动重试产生第二个任务;第 6 张或更多参考图仍显示在 UI / 元数据里,却没有送给 provider;直接构造请求还能把任意资源 ID 写成最终素材引用。
@@ -4501,6 +4678,13 @@
- 处理:所有付费编辑器生成在队列 enqueue 前和 worker / inline 执行前复用只读 `preflight_editor_generation_target_and_return`,按认证 owner 校验可选项目及归一化目录;读取失败和归属不匹配一律失败关闭。helper 返回 canonical 项目与目录并覆写后续入队 / worker / 原子准备使用的 payload,不能校验 trim 后的项目却持久化原始空白值。角色图片、角色动作、图标 spritesheet 与 UI 提取省略目录时按实际默认目录预检;默认目录允许尚未创建,自定义目录必须存在且 owned。预检不替代最终 procedure 复验,也不保证跨外部调用的目录锁定。
- 验证:源码顺序回归必须覆盖图片生成、图片修改、图标 spritesheet、UI 设计图提取、视频、角色动作、SFX 与 BGM 的 enqueue / direct 两层,证明纯本地格式和 `data:` / `blob:` 稳定引用门禁先执行,canonical target 在预检后写回 payload,远端引用解析、generation input rebuild、扣费、入队、provider 与 OSS 均留在预检之后;模块侧扫描证明预检只调用 runtime identity、项目、目录只读校验且不含 insert / update / delete,并覆盖带空白项目、`project`、旧 `folder-*`、默认目录 ID、自定义目录与 `None` 归一化。
## 聚合点赞数不能恢复当前浏览者是否点赞(2026-08-10)
- 现象:陶泥儿精选点赞写入成功、总点赞数也正确,但刷新或重新挂载后图标恢复成未点赞;前端再次点击会发出错误意图或让计数体验混乱。
- 原因:`editor_showcase_asset.like_count` 只表达全局聚合,公开列表未携带 `editor_showcase_asset_like(showcase_id:user_id)` 的 viewer 状态;前端用生命周期内的空 `Set` 充当真相,刷新必然丢失。仅靠 `likeCount > 0` 无法判断其中是否包含当前用户。
- 处理:公开列表使用可选鉴权 viewer 投影;登录态从 Bearer claims 派生 user ID,并在公开列表事务内按确定性 like 主键返回 `viewerLiked`,匿名固定 false。个性化响应禁止共享缓存或错误降级,追加 `Vary: Authorization` 时不得覆盖 handler 或内层中间件已有字段。写入采用服务端确认式更新,账号 / 鉴权 scope 变化后重载并丢弃旧请求回包;request generation 的激活与失效必须跟随已提交 effect,不能在 render 阶段修改 ref;输入 user ID 不能代替 runtime service identity 鉴权。
- 验证:覆盖刷新 / remount 保持已点赞、pending 期间不改图标计数、失败保留旧状态并播报错误、分页保留 viewer state、鉴权恢复不发匿名请求、登录 / 退出 / 换号与旧首屏 / 分页 / POST 回包竞态、被 Suspense 放弃的 viewer 渲染不影响当前已提交请求,以及无效 Bearer 返回 `401 + private,no-store + Vary: Authorization`;中间件测试另需证明已有 `Vary` 字段被保留。
## 非整除 nearest 会让逻辑像素块宽窄不一(2026-08-10)
- 现象:像素规整后的图片虽然保持了源图宽高,放大观察却能看到相邻逻辑块占用的物理列数或行数不同,表现为部分块更宽、部分块更窄;整数倍样例看起来正常,换一张网格数不能整除输入尺寸的图才复现。
@@ -4543,3 +4727,35 @@
- 原因:pool 只按 LLM 凭据和路由复用进程,节点身份只用于进程内 thread map。任一 stdout framing、子进程退出或连接故障都会 drain 整个进程的 pending/turn router,使所有共享节点同时失去可信终态。
- 处理:pool key 必须包含 `projectId + agentId + sessionId + runId`,每个权威节点直接持有独立 app-server 子进程;同节点 turn 还要串行,不能向同一 thread 并发 `turn/start`。只发送当前 CLI schema 定义的字段;stderr 使用有界内存尾部并先脱敏再进入 Runner 诊断。
- 验证:至少两个节点并发各跑多轮,确认存在两个 app-server PID;终止其中一个后只有对应节点进入 reconciliation,另一个仍能收到 `turn/completed`。旧 AppData 缺 `agentMode` 且含非 Responses 路由时必须保留 `provider`,不能在项目自动恢复时批量失败。
## 模拟 Provider 的隔离 AppData 测试必须显式固定执行模式(2026-08-11)
- 现象:测试已经写入本地 mock `baseUrl / apiKey / model`,却收不到任何 HTTP 请求,日志反而显示 Codex app-server 启动或退出;Native shell 全量中多个后台 Agent 用例一起超时。
- 原因:新安装和没有迁移上下文的隔离 AppData 默认使用 `codex_app_server`。只写 `agentLlm` 不能表达测试要走 HTTP Provider;直接切换 runtime config dir 的 fixture 也不会经过会自动补 `agentMode` 的测试 helper。
- 处理:任何要断言模拟 HTTP Provider 请求的配置都必须显式写 `agentMode=provider`。测试 helper 可以统一补齐,但直接写隔离 AppData 的 fixture 仍须在自身 JSON 中声明,不能依赖仓库 `.env`、用户 AppData 或历史迁移。
- 验证:先单跑失败用例确认请求命中 mock server,再执行完整 `npm run check:native-shells`;日志中不得出现该用例启动 Codex CLI/app-server,所有 Provider/MCP 请求数量和顺序按 fixture 闭合。
## Tauri 生成与资源编辑恢复不能依赖 UI 快照、旧 Key 指纹或队列首项(2026-08-11
- 现象:应用重启后,任务视频、项目版本或 refine 草稿无法恢复;轮换 Developer API Key 后已有 operation 被误判为配置变化,已受理任务一次 401/403 还可能永久进入对账;文本 Provider 已成功但尚未 staging 时崩溃会重复调用。目录中放入大量无关文件还能绕过 pending 扫描上限。一条远端已明确失败的老 operation 会持续占据队列首项,挡住后续已受理或已下载任务;manifest 已写而 project revision 未写时,又可能被误标为 committed,或者 journal 已证明提交后因项目继续合法修改而无法补 ledger。durable committed 后遗留 staging 可能因一次删除失败而被误报为提交失败,也可能在正式媒体或 manifest 身份已经漂移时被直接删除;manifest 已有派生子版本而 journal 缺失,或旧 journal 没有 revision 身份时,也可能被猜成已经提交。派生视频再次编辑时若把 `assetObjectId` 当远端引用,生成会失败或指向错误身份。
- 原因:早期账本只保存显示层资源 ID,恢复时又依赖当前页面资源对象;refine `draftId` 只在组件 Map;配置指纹混入 Key 并把认证错误写成状态机终态;Provider 正文从内存直接进入解析/staging;扫描计数只在识别出 pending JSON 后递增;本地登记 ID 与 External generation 接受的稳定 `objectKey` 没有分层。恢复 UI 只选排序后第一项,而账本又没有远端终态失败/归档阶段;资产提交恢复把整个历史 after manifest 当作永久相等条件,没有区分目标事务事实与后续合法提交;旧 version journal 只保存 base/target 数值,不能证明完整 project revision before/after 身份。
- 处理:新账本冻结完整源快照,旧账本从权威 manifest、完成任务和版本记录有界恢复;refine 从正式 sidecar 按项目、意图、源素材和 active 状态唯一发现。服务身份用 `service-origin-v1` 哈希规范化 External base URL,确认 UI 只展示去除路径与凭据的服务 origin;旧 Key-bound 指纹由快照绑定的显式挑战迁移,确认前零网络动作,已受理任务换 Key 后只 GET 原 operation。Provider 调用前先持久化 request-issued,成功正文再写 durable handoff 后解析/stagingissued 无 handoff 只能对账。扫描在读取每个目录条目时先计数,任何文件都消耗预算。提交前复验源摘要,远端请求只使用账本已确认的稳定 `objectKey`,恢复始终复用原 operation 和请求字节。
- 队列与事务:独立恢复面板必须展示后端权威队列的所有 operation,读取失败不能伪装为空。`remote-failed` 不再重放,只能显式标为 `archived` 并保留账本;`reconciliation-required` 不能归档。派生 asset 使用 `prepared -> media-installed -> manifest-written -> revision-written -> committed` journal,只对可证明状态前向恢复;尚未证明目标写入时严格核对 before/after,已证明目标 asset/media 与 target revision 后允许 manifest/revision 被后续合法提交继续推进,并补齐同一 ledger。committed 后只有 staging 与正式媒体摘要一致、manifest 按 ID 或路径唯一精确匹配 journal asset 时才尽力清理;删除 I/O 失败保持 durable committed,身份或媒体漂移保留 staging 并进入对账。version journal 同样冻结 project revision before/after 身份;manifest 已有子版本但 journal 缺失,或旧 journal 面对已推进 revision 无法补证时都失败关闭。
- 验证:覆盖跨进程唯一 refine 草稿发现和多候选失败关闭、文本 Provider 成功到 staging 崩溃后零重复调用、任务视频/版本旧账本恢复、所有目录条目上限、Key 轮换与旧 Key 无法验证时的显式确认、Accepted 后 401/403 再换 Key 只 GET 原 operation、远端明确失败只归档且零新网络/扣费、三条乱序恢复队列、项目切换迟到结果、asset transaction 各崩溃阶段、revision 后项目继续合法修改仍补齐 ledger、committed 后 staging 清理成功/删除 I/O 失败/媒体或 manifest 漂移保留、源摘要漂移拒绝、committed 视频二次派生,以及 manifest 子版本缺 journal、旧 version journal 无法证明 revision 推进与 version journal exactly-once。
## 子 Agent 澄清不能直接穿透用户输入权限(2026-08-12)
- 现象:child 需要产品取舍时若直接调用 `user.input_request` 会被 owner gate 拒绝;若把它误走 `needs-repair`Supervisor 会错误返工而永远不向用户提问。
- 正确路径:child 返回短小的 `AGC_NEEDS_USER_INPUT_V1` envelopeRuntime 生成 `needs-user-input` delivery,父 Supervisor 认领后创建自己的 durable `user.input_request`。回答仍绑定原父 run,续建 child 由稳定 delegation identity 幂等控制。
- 验证:重复 wake / Runner 重启不得创建第二个用户输入 action;问题数量、字段长度、问题 SHA 和答案 SHA 不匹配时必须 fail-closed。child 直接请求用户输入仍应保持拒绝。
- 恢复加固:正常 completed child 的最终回复也必须进入 envelope 解析;回答后 pending 会被下一轮动作替换,因此 continuation 不能读取 current pending 作为证据,必须读取原 delivery 上的 durable request/answer 绑定。多个 child 各用一条用户请求逐一收束,禁止把不同 delivery 的问题和答案指纹拍平混用。
- 协议演进:`agent.delegate` 的澄清 continuation 字段虽然在 strict schema 中是 required nullable,但 Runtime 解析器仍必须接受完全未携带这三个字段的既有调用;只允许三者全缺失、全 `null` 或全为合法字符串,部分出现、部分字符串和非法 SHA 均失败关闭。新增 schema 字段时要同步原生函数目录断言与旧调用回归,避免协议修复轮次打乱 Supervisor 协作计划。
- 门禁优先级:已经存在真实 `preview.validate` 失败 observation 或 durable `failed_playtest_revision` 时,具体试玩修复与新 revision 重新验证门禁必须先于通用“首次 mutation”门禁;否则 Runtime 会把明确的试玩修复错误收窄成普通 pre-mutation repair,导致 Supervisor 无法选择正确的协作动作。
## 无限画布延迟草稿与零位移不能制造新状态(2026-08-11)
- 现象:r5 的 `loadDraft` 比 r6 更晚回包时会把草稿回退;pointerdown 后没有任何移动,pointerup 仍增加一条空 undo 并触发 CAS 保存;Tauri 自行维护 Shift toggle 后,Shift 单击唯一选中图层会意外清空选择。同一 `canvas.failed` 视图还可能把草稿保存或提交故障显示成生成重试。
- 原因:延迟回包只与发起时 revision 比较,没有在落地时复核当前最高 revision;指针按下就 capture history,而不是等首次真实几何变化;宿主复制了共享 selection 规则;失败状态没有携带发生故障的 operation 类别。
- 处理:generation progress、保存队列、生成/提交回包和延迟 `loadDraft` 统一用当前 scope、触发最低 revision 与回包当下草稿的单调门禁;同 revision 只允许完整相等回包。Tauri 指针和键盘选择复用 `resolveLayerPointerSelection`。pointerdown 只冻结快照,首次真实 move/resize/pan 才 capture 一次;零位移、未变选择和锁定图层不增加 undo、documentVersion 或草稿保存。
- 失败边界:`canvas.failed` 必须携带 `generation / draft-save / asset-commit / recovery / cancellation`,只有 `generation` 失败显示“返回修改/重新确认”。保存/CAS 只重试或重载,提交/恢复只安全恢复或对账,取消故障只保留草稿继续编辑;初始恢复失败也不得进入生成重试。
- 验证:用 deferred Promise 覆盖 r5/r6 逆序、保存与 progress 交错和 scope 切换;同时覆盖 Shift 单选自身、多选拖动、指针完整序列、零位移、首次有效移动只一条 history,以及五类失败的可访问名称与按钮集。
File diff suppressed because one or more lines are too long
@@ -0,0 +1,319 @@
# AI 游戏创作 Agent Runtime 交互边界重构实施计划
更新时间:`2026-08-12`
状态:评审中
## 0. 目标与范围
统一 ConsumerGUI / CLI / 自动化测试)与 Runtime 之间的公开交互边界,达到:
- **Consumer 只做两件事**`render(state)``dispatch(intent)`,中间不保留决策。
- **交互 Loop 收归后端 Supervisor Shell**Consumer 不再根据 Runtime 状态自行选择 start / steer / confirm / retry / resume。
- **GUI、CLI、测试夹具是同一套协议的平等 Consumer**,GUI 没有任何特权通道。
- **Runner 自驱**:工作发现、恢复、继续执行不依赖 Consumer 在线或主动触发。
本重构**不重新设计 Runtime 内部执行模型**(Part D 保持黑盒),只补充 Shell 需要的边界能力。
### 不在本轮
- Agent 执行状态机(main_loop / task_queue / recovery)内部重构。
- Runner 进程生命周期策略(开机自启 / 无 GUI 常驻)——自驱只限于"Runner 存活期间",保留 GUI 启动 + GUI-owner watchdog。
- LLM / Provider / 提示词体系改动。
---
## 1. 交互 Loop 协议(Interaction Contract
这是 Consumer 与 Supervisor Shell 之间唯一的公开协议。协议不绑定 transportTauri 命令 / Runner 协议 / 进程内函数均可用同一套 DTO)。
### 1.1 出向事件(Shell → Consumer
| 事件 | 含义 | 现状落点 | 是否新增 |
|---|---|---|---|
| `progress` | 运行推进 | `status/phase` + `recentEvents` + `waitingOn/nextStep`,经 `read_game_creator_agent_runtimes``game-creator-agent-progress` 事件 | 收敛 |
| `needs_input` | 等待澄清回答 | `userInputRequest` / phase `waiting-for-user-input``AgentRuntimeUserInputRequest` | 收敛 |
| `approval_required` | 等待开发者批准工具动作 | `pendingToolAction` / phase `waiting-for-confirmation``AgentRuntimePendingToolActionSummary` | 收敛 |
| `tool_request` | 工具已请求/执行中 | 现散在 `recentToolCalls` + phase `action` | **新增派生** |
| `artifact` | 产物 / manifest 变化 | `game-creator-manifest-invalidated` + finalization journal | 收敛 |
| `done` | 本轮终态 | `completed` + `AgentRuntimeFinalizationJournal` + responseStream `committed` | 收敛 |
| `error` | 失败 / 需人工核对 | `failed` / `needs-reconciliation` + `error` | 收敛 |
协议 DTO(camelCase 序列化,与现有一致):
```rust
enum AgentRuntimeOutboundEvent {
Progress(AgentRuntimeProgress),
NeedsInput { request: AgentRuntimeUserInputRequestView },
ApprovalRequired { action: AgentRuntimePendingToolActionSummary },
ToolRequest(AgentRuntimeToolRequest), // 新增:Shell 从 recentToolCalls+phase 投影
Artifact(AgentRuntimeArtifactEvent), // 收敛 manifest-invalidated + finalization
Done(AgentRuntimeDoneEvent),
Error(AgentRuntimePublicError),
}
struct AgentRuntimeProgress {
project_path: String, agent_id: String, run_id: String,
status: String, phase: String,
current_task: String, current_action: String,
plan_steps: Vec<AgentRuntimePlanStep>, active_plan_step_index: Option<u32>,
waiting_on: String, next_step: String, // Shell 负责填充,Consumer 不再映射 phase→文案
updated_at: u64,
}
```
> 与现状的关键差异:`progress` 里的 `waiting_on`/`next_step` 由 **Shell 填充**。当前是前端在 `model.ts:612/644` 硬编码 phase→中文文案,收归 Shell 后前端删除该映射。
### 1.2 入向命令(Consumer → Shell
| 命令 | 吸收的旧命令 | 说明 |
|---|---|---|
| `submit_intent` | `start_*` / `steer_*` | 一条消息可能 steer 进现有 run,也可能 start 新 run,由 Shell 判定 |
| `answer` | `answer_game_creator_agent_runtime_user_input` | 回答澄清 |
| `approve` | `confirm_*` / `reject_*` | 批准或拒绝,含 policy 确认卡 |
| `cancel` | `cancel_game_creator_agent_runtime_task` | 取消 |
| `resume` | `resume_*` / `confirm_resume_*` / `retry_*` / `confirm_retry_*` / `schedule_game_creator_agent_ready_tasks` | 恢复/重试/调度统一入口 |
命令签名:
```rust
#[tauri::command]
async fn submit_game_creator_agent_intent(
project_path: String, session_id: String, intent: String,
run_profile: Option<String>, source: Option<String>,
) -> Result<AgentRuntimeIntentResult, String>;
#[tauri::command]
async fn answer_game_creator_agent_interaction(
project_path: String, run_id: String, action_id: String,
request_id: String, response_id: String, answers: BTreeMap<String, String>,
) -> Result<AgentRuntimeResult, String>;
#[tauri::command]
async fn approve_game_creator_agent_interaction(
project_path: String, run_id: String, action_id: String,
approved: bool, note: String,
) -> Result<AgentRuntimeResult, String>;
#[tauri::command]
async fn cancel_game_creator_agent_run(
project_path: String, agent_id: String, run_id: String,
) -> Result<AgentRuntimeResult, String>;
#[tauri::command]
async fn resume_game_creator_agent_project(
project_path: String,
) -> Result<Vec<AgentRuntimeResult>, String>;
```
协议外 API(不进交互 Loop,作为管理面保留在 Shell 上):goal CRUD、`compact`、会话管理、配置读写。
### 1.3 统一 InteractionRequired 语义
Shell 向 Consumer 暴露"必须等待外部回答"的单一概念,替代现在分散的 `pendingToolAction` / `userInputRequest` / policy 确认卡:
```rust
enum AgentRuntimeInteractionRequired {
UserInput { request: AgentRuntimeUserInputRequestView },
Approval { action: AgentRuntimePendingToolActionSummary },
PolicyApproval { policy: String }, // 新增:吸收 confirm_resume/confirm_retry 的 agent.resume 确认卡
}
```
> 现状的 `confirm_resume``commands.rs:1148`)与 `confirm_retry``commands.rs:1020`)是"要求用户确认策略"的产物,由 GUI 弹确认卡实现。收归 Shell 后,Shell 评估 `enforce_project_auto_permission_policy``verification.rs:813`),需要确认时返回 `InteractionRequired::PolicyApproval`,用户确认后经统一的 `approve` 命令继续。
---
## 2. 现状盘点与复用清单
### 2.1 可直接复用的资产(近 1 个月内形成,活跃演进期)
| 资产 | 位置 | 复用方式 |
|---|---|---|
| CLI 交互决策状态机 | `swarm_cli/turn_dispatch.rs:245` `decide_interaction_action``:48` `handle_swarm_user_turn`steer/start、Reply/Execute/Resume、goal 门禁) | **整体上提**到后端 Shell(纯 Rust、无 UI 纠缠) |
| Runner 自驱续跑定时器 | `runtime_driver/provider_recovery.rs``schedule_waiting_provider_retry_wake_after_lane_release` 等 | 已存在,P4 直接复用 |
| 确定性 e2e | `scripts/agent-runtime-deterministic-playable-e2e.mjs` + `deterministic-lane-defense-provider.mjs``expectedProviderStats`/`expectedChildReport` 断言) | P0 基线扩展(协议级 trace |
| 版本协商 fallback | 前端 `model.ts:1186-1226` `isMissing*CommandError` | 迁移期新旧并存的标准模式 |
| 逐命令幂等 | `accepted_run_id``runtime_state.rs:1548`)、runner requestId 缓存(`runner/protocol.rs:353`)、goal CAS | P1 设计直接沿用 |
| 进程内集成测试 | `src-tauri/tests/``command_runtime.rs``runtime_actions/``collaboration/``goal.rs`) | P1-P6 每步回归的护栏 |
### 2.2 需要收敛/改造的点
| 点 | 位置 | 动作 |
|---|---|---|
| 前端 phase→文案映射 | `model.ts:612/644/974/1498/1554` | Shell 填充 `waiting_on/next_step` 后删除 |
| 前端 steer/start 决策 | `model.ts:849` `submitProjectSupervisorRuntimeTask``App.tsx:5791` | 迁入 Shell`submit_intent` |
| 前端 confirm/retry/resume 门禁 | `model.ts:1131-1154``panels.tsx:381-497` | 由 `InteractionRequired` + `approve/resume` 取代 |
| 前端跨轮状态修补 | `model.ts:244` `normalizeAgentRuntimeState``:385` `mergeAgentRuntimeStateIntoMap` | 依赖 P2 Snapshot 稳定后删除 |
| CLI 独立状态机 | `swarm_cli/turn_dispatch.rs` | 上提 ShellCLI 只留终端交互(stdin/stdout/observer |
| 重复触发恢复 | `App.tsx:2799/10341``useDeveloperAgentPanel.ts:684`(启动时 resume)、`App.tsx:10550`devMode schedule 按钮) | P4 后删除,由 Runner 自驱 |
---
## 3. 分阶段实施计划
> 主线:**先建安全网 → 建统一入口 → 统一状态读取 → 收回决策 → Runner 自驱 → 迁移全部 Consumer → 删旧面**。
### P0 行为基线(安全网)
**目标**:在改动前建立可判断"公开行为是否变化"的验证能力。
- 复用 `deterministic-lane-defense-provider.mjs`,在现有 `agent-runtime-deterministic-playable-e2e.mjs` 基础上**增加协议级事件 trace**:
- 录制一条确定性完整会话的**出向事件序列**progress/needs_input/approval_required/done/error 的顺序与关键载荷),归一化 timestamp、`run_id`/`steer_id`/`request_id` 随机 ID、`accepted_run_id` 对账值。
- 断言当前 master 的 trace 与预期一致(快照 diff)。
- 建立统一的回归命令,P1-P6 每阶段结束必跑:
- 确定性 e2e(功能正确性)
- `src-tauri/tests/` 进程内集成测试(单元级护栏)
- 协议级 trace(迁移等价性)
- **不动 Runtime 代码**,只建安全网。
**验收**:上述三条命令在当前 master 全绿,trace 基线文件入库。
### P1+P2 统一入口 + 状态读取(adapter + Snapshot 投影)
**目标**:Consumer 改走新协议调用旧实现;状态读取收敛为稳定的公开 Snapshot。**旧接口全保留**为迁移期兼容路径。
**P1 适配器(`submit_intent` / `approve` / `answer` / `cancel` / `resume` 新命令)**
> **P1 与 P3 的边界**P1 只做"命令可用 + `submit_intent` 内部判定 steer/start"。`answer`/`approve`/`resume` 在 P1 **只是入口封装**(内部调旧函数),"收到交互该调哪个命令、能否重试"的判定**仍在前端**;到 P3 才把判定收走,前端只剩 `submit`/`respond` 两种动作。
- 新增 `src-tauri/src/agent/supervisor_shell/`,内含:
- `intent.rs``submit_intent` 内部路由——读当前 runtime → 判定 steer/start(逻辑取自 `swarm_cli/turn_dispatch.rs` 的 steer 判定与前端 `matchingAgentRuntimeForSteer`)→ 调现有 `steer_game_creator_agent_runtime_task_for_profile_at``start_game_creator_supervisor_background_task_for_session_at` → 返回 `{ mode, accepted_run_id, runtime }`
- `interaction.rs``approve/answer/cancel` 薄封装现有 `confirm/reject/answer/cancel` 内部函数(P1 只封装,判定仍在前端)。
- `resume.rs``resume_game_creator_agent_project` 吸收 `resume/confirm_resume/retry/confirm_retry/schedule_ready`——Shell 判定是否需 policy 确认、是否需先 cancel 再重试(`needs-reconciliation` 分支,逻辑取自 `App.tsx:6205` `handleProjectSupervisorRetry`)。
- 新命令与旧命令**同时注册**`main.rs` invoke_handler)。
- 前端新增"调新命令 → 后端报 unknown command → 回退旧命令"的版本协商(复用 `isMissing*CommandError` 模式),保证打包版本不一致时旧链路可用。
- **fallback 边界**:仅"命令不存在(版本不兼容)"确定性回退;写操作的其他错误原样呈现,不做盲目重试(防重复入队)。
**P2 Snapshot 投影(状态读取收敛)**
> **投影 = 读模型**:把同一份 Runtime durable state(唯一事实来源)按一个稳定、精简、面向消费的 schema 重新导出,作为 Consumer 的权威视图。它**不是新的事实来源**,只是同一份事实的另一种呈现;Consumer 依赖投影,业务真相仍在 durable state。
- 新增 `supervisor_shell/snapshot.rs``AgentRuntimeSnapshot` 投影。
- 输入:现有 `AgentRuntimeResult.state` + `recent_events`/`recent_tasks`/`response_stream`/`user_input_request`
- 输出:稳定的公开视图(agent/session/run 身份、status/phase、`InteractionRequired`、progress、终态、公开错误)。
- **内部字段(recentToolCalls、observations、allowedTools、contextUsage 等)不进 Snapshot**。
- 关键:**后端先补"稳定读取"**——现状前端 `normalizeAgentRuntimeState` 做跨轮 carry-forward,是因为后端 read 在恢复/竞态时字段不稳定。P2 后端投影保证同一 run 身份下字段自洽,前端才能删掉自己的修补。
- 新增 `read_game_creator_agent_runtime_snapshot(s)` 命令(或改造现有 read 返回 Snapshot),旧 `read_game_creator_agent_runtime(s)` 保留。
- 前端 `normalizeAgentRuntimeState` / `mergeAgentRuntimeStateIntoMap` / phase→文案映射**依赖 P2 稳定后删除**(本轮先做投影,下一阶段删前端逻辑)。
**验收**
- 新命令与旧命令对同一场景返回的终态一致(用 P0 trace + 确定性 e2e 断言)。
- 前端在"走新 Snapshot"下渲染与旧路径一致(组件回归)。
- 现有 `command_runtime.rs` / `collaboration/` 测试全绿(旧逻辑未动)。
### P3 Loop 收归 Supervisor Shell
**目标**Consumer 只 dispatch 意图,不再持有生命周期判断。
- 完成 `submit_intent` / `approve` / `answer` / `cancel` / `resume` 对全部旧分叉的吸收(P1 已建,本轮做全):
- `approve` 吸收 confirm/reject,并按 interaction kind 分派(`UserInput`/`Approval`/`PolicyApproval`)。
- `resume` 吸收 resume/confirm_resume/retry/confirm_retry/schedule_ready。
- 建立 `AgentRuntimeInteractionRequired`(见 1.3),Shell 统一暴露"需等待外部回答"。
- Shell 负责填充 `waiting_on`/`next_step`(从 phase 映射,逻辑上提自 `model.ts:612/644`)。
- 新增派生事件 `tool_request`Shell 从 `recentToolCalls` + phase 投影)。
- **前端删除**
- `submitProjectSupervisorRuntimeTask` 的 steer/start 决策(`model.ts:849`)。
- `agentRuntimeCanCancel/CanRetry/CanConfirm` 门禁(`model.ts:1131-1154`)。
- `App.tsx:5981/6127/6205` 的 confirm/retry/repair 路由逻辑。
- CLI`swarm_cli`)改为调用同一 Shell:决策逻辑上提后,CLI 只保留 stdin/stdout 终端交互与 observer 渲染。
**验收**
- CLI 走新协议跑通完整 supervisor+子 Agent 流程(`agent-swarm-test-chat.mjs` / 确定性 e2e)。
- GUI 提交、确认、重试、恢复均通过统一 `submit_intent/approve/resume`,无 `steer_*`/`confirm_*`/`retry_*` 直接调用。
- P0 协议级 trace 在"新旧实现各放一遍"下事件序列一致。
### P4 Runner 自驱
**目标**:工作发现、恢复、继续执行不依赖 Consumer 触发。**这是重构主线的一部分,不是独立项目。**
- 项目目录簿:`runner/state.rs``known_roots`(当前进程内)→ 持久化到 AppData(复用 runner 的 `--config-dir`),Runner 重启后仍知道持有过哪些项目。
- 启动自恢复:`runner/server.rs` 启动完成后,对 known roots 调 `has_recoverable_game_creator_agent_background_tasks_at``recovery_scan.rs:425`),有可恢复工作则自动 `resume_game_creator_agent_background_tasks_at`
- 空闲自扫描:主 accept loop`server.rs:275`,已有 25ms `EXTERNAL_AGENT_RUNNER_LOOP_INTERVAL`)内增加"是否有 pending 任务需 wake"的轻量检查,替代 Consumer 调 `schedule_game_creator_agent_ready_tasks` / `wake_pending`
- 吸收 `schedule_game_creator_agent_ready_tasks`manifest ready 任务由 Runner 扫描发现并调度,删除前端 devMode 按钮(`App.tsx:10550`)。
- **边界(不在本轮)**:Runner 仍由 GUI 启动(`main.rs:2124`),保留 GUI-owner watchdog`server.rs:169`)与 `game_chat_release` 退出协议(`main.rs:2311`)。"自驱 = 存活期间自调度 + 启动自恢复",**不含**开机自启/无 GUI 常驻。
- 对应删除前端恢复触发职责:`App.tsx:2799/10341``useDeveloperAgentPanel.ts:684` 的启动时 resume、resume 确认卡回调。
**验收**
- Runner 进程内:确认一个 pending 任务后无需任何 Consumer 调用即自动执行;恢复 pending 动作后自动续跑。
- 确定性 e2e 增加"Runner 独立进程跑完整流程"用例(复用 `agent-runtime-real-e2e/harness/process.mjs` 的二进制编译能力)。
- `confirm_resume` 恢复确认卡流程改为 `InteractionRequired::PolicyApproval``approve`
### P5 迁移 GUI / CLI / Tests
**目标**:三类 Consumer 全部迁移到统一 Intent、Snapshot、Runtime Output。
- GUI
- 状态渲染改读 `AgentRuntimeSnapshot`;删除 `normalizeAgentRuntimeState` / `mergeAgentRuntimeStateIntoMap` / phase 文案映射。
- 交互全走 `submit_intent/approve/answer/cancel/resume`;删除 steer/confirm/retry/resume 直接调用与门禁。
- 保留只读消费形态:response stream 展示、对话合并、画布资产编排(这些不进 Loop 协议)。
- CLI`cli.rs``swarm_cli` 改用 Shell 命令;删除各自状态机(决策已上提)。
- Tests`src-tauri/tests/` 迁移到新命令;`agent-runtime-real-e2e` 与确定性 e2e 走同一协议。
- 迁移顺序:先 CLI(最薄)→ 再 Tests → 最后 GUI(唯一消费 response stream / conversation 合并 / goal CAS / 委派修复路由,工作量最大)。
**验收**GUI、CLI、Tests 调用面收敛到同一组命令,代码差异只剩输入输出形式。
### P6 删除旧公开面
**目标**Interaction Contract 成为唯一稳定公开边界。
- 删除旧 Tauri 命令:`start_game_creator_agent_runtime_task` / `start_game_creator_supervisor_runtime_task` / `steer_*` / `confirm_*` / `reject_*` / `retry_*` / `confirm_retry_*` / `resume_game_creator_agent_runtime_tasks` / `confirm_resume_*` / `schedule_game_creator_agent_ready_tasks` / `read_game_creator_agent_runtime(s)``main.rs:2188-2208`)。
- 删除对应后端 wrapper 与前端 `app/types.ts` 旧 DTO、旧事件协议、迁移期兼容逻辑。
- 现有 start / steer / resume / recovery 能力作为 Shell 内部实现保留(改名/内联)。
**验收**`grep` 无旧命令名残留;全量回归(P0 基线 + 单元 + e2e)全绿。
---
## 4. 迁移策略(新旧并存 + 等价性)
1. **接口即实现,不留空窗**:新命令从注册第一天起就是真实可用——内部套用现有旧函数(adapter 套旧实现是**常态、透明**,前端不知道也不关心)。不存在"接口先立、实现待填"的中间态;分阶段的不是"接口 vs 实现",而是"谁先切到新接口"。
2. **新旧并存**:P1 起新命令与旧命令同时注册,Consumer 逐个切换,旧路径逐条下线(strangler fig)。
3. **版本协商 fallback(仅用于迭代空窗期)**:前端调新命令,**仅当**后端报 unknown command(前端版本 ≠ 后端版本,打包错位)时回退旧命令;其他运行错误**原样呈现,不盲目重试**(防重复入队)。后端新版随应用覆盖到位后,fallback 即死代码,P6 删除。
4. **等价性保障**
- 确定性 provider + 协议级 trace(P0)作为新旧实现的对照基线。
- 进程内集成测试每阶段全跑。
- 关键迁移点(steer/start 判定、resume 路由、needs-reconciliation 重试)用"同一输入 → 新旧实现输出一致"的单测锁定。
---
## 5. 里程碑与验收门禁
| 里程碑 | 交付 | 门禁 |
|---|---|---|
| M0 | P0 基线 + trace 入库 | 回归三件套全绿 |
| M1 | P1 新命令 + adapter(新旧并存) | 新/旧命令终态一致;现有测试全绿 |
| M2 | P2 Snapshot 投影 + 前端读 Snapshot | 前端删 normalize 后渲染回归一致 |
| M3 | P3 Loop 收归(Intent + InteractionRequired | CLI 走新协议跑通完整流程;前端无 steer/confirm/retry 直调 |
| M4 | P4 Runner 自驱 | Runner 独立进程自动跑完;resume 确认走统一 approve |
| M5 | P5 全部 Consumer 迁移 | GUI/CLI/Tests 调用面收敛到同一协议 |
| M6 | P6 删旧面 | 无旧命令残留;全量回归绿 |
---
## 6. 风险与未决问题
| 风险/问题 | 影响 | 缓解 |
|---|---|---|
| P2 依赖"后端 read 先稳定",否则前端不敢删 normalize | 阶段顺序敏感 | P2 后端投影先行,前端删逻辑放同一阶段尾 |
| P4 自驱与 GUI-owner 安全模型冲突 | 若误解为"无 GUI 常驻"会引安全评审 | 计划内明确边界,实现不越界 |
| steer/start 判定含 UX 语义(mode/source/runProfile、steerId 生成、acceptedRunId 对账) | 收归 Shell 后前端展示可能退化 | Shell 返回 `{ mode, accepted_run_id, steer_decision }` 补足展示信息 |
| `tool_request` 无现成单一落点 | 需 Shell 派生投影 | 提前排进 P2/P3 投影工作量 |
| 协议级 trace 的随机 ID 归一化 | 基线易碎 | 复用确定性 provider,归一化规则集中一处 |
## 7. 建议实施顺序(一句话)
P0 建安全网 → P1+P2adapter + Snapshot,纯增量、旧接口全留、fallback 兜底)→ P3 收 Loop → P5 迁移(先 CLI 后 GUI)→ P6 删旧面;P4(Runner 自驱)作为主线中心件贯穿 M4,不单独立项,但边界(存活期间自调度,不含无 GUI 常驻)在计划内写死。
---
## 8. 相对原方案的调整点
本计划在原方案基础上做了以下调整,均基于 master 现状与既有安全模型:
1. **P0 基线**:原方案的 Golden Replay 改为"确定性 e2e + 协议级事件 trace"——在既有确定性 provider e2e`deterministic-lane-defense-provider.mjs`)上扩展,录制协议级事件序列并归一化 timestamp 与随机 ID,不重建基线体系。
2. **P4 定位与边界**:原方案将"Supervisor Lifecycle Coordinator"列为独立阶段;现作为主线组成部分(里程碑 M4,不单独立项)。自驱限于"Runner 存活期间自调度 + 启动自恢复",排除"开机自启 / 无 GUI 常驻",与既有 GUI-owner 安全模型一致。
3. **术语收敛**:原方案"Interaction Contract / Intent / Snapshot"统一为本计划"交互 Loop 协议"5 入向命令 + 7 出向事件)与"投影"(读模型),含义不变。
4. **迁移原则显式化**:接口即实现(adapter 套旧逻辑为常态);fallback 仅在版本空窗期、只认 unknown command。原方案未明确此点。
File diff suppressed because one or more lines are too long
@@ -7,10 +7,10 @@
- 同一面板分为“AI生成/改造”和“视频直接转换”两个标签,旧配方默认恢复 AI;两个标签独立保留草稿;标签为分段按钮样式(明显可点击、带选中态),不再像标题。
- AI 视频以 `motion` 表示动作视频、`appearance` 表示可选角色外观图(可多张);Provider 的 `reference_video` 负责动作、`reference_image` 负责外观。动作视频已选中后,外观图是可选的普通图片参考,不要求画布图层标记为 `character`;上传序列帧 PNG 作为一张完整参考图,不识别网格或拆帧。
- 直接转换使用内部 `/api/editor/character-animations/video-conversions` 与 durable job `editor_character_animation_video_conversion`,固定免费、保留背景,不调用 Seedance、BgFilter 或其它去背景模型,也不重复保存源视频。**去背景暂缓**:直接转换的源视频背景非纯色,本地键色抠图不适用,待确定方案后再做
- 源视频仅接受 owner-scoped MP4/MOV,限制 50MB、215 秒。FFprobe 读取旋转后的显示尺寸、精确时长与平均 FPS;FFmpeg 覆盖完整时间轴,采样率为 `clamp(min(源平均 FPS, 8), 1, 8)`,产出 2120 帧
- 直接转换使用内部 `/api/editor/character-animations/video-conversions` 与 durable job `editor_character_animation_video_conversion`,固定免费、保留背景,不调用 Seedance、BgFilter 或其它去背景模型,也不重复保存源视频;生成完成后如用户点击工具栏 `去背景`,再以独立的零泥点 durable 派生任务处理完整序列
- 源视频仅接受 owner-scoped MP4/MOV,限制 50MB、215 秒。FFprobe 读取旋转后的显示尺寸、精确时长与平均 FPS;先按 `clamp(min(源平均 FPS, 8), 1, 8)` 计算目标帧数,再把帧数限制到 2–66,并按 `最终帧数 ÷ 精确时长` 回算 FFmpeg 覆盖完整时间轴所用的实际采样率
- 480p/720p 表示最大长边,只缩小不放大。正式 `imageSequenceDurationMs` 等于探测时长,播放器、普通 ZIP 与 Spine JSON 的单帧时长统一按 `imageSequenceDurationMs ÷ frameCount`
- 直接转换仍落 `character-animation` / `image-sequence`,配方 action 为 `character-animation.convert`,来源槽位为 `source`。最多 120 帧时第一帧同时承载最终 resource/asset;角色动画拆帧允许 120 项,图标图集公开切片继续限制 64 项,既有 512 KiB/2 MiB JSON 门禁不放宽。
- 直接转换仍落 `character-animation` / `image-sequence`,配方 action 为 `character-animation.convert`,来源槽位为 `source`。最多 66 帧时第一帧同时承载最终 resource/asset;角色动画拆帧允许 66 项,图标图集公开切片继续限制 64 项,既有 512 KiB/2 MiB JSON 门禁不放宽。
- `/api/external/v1` 保持原图片模式,不暴露内部新增字段或直接转换接口,不修改 OpenAPI。
## 1. 文档目的
@@ -93,7 +93,7 @@ manifest.txt
现有角色动作能力已经包含:
- 统一从底部工具栏“Spine序列帧动画”进入;不再从图层右键菜单或选中图层浮动工具栏暴露独立入口;当前选中视频(或带预览视频的序列帧)时,底部入口默认打开“视频直接转换”,其它情况默认打开“AI生成/改造”;
- 底部工具栏保留“Spine序列帧动画”通用入口;角色图层的选中浮动工具栏和右键菜单保留 `生成动画` 快捷入口,但只打开同一个 `character-animation` generation dialog,不维护平行面板;当前选中视频(或带预览视频的序列帧)时,底部入口默认打开“视频直接转换”,其它情况默认打开“AI生成/改造”;
- `character-animation` 生成占位框;
- 动作描述;
- 待机、行走、奔跑、跳跃、攻击、受击、倒下预设;
@@ -232,7 +232,7 @@ flowchart LR
| 需求 | 当前能力 | 结论 |
| --- | --- | --- |
| 工具栏在视频后新增入口 | 当前只能从角色图触发 | 需要新增底部入口 |
| 工具栏在视频后新增入口 | 当前已有角色图 `生成动画` 快捷入口 | 新增底部通用入口并复用同一 dialog |
| 纯文本生成 | 当前必须有角色源图 | 需要扩展后端 T2V |
| 图片 + 文本 | 已实现角色立绘图生视频再抽帧 | 主要复用 |
| 已有序列帧改造 | 仅有部分恢复/首帧能力,来源语义不完整 | 需要明确输入形态并补全 |
@@ -242,7 +242,7 @@ flowchart LR
| 动作“更多”逐行展开 | 当前只有固定 7 个预设 | 需要前端扩展 |
| 1080p | Fast 前后端均不支持 | 暂不开放 |
| 40 泥点/次 | 当前 480p × 4 秒恰好为 40;其他组合不同 | 需确认固定价还是按秒价 |
| 去背景 | 当前生成链自动逐帧去背景 | 与需求中的手动动作冲突 |
| 去背景 | AI 生成链自动透明化;直接转换保留背景 | 保留零泥点手动派生任务 |
| 拆帧生成单 PNG | 帧对象已有,但没有全部登记为独立账号素材 | 可复用对象、补资产绑定 |
| ZIP 下载 | 已完成且匹配附件 | 不重做 |
@@ -475,7 +475,7 @@ flowchart TD
- 任一必需帧失败,整项失败,不发布残缺序列;
- 明确失败退款;
- 原子提交结果未知时按现有对账语义处理,不能先退款再猜提交失败;
- 拆帧只复用已有对象,不再次收 Seedance 生成费;若逐帧去背景改为手动动作,需要另行确定费用
- 拆帧只复用已有对象,不再次收 Seedance 生成费;手动去背景作为零泥点 durable 派生任务执行
## 9. 提示词策略
@@ -520,11 +520,11 @@ flowchart TD
`ImageCanvasBottomToolbarView.tsx`
- 在“生成视频”后保留唯一的“Spine 序列帧动画”入口;
- 在“生成视频”后保留“Spine 序列帧动画”通用入口;
- 点击后在视口中心创建统一的 `character-animation` generation dialog
- 统一面板内同时承载“AI生成/改造”和“视频直接转换”两个标签;
- 当前选中视频或带预览视频的序列帧时默认进入“视频直接转换”,当前选中角色图时默认进入“AI生成/改造”,无适用选中图层时默认进入“AI生成/改造”;
- 右键菜单和选中图层浮动工具栏不再提供“生成动画”或“视频转换为Spine序列帧动画”平行入口;
- 角色图层右键菜单和选中图层浮动工具栏保留 `生成动画` 快捷入口,并复用上述统一 dialog;不新增“视频转换为Spine序列帧动画”平行入口;
- 不在当前面板下方追加内容;移动端保持横向滚动或现有工具栏收纳规则。
`CanvasTool` 增加 `character-animation`,但不新增页面路由。
@@ -601,19 +601,12 @@ flowchart TD
#### 去背景
当前后端默认已经逐帧去背景。推荐方案是
- 生成结果始终为透明 RGBA
- 不再展示“去背景”,或仅在检测到历史不透明序列时展示;
- 不重复对透明帧调用抠图 provider。
如果产品坚持“先保留纯色背景,用户点击后再去背景”,则必须改为派生任务:
AI 生成链默认逐帧去背景,视频直接转换固定保留源背景。`character-animation` 浮动工具栏继续提供 `去背景`,与 `改造 / 拆帧 / 下载` 并列;点击后必须走现有 durable 派生任务,不得把序列第一帧当普通图片处理。派生任务保持以下边界
- 原始不透明序列与透明序列是两个正式 resource;
- 去背景全帧成功后一次性发布新序列;
- 任一帧失败不产生残缺结果;
- 需要单独确定泥点失败退款;
- 该选择会改变当前成熟链路,不建议作为默认。
- 当前费用固定为 0 泥点失败不产生不完整正式结果。
#### 拆帧
@@ -690,7 +683,7 @@ idle
- 改造完整恢复;
- 拆帧绑定既有 frame asset objects
- 条件式去背景(若最终保留)
- 保留零泥点 durable 去背景派生任务
- 顶部下载默认 Spine ZIP
- 元数据文案。
@@ -89,6 +89,8 @@ canvasCompletion?
场景参考图沿用普通图片生成的客户端前置门禁,最多 5 张;超限时不得发送 HTTP 请求。场景生成 POST 使用生成专用零重试策略,避免 inline 响应丢失后重复调用 Provider。
生成完成后的 `scene` 是静态图片素材:它属于画布素材标签的图片媒体族,允许用户把图片图层覆盖标记为 `scene`,并支持既有图片快速编辑链路。前端标签菜单与 SpacetimeDB 结构化布局白名单、前端快速编辑入口与 api-server 图片编辑白名单必须分别成对同步。该编辑能力不改变通用图片生成接口对 `kind = scene` / `assetKind = scene` 的拒绝;新场景仍必须从本节的结构化专用接口生成。
后端对模型、比例和清晰度先沿用 `normalize_editor_generation_options` 标准化,再使用标准化比例决定画幅描述和入队价格。
## 5. Prompt
File diff suppressed because one or more lines are too long
@@ -133,7 +133,7 @@ DELETE /api/profile/api-keys/{keyId}
### 当前状态:v1 尚无外部存量调用方
截至 2026-07-31`external_api_key` 表内没有属于外部第三方存量调用方,v1 处于「已发布但无存量集成」阶段。本节记录的豁免只在该前提成立时有效。
截至 2026-08-08,已按当前线上 API Key 与调用方状态再次确认没有外部第三方存量调用方,v1 处于「已发布但无存量集成」阶段。本节记录的豁免只在该前提成立时有效;本次确认不自动延续到今后的 breaking change,每次仍需重新取得当日线上状态并形成明确决策
### 已接受的未版本化 breaking change
@@ -148,6 +148,8 @@ DELETE /api/profile/api-keys/{keyId}
2026-07-31 同一豁免还覆盖了「八类生成从同步成功响应切换为 `202 + operationId`,新增统一查询接口」这一 breaking change。旧调用方若仍把生成 POST 响应当作媒体结果会立即失败;接受原地修改 v1 的唯一依据同样是上线前已确认没有外部第三方存量调用方。托管 MCP、集成 manifest 与 Skill archive 均为新增入口,不产生既有客户端兼容债务。
2026-08-08「收紧图片编辑主来源契约」把 `POST /api/external/v1/editor/images/edits` 的既有必填 `sourceImageSrc` 替换为新的必填 `sourceReferenceId`,并移除可选 `sourceResourceId / assetKind``info.version` 继续为 `1.0.0`,路径继续为 `/api/external/v1`。严格客户端会因必填字段改名、旧字段被 `additionalProperties: false` 拒绝而立即失败,这属于本节定义的 breaking change。产品负责人已于 2026-08-08 根据当前线上 API Key 与调用方状态确认仍无外部调用方,因此明确接受本次不增加兼容字段、不新开 `/v2`、不设弃用期的原地变更。该豁免只覆盖本次字段替换,不得被后续 breaking change 自动引用。
### 豁免的失效条件
API Key 由用户在个人中心自助发放,因此「无外部调用方」不是受控状态,可能在无人决策的情况下变为假。本节豁免在下列任一条件出现后立即失效:
@@ -214,7 +216,7 @@ SpacetimeDB procedure
外部生成接口复用站内编辑器已有 DTO、入队器和 worker executor,不维护第二套生成语义:
- 图片生成 / 重绘 / 规范图 / 宣发图 / UI 设计图复用 `/api/editor/images/generations``/api/editor/images/edits` 的校验、模型归一、计费和持久化规则,但 External handler 固定只入队。主站和 External 的通用图片入口共用场景专用合同边界校验,禁止用 `kind = scene``assetKind = scene` 绕过后端场景 Prompt 组装;场景专用 handler 自己构造规范请求,不受该通用入口校验影响。
- 图片生成 / 重绘 / 规范图 / 宣发图 / UI 设计图复用 `/api/editor/images/generations``/api/editor/images/edits` 的校验、模型归一、计费和持久化规则,但 External handler 固定只入队。External v1 图片修改必须提交当前账号已登记的项目资源 ID 或素材 ID 作为 `sourceReferenceId`;上传对象必须先登记为项目资源或素材。objectKey、URL、Data URL、Blob URL 以及旧 `sourceImageSrc/sourceResourceId/assetKind` 字段均返回 `400`。服务端分别按资源 ID 与素材 ID 主键窄查,双表冲突、未命中、越权或对象记录无效均失败关闭,快速编辑的完整有效类型白名单为 `null / spec / character / icon-spritesheet / icon-spec / publication-material / ui-design / scene`OpenAPI 的 `x-genarrative-allowed-effective-asset-kinds` 必须与后端白名单精确一致。请求带 `targetLayerId` 时必须同时带 `projectId`;目标图层必须关联有效项目资源,来源与目标优先比较 `assetObjectId`,缺失时比较 canonical `(bucket, objectKey)`,来源默认类型还必须与目标资源默认类型一致。入队载荷保存版本化权威快照,worker 执行前再次定点解析并拒绝身份或类型漂移;仅以素材 ID 编辑时不伪造项目资源关系。主站和 External 的通用图片入口共用场景专用合同边界校验,禁止用 `kind = scene``assetKind = scene` 绕过后端场景 Prompt 组装;场景专用 handler 自己构造规范请求,不受该通用入口校验影响。
- 图标 spritesheet 和 UI 设计图素材提取复用站内拆分逻辑,生成图集后按连通域切片,并把图集与切片都按请求写入项目资源和素材库。
- 角色动画、视频、音效和背景音乐复用站内编辑器生成链路;请求携带 `assetFolderId` 时按站内规则写入素材库,音频类外部调用使用 API Key 所属账号作为 asset owner。
- API Key 管理接口仍只属于登录态个人中心,不进入外部 OpenAPI JSON。
@@ -94,6 +94,8 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去
角色动作正式字段收口使用 `node scripts/spacetime-normalize-editor-character-actions.mjs --database <database> --server-url <url>`,且同样只能由已授权 migration operator 执行。必须先发布包含 normalization cursor 索引和 `normalize_editor_character_animation_metadata_and_return` 的 SpacetimeDB 模块,在 API / worker 仍处于维护模式时先运行默认全量 dry-run;脚本固定按 `asset → project-resource → showcase → canvas` 扫描,普通 scope 每批最多 25 行,canvas 每批最多 5 行。全量 dry-run 会在不写库的情况下把 asset 计划结果投影给同 owner / task / 首帧对象精确匹配的 project-resource,再把前置 scope 的计划结果投影给 canvas 检查;因此同 task 的误标预览 MP4 会先按权威视频对象排除,最终图片序列会逐帧核对并补齐精确 `asset_object` 身份。canvas 中仍引用误标 preview resource 的普通 video layer 会按 project-resource 计划态 `video` 跳过,只有 layout 明确声明动作却指向视频,或资源规划本身失败时才形成 blocker。apply 时仍要求前置 scope 已按顺序物理完成,不能跳过 asset 直接让 project-resource 借未落库结果。历史 canvas 复制的 `sourceResourceId` 不是迁移证据,不要因它仍指向原角色而手工改库,补建资源会采用最终账号素材的 DB 血缘。出现 blocker 时脚本会打印 ID、原因、owner、project、task、对象身份和来源资源;先据此区分最终候选为零 / 多个、正式与旧版冲突、帧对象不匹配或缺失资源,不得跳过 scope。确认 dry-run 后追加 `--apply`,脚本会对每批重新 dry-run、携带该批 SHA-256 apply,并在最后从头要求四个 scope 均为零匹配、零 blocker。只有该复核通过后才发布移除 action fallback 的 API / Web。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-normalize-editor-character-actions.mjs``scripts/spacetime-migration-common.mjs`。本地切换分支时若要避免 dev publish 因 schema 冲突使用 `-c=on-conflict` 清库,启动命令必须追加 `--preserve-database`,让冲突直接失败。
普通图片错误素材类型清理使用 `npm run spacetime:editor-image-asset-kind:clean -- --database <database> --server-url <url>`,只能由已授权 migration operator 执行。先进入维护模式并发布包含 `clean_editor_image_asset_kind_and_return` 的 SpacetimeDB module,并保持旧版本 API / controller / worker 停止;随后运行默认全量 dry-run,核对 `asset → project-resource → showcase → canvas` 各 scope 的扫描数、命中行数、字段数和 blocker 均符合预期,再追加 `--apply`。脚本对每批重新 dry-run、绑定包含画布迁移摘要、结构化 layer 与 generation-dialog 权威 JSON 的 SHA-256,最后自动从头复核零命中;任一画布数据异常都会只输出哈希化 ID、scope 与原因并停止,不能跳过。清理只处理精确业务旧值,不修改 `asset_object.asset_kind`、MIME 或媒体类型;project-resource scope 在清行前验证同工程 migration 并将其状态纳入批次 hashlayout version 0 的 legacy 画布可以没有 migration,但 structured 画布缺 migration 必须立即形成 blocker,资源行不得先被清空;清行后能保持原 status 不变量时立即刷新摘要,否则只允许留给后续精确 canvas 字段清理收口。canvas scope 在任何布局写入前再次按 active / backfilled / rolled_back 状态验证原 migration 凭证和双份 legacy / structured 不变量,将 `editor_canvas_generation_dialog.dialog_json` 与 layer rows 一并扫描并在同一事务 patch;只允许本批资源清零及精确字段删除造成的差异,写入后从全部结构化权威行重建 layout、再次复核新状态才受控重签摘要,同时保持业务 revision、migration status 与全部时间戳不变。新版本 API、SpacetimeDB storage 创建入口、legacy 画布元数据提取和项目资源落表边界都会将 trim 后精确等于 `image``assetKind` 归一为 `NULL`,防止旧页面、滞留请求或 legacy 保存重新制造废弃值。完成零残留复核,并分别确认 cleaned backfilled 可激活、active 可继续保存、rolled_back 可重复复检后恢复应用版本,最后退出维护。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-clean-editor-image-asset-kind.mjs``scripts/spacetime-migration-common.mjs`
自 2026-07-11 起,`Genarrative-Full-Build-And-Deploy` 的每日 04:00 timer 默认以 `DEPLOY_TARGET=development``STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,不得依赖下游 Job 默认值或提前各自发布;统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。上文“定时构建缺少审批人时失败”的旧口径不再作为当前 dev 定时发布行为。
Full Job 通过 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 `KEEP_MAINTENANCE_MODE=true`,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 `Exit Maintenance` 阶段。该阶段只能通过 `agent none` 和显式 `node(...)` 分配目标机,直接执行 `/opt/genarrative/current/scripts/deploy/maintenance-off.sh`;目标机不得 checkout Git、挂载 Git SSH 凭据或依赖 Jenkins workspace 源码。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。`Genarrative-Api-Deploy` 也单独暴露 `KEEP_MAINTENANCE_MODE` 参数,并转换为随发布包脚本的 `--keep-maintenance-mode`;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 `warning` 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。
@@ -234,7 +236,7 @@ npm run check
仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master``codex/ai-game-creator-app` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成四个必须通过的 job:
- `Repository checks`:执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。
- `Repository checks`调用唯一入口 `npm run check:repository-ci`执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。本地 master `pre-push` 复用同一入口,禁止在 workflow 与 hook 中维护两份近似命令。
- `Frontend tests`:按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行干净的 `npm ci`,再独立执行根 `npm run test``npm run bgfilter-worker:smoke-test``npm run check:production-health-patrol``npm run check:production-api-release``npm run check:production-api-deploy`,让 Vitest、Node test smoke harness 及不依赖真实服务的生产巡检 / 发布 / 部署行为 fixture 在 Gitea job 中持续执行;其中 `.test.mjs` 使用 Node test runner,不依赖 Vitest 的 `scripts/**/*.test.ts` 收集规则。
- `Backend tests`:先对 `server-rs/Cargo.lock` 执行带 5 次整命令级有界重试的 `cargo fetch --locked`,再执行 `npm run check:server-rs-ddd``cargo test --locked --workspace --no-fail-fast``api-server --all-targets` 编译和 `spacetime-module` 编译;依赖准备必须位于会触发 Cargo build 的 DDD / 产物边界门禁之前,避免锁新增依赖未命中镜像缓存时绕过既有下载重试。runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。
- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke;共享 Agent Runtime 后台锁 suite 固定 `--test-threads=1`,不能用并行偶发失败后的逐项通过替代整套稳定门禁。
@@ -266,6 +268,8 @@ bash scripts/gitea-ci-job-image.sh load-runner
workflow 首次成功运行后,在 Gitea `master` 分支保护中把 `Project CI / Repository checks (pull_request)``Project CI / Frontend tests (pull_request)``Project CI / Backend tests (pull_request)``Project CI / Native shell tests (pull_request)` 四个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 `<workflow> / <job> (<event>)`。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `genarrative-ci` 标签的 runner,再检查精确 Image ID 是否已装入内层 Docker。
master 日常交付必须禁止直接 push,只允许经 PR 在当前 head 的四个 required context 全绿后合并;本地 `pre-commit` 的 staged ESLint/Prettier 和 master `pre-push` 的 Repository checks parity 只用于提前发现问题,可被 `--no-verify` 绕过,不能充当服务端权威门禁。紧急直推白名单如需保留,应按人员和时限最小化,并要求执行同一 `npm run check:repository-ci <base> <head>` 后回读 push CI。
视觉小说负向扫描与验收门禁:
```bash
@@ -597,7 +601,7 @@ Pingora current release 自审脚本 `scripts/ops/pingora-current-release-audit.
同一 API release 随包依赖还必须包含 `scripts/check-pingora-release-readiness.mjs``scripts/check-pingora-canary-live.mjs`。前者在 current release 上以 `--release-runtime-only` 汇总运行时复核,后者支撑目标 Nginx canary live smoke;缺少任一脚本时不能进入直连切换窗口。
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm``spacetime_module.wasm.sha256``release-manifest.json``scripts/deploy/production-stdb-publish.sh``scripts/deploy/production-runtime-writer-identity-rotate.mjs``scripts/deploy/maintenance-on.sh``scripts/deploy/maintenance-off.sh``scripts/spacetime-migration-common.mjs``scripts/spacetime-maintain-external-generation-jobs.mjs``scripts/spacetime-normalize-editor-character-actions.mjs``scripts/spacetime-migrate-editor-canvas-layout.mjs``scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json``migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret Filepublish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secretID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm``spacetime_module.wasm.sha256``release-manifest.json``scripts/deploy/production-stdb-publish.sh``scripts/deploy/production-runtime-writer-identity-rotate.mjs``scripts/deploy/maintenance-on.sh``scripts/deploy/maintenance-off.sh``scripts/spacetime-migration-common.mjs``scripts/spacetime-maintain-external-generation-jobs.mjs``scripts/spacetime-clean-editor-image-asset-kind.mjs``scripts/spacetime-normalize-editor-character-actions.mjs``scripts/spacetime-migrate-editor-canvas-layout.mjs``scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json``migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret Filepublish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secretID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
三个 SCM Jenkinsfile 将 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 默认固定为 `genarrative-spacetime-bootstrap-secret-dev-file`。Secret File 的原文只存在于 Jenkins Credentialscredential ID、参数默认值和定时 / 发布行为以仓库 Jenkinsfile 为事实源,不能只改 Job UI,因为 Declarative Pipeline 下一次载入会重写参数定义。旧 Secret Text `genarrative-spacetime-bootstrap-secret-dev` 继续保留给 Database Import / Export,不得原地改类型或删除。
@@ -79,7 +79,7 @@
精选内容只使用用户从账号级素材库主动提交、后台审核通过且展示状态开启的 `editor_showcase_asset` 快照。新生成素材不会默认公开,审核通过后也不会自动展示;运营可在后台按前台具体 Tab 设置分类(角色、UI、音乐、美宣)并开启展示,未设置分类的素材不会隐藏,会进入前台“全部”。旧 `editor_project_resource.public_showcase_enabled` 只保留历史兼容,不再作为 `/creation` 精选事实源。账号级 `editor_asset` 仍是素材库私有事实;只有 `sourceType="generated"`、有媒体内容、提交审核并通过的素材才可进入精选,上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。审核通过时按生成成本返还 50% 泥点,返还流水使用确定性 `editor-showcase-refund:{showcaseId}` 保证幂等;批准状态、钱包返还流水和 `refund_completed_at` 必须在同一个 SpacetimeDB 事务中完成,任一步失败都保持待审核,历史已通过但未返还记录重复批准时按同一流水补齐且不得重复入账。已通过、已开启展示且返还完成的精选快照必须按同 owner 的精确 `assetObjectId``objectKey` 派生匿名读取授权,不能放开整个 `generated-*` 前缀。公开 BFF 必须返回作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。
精选素材流通过 `GET /api/editor/showcase/resources` 按通过审核时间 / `showcaseId` 倒序 cursor 分页读取,每页最多 36 条响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有列表,而不是固定只展示首屏数量。追加、筛选、分类和容器/卡片尺寸变化都必须按当前完整 DOM 顺序重算循环分列;位置未完整时保留正常文档流 fallback,不能让分页 sentinel 因容器短暂零高提前触发。响应可以额外携带后台配置的固定活动卡,用于在列表首位展示运营精选;活动卡作为第一张进入第一列,普通素材从第二张继续同一循环列序。活动卡图片继续保持 private:后台直传 OSS 后必须先确认正式 `asset_object`,公开读取只由已启用 `global` 配置对活动卡专用目录中的当前 exact `imageObjectKey` 派生,禁用或替换图片后旧 key 不再可读;历史缺 metadata 的当前活动卡可通过同一 exact 配置授权恢复,但新上传不能跳过 confirm。
精选素材流通过 `GET /api/editor/showcase/resources` 按通过审核时间 / `showcaseId` 倒序 cursor 分页读取,每页最多 36 条。该 GET 保持公开,但接受可选 Bearer:匿名响应中每项固定 `viewerLiked=false`;有效登录态由 BFF 从已验证 claims 派生用户 ID,并在同一 SpacetimeDB 事务快照中返回该用户的权威 `viewerLiked` 与全局 `likeCount`。只要请求携带无效或过期 Bearer 就返回 `401`,个性化读取失败也显式失败,禁止静默降级成“未点赞”。响应统一发送 `Cache-Control: private, no-store``Vary: Authorization`响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有列表,而不是固定只展示首屏数量。追加、筛选、分类和容器/卡片尺寸变化都必须按当前完整 DOM 顺序重算循环分列;位置未完整时保留正常文档流 fallback,不能让分页 sentinel 因容器短暂零高提前触发。响应可以额外携带后台配置的固定活动卡,用于在列表首位展示运营精选;活动卡作为第一张进入第一列,普通素材从第二张继续同一循环列序。活动卡图片继续保持 private:后台直传 OSS 后必须先确认正式 `asset_object`,公开读取只由已启用 `global` 配置对活动卡专用目录中的当前 exact `imageObjectKey` 派生,禁用或替换图片后旧 key 不再可读;历史缺 metadata 的当前活动卡可通过同一 exact 配置授权恢复,但新上传不能跳过 confirm。
Tab
@@ -116,7 +116,7 @@ Tab
数据来源:
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_showcase_asset` 快照,快照包含 `showcaseId``assetId`、审核状态、展示状态、`showcaseCategory`点赞数、生成成本、返还泥点、`authorDisplayName` / `display_name``authorPublicUserCode` / 陶泥号用于展示;前端默认进入“全部”,展示所有后端已经筛过的公开结果;角色、UI、音乐或美宣 Tab 只展示对应 `showcaseCategory` 的素材。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_showcase_asset` 快照,快照包含 `showcaseId``assetId`、审核状态、展示状态、`showcaseCategory``likeCount`、当前浏览者的 `viewerLiked`、生成成本、返还泥点、`authorDisplayName` / `display_name``authorPublicUserCode` / 陶泥号用于展示;前端默认进入“全部”,展示所有后端已经筛过的公开结果;角色、UI、音乐或美宣 Tab 只展示对应 `showcaseCategory` 的素材。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`
- 全部、角色、UI、音乐、音效、视频和美宣都必须来自用户素材库中已提交并通过审核的生成素材;不要求素材当前仍保留在某个项目资源中,已通过审核的快照在原素材删除后仍可保留公开展示。
- 未登录用户也可读取公开精选;当没有公开画布生成资源时,不再用公开作品图片补充,只显示简洁空态。
- 上传素材、公开作品图片、mock 资源和假组合都不进入精选。
@@ -133,7 +133,7 @@ Tab
- 最近项目继续使用编辑器项目接口:`listEditorProjects``createEditorProject``/editor/canvas?projectid=xxx`
- 最近项目和项目页打开画布时,浏览器 history 只保留带 `projectid` 的最终画布路由;新建画布引导使用 `guide=toolbar` 一次性 query,主站九宫格直达生成器使用 `tool=<intent>` 一次性 query,画布消费后通过 `history.replaceState` 清理这两个参数。
- 项目封面逻辑复用 `/project` 项目卡已有的画布中心缩略图算法。
- `陶泥儿精选` 读取后端公开精选接口返回的审核通过素材快照;公开事实以后端 `editor_showcase_asset.review_status``display_enabled` 为准,`showcase_category` 只用于前端具体 Tab 归类,前端只做展示组合、Tab 归类、点赞交互和提交审核的乐观状态回滚
- `陶泥儿精选` 读取后端公开精选接口返回的审核通过素材快照;公开事实以后端 `editor_showcase_asset.review_status``display_enabled` 为准,`showcase_category` 只用于前端具体 Tab 归类。点赞不做乐观图标或计数更新:按钮 pending 期间保持原状态,成功后应用服务端返回的 `viewerLiked + likeCount`,失败时保留原状态并在列表上方给出可访问短错误。登录、退出和账号切换都重载首屏并清空分页 / pending / 错误;首屏、分页和点赞回包必须按账号与请求代际丢弃旧响应。已知用户仍在恢复鉴权时不得先发匿名请求;不要求跨 Tab 实时同步
- `/creation/<play>` 的玩法工作台、草稿、生成页、结果页、发布、运行态和作品架链路保持原状。
## 路由与导航
@@ -18,7 +18,7 @@
- `快速编辑`
`角色动画生成面板` 同步纳入本次生成类面板交互统一:点击角色图只聚焦图层,不自动弹出底部重绘或角色动画面板;点击 `生成动画` 后像新建图片一样创建 `角色动作` 画布占位,面板跟随占位底部,参考图首行、单文本无边界、参数按钮向上弹出、生成按钮明确展示泥点。
`快速编辑` 由选中图片后的浮动工具栏显式打开,图片类素材统一进入框选区域 + 单提示词 + 比例 / 尺寸 + 模型选择的修改面板,不再恢复原来源生成器,也不展示额外参考图。`icon` `icon-spritesheet` 图标类素材不支持快速编辑,`icon-spec` 图标规范仍按普通图片支持快速编辑。
`快速编辑` 由选中图片后的浮动工具栏显式打开,图片类素材统一进入框选区域 + 单提示词 + 比例 / 尺寸 + 模型选择的修改面板,不再恢复原来源生成器,也不展示额外参考图。单个拆分 `icon` 不支持快速编辑,完整 `icon-spritesheet` `icon-spec` 图标规范仍按普通图片支持快速编辑。
## 统一布局
@@ -42,7 +42,7 @@
9. 生成规范下的角色规范、图标规范和自定义规范都使用同一生成类 shell:首行参考图区域、中央字段区、底部生成按钮区,不再出现缺首行参考区或单独 footer 样式。
10. 图标规范只使用 `specType="icon"`,历史 `specType="ui"` 快照在恢复边界迁移为 `icon`。表单字段使用 `playSetting / artStyle`,界面标题继续使用「玩法设定 / 美术风格」。两项初始为空且必填,客户端提交前统一 trim 并拒绝空白值;每项独立支持一键优化、处理中锁定自身、成功后单次撤销,操作行最右侧按 Unicode 字符实时显示 `当前数/200`。撤销必须恢复优化前的原始输入(包括首尾空白);手工编辑后立即清除该字段已经失效的撤销快照,失败只保留当前文本与仍然有效的旧撤销快照。LLM 返回空文本、超长文本、Markdown / 结构化内容,或 finish reason 明确表示截断、过滤、失败时,后续有界重试必须携带上次无效输出和对应修正要求,不能把未完成前缀当作成功结果。优化请求必须绑定发起时的生成对象 ID 和请求代次;活动对象身份只在 React effect 提交后更新,并在 cleanup 中失效,丢弃的并发 render 不得改变请求归属;对象切换或新请求取代旧请求后,旧成功或失败结果都不得更新当前面板。任一项处理中或任一项为空时禁用生成。字段标题使用真实 label 关联 textarea,不得把优化 / 撤销按钮包进 label。控件继续使用平台默认样式,不新增图标规范专属 CSS。
11. 图标规范最终生成改走 `POST /api/editor/icon-specs/generations`。前端只提交业务字段和统一参考图 / 项目完成包络,不拼最终 prompt,不提交 `kind / assetKind / ExtraParam`;可选参考图字段为 `referenceId`,只允许当前 owner 的项目资源 ID 或素材 ID。后端固定以 `kind=spec / assetKind=icon-spec / gpt-image-2 / 16:9·2K` 执行图片生成。inline 路径校验业务字段和 `referenceId` 后,补全 `ExtraParam` 与最终 prompt 并交给共享图片生成执行器;queue 路径在预校验后按 `gpt-image-2 / 2K` 运行时定价冻结价格,再以独立 `editor_icon_spec_generation` job kind 入队原始业务载荷。worker 使用入队价格和当前 claim attempt 的计费上下文,重新解析载荷、校验当前 owner 与引用事实,再补全 `ExtraParam` 并调用同一共享图片生成执行器,避免排队期间状态变化产生 TOCTOU。
12. 图片快速编辑不展示额外参考图入口;原图或绘制了红框和序号的标注图始终作为 `/api/editor/images/edits``sourceImageSrc` 直接提交,不作为 `referenceImageSrcs`
12. 图片快速编辑不展示用户可添加的额外参考图入口;原图已登记的 `resourceId``sourceAssetId` 始终作为 `/api/editor/images/edits``sourceReferenceId`,绘制了红框和序号的标注图上传后只作为辅助 `referenceImageSrcs`。从生成型图片编辑 V2 快照恢复且在面板中可见的附加参考图也继续作为辅助引用,不得替代主来源身份
13. 快速编辑打开后,画布视口应调整到原图完整展示,且面板位于原图下方并不遮挡原图;原图右侧显示竖向框选工具,支持矩形、椭圆和画笔自由框选。快速编辑进入时不默认启用框选工具,点击工具后出现选中态并保持高亮,再点同一工具取消启用;红色圈选框使用细描边。每完成一次框选,红色圈选框按完成顺序标注 `1 / 2 / 3...`,并在快速编辑提示词中追加一行 `对N号红色圈选框里的内容做以下修改:`
## 参数交互
@@ -113,7 +113,7 @@
- 占位图的生成器名称 / 原始尺寸、图片图层右上角素材类型标签、查看信息按钮和悬浮尺寸标签都按 viewport 反向缩放,画布缩小时保持屏幕可读尺寸。
- 查看信息按钮固定使用圆形 `i` 图标,不使用中括号、花括号或文本符号样式。
- 视频 / 角色 / 角色动作 / 音效 / 背景音乐待生成占位的角标同样按 viewport 反向缩放,不随画布缩放变小。
- 已生成角色图、角色动作图或其它生成结果图被点击时只选中图层并收起已有生成输入框;重绘、快速编辑和生成动画面板必须由对应工具栏按钮或右键菜单显式打开。
- 已生成角色图、角色动作图或其它生成结果图被点击时只选中图层并收起已有生成输入框;重绘、快速编辑和生成动画面板必须由对应工具栏按钮或右键菜单显式打开。快速编辑使用统一正向白名单,只支持普通静态图片、角色图、规范图、完整图标图集、UI 设计图、宣发图和视频;单个拆分图标(backend reject)、角色动作 / 序列帧、音效与背景音乐不显示快速编辑入口,提交层也必须拒绝绕过入口的调用。
- 角色图层打开“生成动作”后再点击“改造”,必须重新打开角色形象生成器;动作生成对话框只把角色图层作为输入来源,不得被识别为该角色图层自身的来源生成器。
- 角色动作结果图层点击“改造”时,V2 必须通过 `references[id="source"]` 找回原角色图层,legacy 数据才允许以 `sourceResourceId` 回退;关联原角色已不存在时应显示明确提示,不得无响应或降级成图片生成器。
- `改造` 覆盖图片、规范、角色、图标、UI、宣发、游戏场景、视频、音效、背景音乐、角色动作和生成型图片编辑。有效 V2 只按 `action + fields[].id + references[].id` 恢复,引用只匹配当前画布图层;面板直接上传引用和已移出画布的引用不恢复。V2 不新增后续版本,读取时统一经 action 级 runtime decoder 原地收紧:已存在但未知、非法、已下线或与当前模型能力不兼容的参数统一回落到该 action 当前默认值,历史 Veo 也回落到当前默认视频模型;图片比例 / 尺寸按回落后的模型联动校验,角色动作帧数 / 时长按完整档位成对校验。发生参数回落时显示明确告警,再次提交和新快照只使用规范值并继续保存为 `version: 2`。服务端以实际媒体时长覆盖 V2 配方时必须保留 `fields[id="durationSeconds"]`,并写入归一后的有限数值,不能改写为无 `id` 的 legacy 展示字符串。可重新选择的引用缺失时打开面板、留空槽位并提示,提交门禁继续校验必填槽位;必须依赖原 `source` 图层才能构造面板的 action 也始终按 capability 保留改造入口,source 缺失时点击后显示不可替换原因并拒绝改造,运行期间来源变化时仍必须复检并拒绝。有效 V2 不得因引用缺失降级到 legacy。生成型图片编辑 V2 中已持久化的附加 `reference` 应恢复到可见参考槽,并让再次提交的模型、比例、尺寸、像素尺寸、参考图和新快照保持一致;普通快速编辑仍不得提交未展示的隐藏参考图。视频快速编辑必须把实际送入请求的源视频同步保存为 `references[id="videoReference"]`,不能只依赖 `sourceResourceId`;视频 V2 同步保存并恢复 `webSearchEnabled`。历史对话框和 legacy 数据保留 `assetKind/mediaType`、标题别名、资源尺寸 / 模型 / 时长 / `sourceResourceId` 回退,并对默认值恢复显示告警。V2 结构水合必须完整保留 `version/action`、字段与引用 `id`、有限数字、布尔值和无标签引用;一旦出现 `version``action` 却不满足 V2 合同,必须失败关闭,禁止降级成 legacy。
@@ -191,10 +191,10 @@
- `生成音乐` 选项面板出现在音乐按钮上方,不再固定在底栏中间。
- 规范面板比图片生成面板更紧凑,字段间距和输入高度更小,但外层 shell、首行参考图和底部按钮区必须继续对齐生成图片 / 生成角色 / 生成视频。
- 生成规范类图片底部展示禁用态参数按钮 `16:9·2K``gpt-image-2`,视觉对齐可编辑面板的比例 / 尺寸 / 模型按钮;提交参数也固定为这三项,不出现可展开选项。
- 图片快速编辑底部左侧展示比例 / 尺寸组合选择,右侧展示模型选择和 `修改` 按钮;原图或红框序号标注图作为 `sourceImageSrc` 直接编辑,不展示额外参考图条。图标与图集素材不展示快速编辑入口,图标规范仍可快速编辑。
- 图片快速编辑底部左侧展示比例 / 尺寸组合选择,右侧展示模型选择和 `修改` 按钮;原图已登记业务 ID 作为 `sourceReferenceId`,红框序号标注图作为内部辅助 `referenceImageSrcs`,不展示额外参考图条。单个拆分图标不展示快速编辑入口,完整图标图集与图标规范仍可快速编辑。
- 快速编辑打开后画布自动缩放平移到原图完整展示,并让面板位于原图下方且不遮挡原图;原图右侧出现竖向矩形 / 椭圆 / 画笔自由框选按钮。进入快速编辑不默认启用框选,点击工具启用并保持高亮,再点同一工具取消;完成框选后画布红色细框显示连续序号,输入框同步追加 `对N号红色圈选框里的内容做以下修改:`
- 快速编辑提交前保留提示词里对原图的 `原图``当前图片``当前图``图1` 引用,不再改写成 `图N`
- 普通快速编辑提交给后端时只把原图或已绘制红框和序号的标注图作为 `sourceImageSrc`,不提交隐藏的 `referenceImageSrcs`;从生成型图片编辑 V2 快照恢复且在面板中可见的附加参考图除外
- 快速编辑提交给后端时主来源只使用原图已登记的 `sourceReferenceId`;已绘制红框和序号的标注图,以及从生成型图片编辑 V2 快照恢复且在面板中可见的附加参考图,均作为辅助 `referenceImageSrcs`,不得冒充目标图层主来源
- 生成中的占位图聚焦后可用 `Delete` / `Backspace` 删除;删除后异步结果不再落回画布,也不显示额外删除 UI。
- 快速编辑不创建生成中占位图;提交后当前面板显示修改中,异步结果只允许回填到源图。
- 生成视频 / 角色形象 / 角色动作 / 音效 / 背景音乐新建后,画布占位空白样式和右上角标签均与对应生成类型一致,不再统一使用图片占位 icon。