From cd78077e95e6e67065142f9e45a5e68d3f141dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 8 Aug 2026 12:19:59 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=9C=BA=E6=99=AF=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E5=BF=AB=E9=80=9F=E7=BC=96=E8=BE=91=E7=99=BD=E5=90=8D?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同步前端与后端场景快速编辑准入 锁定用户素材标签覆盖的双端白名单 补充新增素材类型的快速编辑检查点 更新场景与画布契约文档 --- .../shared-memory/decision-log.md | 5 +++-- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 4 ++-- ...方案】图片画布游戏场景生成链路-2026-08-04.md | 2 ++ ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 3 +++ .../crates/api-server/src/editor_project.rs | 2 ++ .../src/editor_project_storage.rs | 9 ++++++++ .../ImageCanvasGenerationModel.test.ts | 6 +++++ .../ImageCanvasGenerationModel.ts | 1 + .../ImageCanvasWorldView.test.tsx | 22 +++++++++++++++++++ 9 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 58769e9e6..5d8f5962c 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6524,7 +6524,7 @@ - local 状态:`local-*` 只是 ID 形状,不能直接解释为“素材仍在保存”。新上传 / 新生成素材是否 pending 取资源登记在途状态;严格满足兼容谓词的历史自包含本地角色动作序列是持久化终态,不得误报等待。若当前版本尚不能复制这类序列,以准确原因失败关闭;既非 pending 又不满足历史谓词的 unresolved local 图层也失败关闭,但不得承诺稍后一定自动恢复。layout PATCH pending 不参与资源登记判断,系统剪贴板图片导入不受影响。 - schema 与迁移:在现有 `EditorCanvasLayer` 结构体末尾追加 `#[default(None::)] asset_kind_override: Option`,不删除、改名、重排或改类型。legacy 图层类型与资源默认相同则迁移为 `None`,不同则迁移为 override;资源无默认值时只有全部引用图层显式同值才补资源默认,否则保留各自 override;自包含历史序列的显式类型迁入 override,不伪造资源。同步 `migration.rs`、表目录 / 数据契约、生成 bindings、HTTP DTO 与结构化 canonical hash,并运行 `npm run spacetime:generate`、`npm run check:spacetime-schema`。 - 并发边界:未登记图层被禁止复制后,不再按临时资源 ID 合并项目资源创建请求,也不再用一次响应批量改写共享临时 ID。每个合法新增图层保留自己的响应快照与回调;layout PATCH 的串行 latest-wins 队列、共享资源的多布局引用和 session 资源快照按 `resourceId` 去重继续保留,它们与资源创建 single-flight 是不同机制。 -- 媒体兼容边界:override 只允许在资源默认类型的同一媒体族内变化。动作、视频各自独立成族,`audio/sound-effect/background-music` 同属音频族,其余类型与空默认值同属图片族。前端菜单禁用跨族标签且更新入口重复校验;后端对每个结构化图层按资源完整校验,跨族值清空后回退资源默认类型,不因该兼容错误拒绝整个保存。 +- 媒体兼容边界:override 只允许在资源默认类型的同一媒体族内变化。动作、视频各自独立成族,`audio/sound-effect/background-music` 同属音频族,其余类型与空默认值同属图片族,`scene` 明确属于图片族。前端可选标签与 SpacetimeDB `EDITOR_CANVAS_ASSET_KINDS` 必须同步包含现役用户可覆盖类型;前端菜单禁用跨族标签且更新入口重复校验;后端对每个结构化图层按资源完整校验,跨族值清空后回退资源默认类型,不因该兼容错误拒绝整个保存。 - 历史恢复:客户端读取到已持久化的跨族 override 时必须保留图层、回退资源默认类型、显示明确提示并自动提交清理后的布局;不得再用 `hydrateLayer() -> null -> filter(Boolean)` 静默隐藏持久层仍存在的图层。修复保存只替换命中图层的规范化布局项,其他尚不能 hydrate 的历史项原样保留,避免修复一个标签时顺带删除无关数据。 ## 2026-08-03 Agent Runtime 原生工具合同本地失败关闭 @@ -6590,10 +6590,11 @@ ## 2026-08-04 图片画布快速编辑改用统一正向白名单(2026-08-05 修订) - 背景:画布生成结果统一显示快速编辑,但部分角色动作 / 序列帧和音频结果会进入不受支持的图片编辑链路;不同入口各自判断时也容易继续漂移。 -- 决策:快速编辑只支持普通静态图片、角色图、规范图、完整图标图集、UI 设计图、宣发图和视频。单个拆分图标、角色动作 / 序列帧、音效与背景音乐不支持;新增媒体或素材类型默认不开放。浮动工具栏、图层右键菜单、独立图片菜单、打开面板入口和提交门禁统一调用同一个正向白名单;后端图片编辑 BFF 基于目标图层的有效素材类型与媒体类型执行同一正向门禁,视频快速编辑只走视频生成接口。 +- 决策:快速编辑只支持普通静态图片、角色图、规范图、完整图标图集、图标规范、UI 设计图、宣发图、游戏场景图和视频。单个拆分图标、角色动作 / 序列帧、音效与背景音乐不支持;新增媒体或素材类型默认不开放。浮动工具栏、图层右键菜单、独立图片菜单、打开面板入口和提交门禁统一调用同一个正向白名单;后端图片编辑 BFF 基于目标图层的有效素材类型与媒体类型执行同一正向门禁,视频快速编辑只走视频生成接口。 - 边界:角色动作继续通过对应的动作生成链路处理,不再把当前帧当作可快速编辑图片。 - 2026-08-06 修订:画布 Agent 的 `edit_image` 只接受图片输入,新任务以 `assetKind=null` 表示普通静态图片,不再使用 synthetic `editor_agent_edit_image`。worker 仅按服务端生成的 `editor-agent:` dedupe namespace 识别并归一历史排队 payload;普通调用伪造旧值继续被拒绝。已持久化资源中的旧值只有在后端从真实目标图层 / 项目资源解析后才兼容为空类型,避免历史 Agent 结果失去快速编辑能力,同时不扩大请求白名单。 - 2026-08-07 修订:站内与 External v1 图片编辑请求统一只接受必填 `sourceReferenceId`,且该值必须是当前账号已登记的项目资源 ID 或素材 ID;objectKey、URL、Data URL、Blob URL 以及旧 `sourceImageSrc/sourceResourceId/assetKind` 字段全部返回 400,不提供兼容别名。后端用共享窄查询分别按两张表主键定点解析,双表同 ID、未命中、跨账号、对象缺失或越权均失败关闭;权威类型完全来自业务记录,只允许普通静态图片、规范图、角色图、完整图标图集、图标规范、宣发图和 UI 设计图。请求带 `targetLayerId` 时必须同时带 `projectId`,来源与目标优先比较 `assetObjectId`,任一方缺失才比较 canonical `(bucket, objectKey)`,且来源默认类型必须与目标资源默认类型一致;最终类型取目标覆盖值或目标资源类型。HTTP 入队写入版本化服务端解析快照,worker 执行前按同一业务 ID 再次定点解析,身份或类型漂移即失败关闭。旧任务只把已有资源 ID 或旧来源字符串本身当业务 ID 迁移,绝不按 objectKey 反查。Canvas Agent 必须从 `ImageMetadata.reference_id` 取主来源;红框标注上传图只作为辅助 `referenceImageSrcs`,不能冒充被编辑资源。仅以素材 ID 编辑时,队列审计与 `generationInputs.references` 保留素材 ID,不伪造项目资源关系。 +- 2026-08-08 修订:`scene` 是单张静态图片素材,加入前端快速编辑正向白名单和 api-server 权威来源白名单;编辑结果继续保留 `scene`。用户标签覆盖侧的前端菜单与 SpacetimeDB 结构化布局白名单也必须显式覆盖 `scene`。通用图片生成接口仍拒绝 `scene`,避免绕过结构化场景生成契约。 - 验证:模型测试覆盖允许与拒绝类型,工具栏和两类右键菜单测试覆盖单个拆分图标、角色动作及音频不展示,提交工作流测试覆盖单个拆分图标和角色动作绕过入口时仍拒绝;后端表驱动测试覆盖全部现役素材 / 媒体类型与未知类型,锁定图片编辑端点失败关闭。 - 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts`、`ImageCanvasSelectedLayerToolbarView.tsx`、`ImageCanvasContextMenusView.tsx`、`useImageCanvasGenerationWorkflow.ts`、`useImageCanvasGenerationSubmissionWorkflow.ts`。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index cf3a71b58..e02160321 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -14,12 +14,12 @@ - 中央画布支持背景拖拽平移、滚轮二维平移、`Ctrl / Cmd + 滚轮` 缩放、缩放百分比菜单、显示所有元素和固定比例缩放。 - 画布左下角提供 Lovart 式状态控件:背景色圆点、素材 / 图层入口、小地图开关;小地图显示图层缩略分布和当前视口框,点击小地图执行显示所有元素。 - 画布 chrome 的边框、hover / 选中态、吸附 / 框选参考线、生成类按钮和通用 active 控件使用陶泥儿暖色主题(以 `--platform-accent`、陶土橙主按钮和深棕文字为基准),不得回退为黑色或蓝色主题;元素类型自身的识别色可继续保留。 -- 画布中的图片可展示、悬浮显示图片 Resolution 尺寸与边框,点击后在图片上方显示浮动工具栏;浮动工具栏只保留当前可执行的编辑动作,不放调整 / 复制 / 删除 / 查看信息占位按钮。图片右上角素材类型标签、图片信息角标和悬浮尺寸标签在画布缩小时必须按 viewport 反向缩放,保持屏幕可读尺寸。素材类型采用双层语义:`editor_project_resource.asset_kind` 是跨布局共享的资源默认类型,`editor_canvas_layer.asset_kind_override` 是当前布局实例可空的覆盖值,显示与功能判断统一使用 `effectiveAssetKind = assetKindOverride ?? resource.assetKind`;两层都为空时显示 `未知`。点击标签只修改或清除当前图层的 `assetKindOverride`,不能创建新资源、替换 `resourceId` 或修改其他引用同一资源的图层;清除覆盖后立即恢复继承资源默认类型,也不能触发图层选择 / 拖拽事件。标签覆盖必须与资源默认类型属于同一媒体族:`character-animation` 独立为动作族,`video` 独立为视频族,`audio/sound-effect/background-music` 属于音频族,其余类型和空默认值属于图片族;标签菜单禁用跨族选项,更新回调再次校验。加载到历史跨族 override 时不得静默过滤图层,必须清除内存 override、按资源默认类型恢复图层、向用户提示并排队修复保存。图片信息角标使用圆形 `i` 图标,不使用中括号或花括号样式。图片不再维护独立展示 `Size` 字段,画布显示宽高统一取 `originalWidth/originalHeight`(图片信息中的 `Resolution`)。 +- 画布中的图片可展示、悬浮显示图片 Resolution 尺寸与边框,点击后在图片上方显示浮动工具栏;浮动工具栏只保留当前可执行的编辑动作,不放调整 / 复制 / 删除 / 查看信息占位按钮。图片右上角素材类型标签、图片信息角标和悬浮尺寸标签在画布缩小时必须按 viewport 反向缩放,保持屏幕可读尺寸。素材类型采用双层语义:`editor_project_resource.asset_kind` 是跨布局共享的资源默认类型,`editor_canvas_layer.asset_kind_override` 是当前布局实例可空的覆盖值,显示与功能判断统一使用 `effectiveAssetKind = assetKindOverride ?? resource.assetKind`;两层都为空时显示 `未知`。点击标签只修改或清除当前图层的 `assetKindOverride`,不能创建新资源、替换 `resourceId` 或修改其他引用同一资源的图层;清除覆盖后立即恢复继承资源默认类型,也不能触发图层选择 / 拖拽事件。标签覆盖必须与资源默认类型属于同一媒体族:`character-animation` 独立为动作族,`video` 独立为视频族,`audio/sound-effect/background-music` 属于音频族,其余类型和空默认值属于图片族;`scene` 属于图片族,并且必须同时存在于前端可选标签与 SpacetimeDB 结构化布局白名单。标签菜单禁用跨族选项,更新回调再次校验。加载到历史跨族 override 时不得静默过滤图层,必须清除内存 override、按资源默认类型恢复图层、向用户提示并排队修复保存。图片信息角标使用圆形 `i` 图标,不使用中括号或花括号样式。图片不再维护独立展示 `Size` 字段,画布显示宽高统一取 `originalWidth/originalHeight`(图片信息中的 `Resolution`)。 - 默认工具为选择模式;底部工具栏采用 AI 画布工作流工具组:选择、抓手、上传、生成图片、生成视频、生成音乐、生成规范、生成角色形象、生成图标素材、生成 UI 设计图。底部栏不再展示文字工具、形状标注工具和导出工具;上传与生成图片之间、生成音乐与生成规范之间各有一个半图标高度分割线。 - 鼠标中键拖拽始终平移画布;长按 Space 临时进入抓手模式,松开后恢复原工具。 - 图片拖拽时显示水平 / 垂直吸附参考线,吸附到其它图层、生成占位框或画板的边缘与中心线;当移动元素接近两个同轴元素形成的等距位置时,支持横向或纵向等距吸附。 - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片,并提供“复制信息”复制当前可见字段。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 -- 快速编辑使用统一正向白名单:只支持普通静态图片、角色图、规范图、完整图标图集、图标规范、UI 设计图、宣发图和视频;单个拆分图标、角色动作 / 序列帧、音效与背景音乐不支持。浮动工具栏、图层右键菜单、独立图片菜单、打开面板入口和提交门禁必须复用同一白名单,新增媒体或素材类型默认不开放。图片编辑请求只提交必填 `sourceReferenceId`,取正式 `resourceId ?? sourceAssetId`;没有正式业务 ID 的本地图层、生成对话临时图层或未登记上传图必须先走既有资源登记,否则前端失败关闭。objectKey、URL、Data URL、Blob URL 不得作为主来源身份,旧 `sourceImageSrc/sourceResourceId/assetKind` 也不得发送。带红框的快速编辑仍以原图业务 ID 为主来源,上传后的编号标注图只放入辅助 `referenceImageSrcs`。`/api/editor/images/edits` 入队前通过当前账号窄查询解析来源对象与权威类型;带 `targetLayerId` 时必须同时带 `projectId`,目标图层须关联有效项目资源,双方对象和默认类型必须一致,最终有效类型取目标覆盖值或目标资源类型。视频快速编辑只能走视频生成接口,角色动作只能走对应动作生成链路。画布 Agent 的 `edit_image` 必须从 `ImageMetadata.reference_id` 获取项目资源 ID 或素材 ID,缺失时拒绝创建任务,不得用 `image_data_key` 或 objectKey 代替。 +- 快速编辑使用统一正向白名单:只支持普通静态图片、角色图、规范图、完整图标图集、图标规范、UI 设计图、宣发图、游戏场景图和视频;单个拆分图标、角色动作 / 序列帧、音效与背景音乐不支持。浮动工具栏、图层右键菜单、独立图片菜单、打开面板入口和提交门禁必须复用同一白名单,新增媒体或素材类型默认不开放。图片编辑请求只提交必填 `sourceReferenceId`,取正式 `resourceId ?? sourceAssetId`;没有正式业务 ID 的本地图层、生成对话临时图层或未登记上传图必须先走既有资源登记,否则前端失败关闭。objectKey、URL、Data URL、Blob URL 不得作为主来源身份,旧 `sourceImageSrc/sourceResourceId/assetKind` 也不得发送。带红框的快速编辑仍以原图业务 ID 为主来源,上传后的编号标注图只放入辅助 `referenceImageSrcs`。`/api/editor/images/edits` 入队前通过当前账号窄查询解析来源对象与权威类型;带 `targetLayerId` 时必须同时带 `projectId`,目标图层须关联有效项目资源,双方对象和默认类型必须一致,最终有效类型取目标覆盖值或目标资源类型。视频快速编辑只能走视频生成接口,角色动作只能走对应动作生成链路。画布 Agent 的 `edit_image` 必须从 `ImageMetadata.reference_id` 获取项目资源 ID 或素材 ID,缺失时拒绝创建任务,不得用 `image_data_key` 或 objectKey 代替。 - 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。重绘 / 改造输入框只允许从 `generationInputs.fields` 中恢复用户可见输入快照,例如普通生成提示词、视频描述、音效 `prompt`、背景音乐 `gpt_description_prompt`、角色设定、UI 用户输入、图标素材描述、规范表单和宣发素材字段;禁止回退展示资源 `prompt` / `actualPrompt` 中可能存在的后端拼接 Prompt、固定生成模板或模型默认提示词。当前 BGM 是字段语义上的例外:其新记录中的 `prompt` 与 `actualPrompt` 必须等于输入框已经写回的 canonical `gpt_description_prompt`,不得包含隐藏内容;但改造输入仍只从 `generationInputs.fields` 恢复,不能因此放宽为从资源审计字段回退。没有用户输入快照的旧图层打开改造时保持空输入,等待用户重新填写。 - 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑统一打开框选区域 + 单提示词 + 模型选择面板,默认沿用原图模型,不展示参考图或比例 / 尺寸控件;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型;两类面板不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`,由后端自动决策具体抠图背景色,`anime-seg` 作为内部保留能力不在用户界面暴露。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端优先复用当前图层 objectKey;尚未登记的本地图片先上传 OSS,再把 objectKey 交给同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;普通图片、角色、图标图集、UI 设计图及其重绘 / 改造入口必须在比例或清晰度恢复、切换时同步把占位框 `width/height/originalWidth/originalHeight` 更新为目标像素尺寸,生成中不得继续显示默认 1K 框;UI 素材提取的 1K / 2K 图集占位和旧图片修改入口也分别使用本次目标尺寸与源图真实尺寸。待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片解析为已上传的 objectKey 或资源 ID;浏览器临时图片需先上传 OSS;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作的 `生成动画` 仍固定使用 `seedance2.0-fast` 动作 / 视频模型,角色动作素材不提供 `快速编辑`,只能通过角色动作生成链路继续编辑。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 - 图片画布抠图统一通过唯一、只监听 loopback 的 `bgfilter-worker` 调用 BgFilter provider。手动去除背景面向用户任意图片,仍走登录态同源 BFF `POST /api/editor/images/background-removals` 和外部生成队列;API 在入队前拒绝 `data:` / `blob:` 内联媒体,父流程将稳定引用解析为当前账号已登记且归属已校验的私有 OSS object key,并在同一轮账号项目 / 素材快照读取中同时恢复用户可见源模型,禁止为 object key、所有权和源模型分别重复拉取全量快照;随后只通过一次内部 HTTP RPC 传递 object key、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和固定的 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传源图字节、签名 URL、`file` 或 `screen_color`。子 worker 在每次真实 provider attempt 前签发 600 秒 URL,承担默认 `Q=2048` admission 保险丝、provider 并发 `N=16`、严格最多两次顺序 attempt、响应字节与图片尺寸校验,并把成功图片作为内部 HTTP 二进制 body 直接返回;父流程同步等待该响应且不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`;attempt 按 `N × est × 2`、调用预算按 `2 × attempt + 1s` 派生,冻结 `est=5000ms` 时分别为 `160s / 321s`。complex 的真实 provider 失败会累计并打开自身熔断,但与 flat 状态隔离;complex 任意失败或熔断仍直接返回父流程失败,不接入阿里云 / 本地键色降级。provider 配置继续统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL` 和 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,父子共同使用 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY` 与 `GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS` 派生预算;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 provider token 兼容别名;内部调用另使用 `GENARRATIVE_BGFILTER_WORKER_BASE_URL` 和独立内部 Token。所有令牌只在服务端注入,前端不持有令牌。成功字节返回父流程后,仍由父流程完成最终处理、OSS / asset object 持久化、结果图层与最新项目快照写回;接口只向前端返回 `queueState`,有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端。 diff --git a/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md b/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md index 539daf15c..e5c543567 100644 --- a/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md +++ b/docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md @@ -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 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index c1c0fef74..3d6e649a3 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -463,6 +463,8 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 | 后台契约 | `server-rs/crates/shared-contracts/src/admin.rs`、`api-server/src/admin.rs` | 后台素材查询或精选审核需要展示该类别时返回完整正式字段,不能只返回封面或首帧 | | TypeScript 客户端类型 | `src/services/image-editor/editorProjectClient.ts`、`apps/admin-web/src/api/adminApiTypes.ts` | 接收后端 camelCase 字段,不定义第二套业务真相 | | 画布 layer / layout 映射 | `src/components/image-editor/` | 新类别能进入画布时贯通资源加载、素材点击 / 拖放、保存恢复、复制、撤销、删除和导出;layout 只保留恢复副本 | +| 用户标签覆盖白名单 | `src/components/image-editor/ImageCanvasWorldView.tsx`、`server-rs/crates/spacetime-module/src/editor_project_storage.rs` | 显式决定用户能否把同媒体族图层覆盖为该类别;允许时同步前端 `CANVAS_ASSET_KIND_TAG_OPTIONS` 与后端 `EDITOR_CANVAS_ASSET_KINDS`,并分别覆盖菜单选择和结构化 `assetKindOverride` 解析测试;不允许时补拒绝测试,不能只改一端 | +| 快速编辑正向白名单 | `src/components/image-editor/ImageCanvasGenerationModel.ts`、`server-rs/crates/api-server/src/editor_project.rs` | 显式决定该类别是否支持快速编辑;允许时同步前端 `QUICK_EDIT_SUPPORTED_ASSET_KINDS` 与后端 `ensure_editor_image_edit_source_kind_allowed`,并覆盖入口可见性、提交门禁、权威来源类型和未知类型失败关闭;不允许时保持默认关闭并补反例,不能仅因属于图片媒体族自动放行 | | 素材库与 renderer | `src/components/image-editor/` | 由 `assetKind` 派生图片、视频、音频或序列 renderer,并实现正确缩略图、预览和下载行为 | | 后台媒体 renderer | `apps/admin-web/src/components/AdminEditorAssetMedia.tsx` | 后台需要预览时复用现有组件;列表只加载最小媒体,弹窗再按需加载完整媒体 | | 精选 read model 与 renderer | `api-server`、`src/components/creation-home/` | 新类别允许公开时贯通正式字段、卡片、弹窗和损坏数据行为 | @@ -472,6 +474,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 其它注意事项: - `assetKind` 是数据库、Rust DTO 和对外 JSON 的唯一媒体类别真相;不要新增或返回并列的 `mediaType`。前端 renderer 可以保留内部派生类型,但不得回写后端。 +- 每次新增 `assetKind` 都必须分别完成“用户标签覆盖”和“快速编辑”两项资格评估;两者互不推导。可被用户选择不等于可快速编辑,属于图片媒体族也不等于自动进入快速编辑正向白名单。 - 只是新增分类或 renderer 且现有媒体字段足够时,不改 schema。只有必须跨刷新、复用、审核或公开保留的数据才新增类别专属字段。 - legacy 数据必须先通过有界、可审计、带 dry-run/hash/apply 门禁的数据库迁移收口;迁移后的 api-server、mapper、主站、后台和画布只读取正式字段,不保留运行时 fallback。 diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index ba79ed17a..71703311b 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -3945,6 +3945,7 @@ pub(crate) fn ensure_editor_image_edit_source_kind_allowed( | "icon-spec" | "publication-material" | "ui-design" + | "scene" ) ); let media_type_allowed = matches!(media_type, None | Some("image")); @@ -20275,6 +20276,7 @@ mod tests { (Some("icon-spec"), Some("image"), true), (Some("publication-material"), Some("image"), true), (Some("ui-design"), Some("image"), true), + (Some("scene"), Some("image"), true), (Some("icon"), Some("image"), false), (Some("character-animation"), Some("image-sequence"), false), (Some("sound-effect"), Some("audio"), false), diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index dde1f55b4..7652da375 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -17080,6 +17080,15 @@ mod tests { let rebuilt: JsonValue = serde_json::from_str(&rebuilt).expect("rebuilt layout json"); assert!(rebuilt[0].get("assetKindOverride").is_none()); + let scene = + r#"[{"layerId":"layer-scene","resourceId":"resource-1","assetKindOverride":"scene"}]"#; + let parsed = parse_structured_canvas_layout(scene) + .expect("scene must remain a user-overridable canvas asset kind"); + assert_eq!( + parsed.layers[0].asset_kind_override.as_deref(), + Some("scene") + ); + for invalid in [json!(""), json!("unknown-kind"), json!(42), json!({})] { let layout = json!([{ "layerId": "layer-1", diff --git a/src/components/image-editor/ImageCanvasGenerationModel.test.ts b/src/components/image-editor/ImageCanvasGenerationModel.test.ts index e725c2d7c..19cecc4fa 100644 --- a/src/components/image-editor/ImageCanvasGenerationModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationModel.test.ts @@ -435,6 +435,12 @@ describe('ImageCanvasGenerationModel', () => { assetKind: 'icon-spritesheet', }), ).toBe(true); + expect( + isQuickEditSupportedLayer({ + ...buildSourceLayer(), + assetKind: 'scene', + }), + ).toBe(true); expect( isQuickEditSupportedLayer({ ...buildSourceLayer(), diff --git a/src/components/image-editor/ImageCanvasGenerationModel.ts b/src/components/image-editor/ImageCanvasGenerationModel.ts index 2ace8b307..b8b959bf4 100644 --- a/src/components/image-editor/ImageCanvasGenerationModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationModel.ts @@ -686,6 +686,7 @@ const QUICK_EDIT_SUPPORTED_ASSET_KINDS = new Set([ 'icon-spec', 'publication-material', 'ui-design', + 'scene', 'video', ]); diff --git a/src/components/image-editor/ImageCanvasWorldView.test.tsx b/src/components/image-editor/ImageCanvasWorldView.test.tsx index f97d0fb68..93e42a3d4 100644 --- a/src/components/image-editor/ImageCanvasWorldView.test.tsx +++ b/src/components/image-editor/ImageCanvasWorldView.test.tsx @@ -1108,6 +1108,28 @@ describe('ImageCanvasWorldView', () => { } }); + it('offers scene as a user-selectable label for image layers', () => { + const imageLayer = createLayer({ + resourceAssetKind: 'image', + assetKind: 'character', + }); + const { props } = renderWorldView({ layers: [imageLayer] }); + + fireEvent.click(screen.getByRole('button', { name: '素材标签:角色' })); + + const sceneItem = within( + screen.getByRole('menu', { name: '选择素材标签' }), + ).getByRole('menuitem', { name: '场景' }); + expect(sceneItem.hasAttribute('disabled')).toBe(false); + + fireEvent.click(sceneItem); + + expect(props.onUpdateLayerAssetKind).toHaveBeenCalledWith( + imageLayer.id, + 'scene', + ); + }); + it('hides resolution and duration text for focused audio generation frames', () => { const dialog = createGenerationDialog({ id: 'dialog-audio',