From 31f7d5ea1b826819a7942c0f0dc2f12d9474abaf Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 30 Jul 2026 04:28:53 +0000 Subject: [PATCH 01/11] =?UTF-8?q?=E5=81=9C=E6=AD=A2=E5=90=91=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=B3=84=E9=9C=B2=E6=8A=A0=E5=9B=BE=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 2 + .../crates/api-server/src/editor_project.rs | 33 +++++-------- .../ImageCanvasEditorModel.test.ts | 12 +++++ .../image-editor/ImageCanvasEditorModel.ts | 47 ++++++++++++++++++- .../ImageCanvasExportModel.test.ts | 3 ++ .../image-editor/ImageCanvasExportModel.ts | 6 ++- .../ImageCanvasGenerationModel.ts | 20 +++++++- .../ImageCanvasMetadataModalView.test.tsx | 44 +++++++++++++++++ .../ImageCanvasMetadataModalView.tsx | 20 +++++--- 9 files changed, 155 insertions(+), 32 deletions(-) diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index d7422fe6f..d906ed40a 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -169,6 +169,8 @@ - 工程刷新后能从后端恢复资源、图层布局和 viewport。 - “我的”页项目入口能进入 `/project`;项目页能列出工程、重命名 / 删除单个工程、批量选择和批量删除;点击工程后进入 `/editor/canvas?projectid=` 并按 query 加载该工程。 +- 用户可见的 `generationInputs.fields` 不得写入或展示“处理模型”;角色抠图、图标 / UI 图集抠图及自动拆分形成的派生资产,其 `Model` 继续继承并展示源生图模型,不得改写为 `birefnet`、`anime-seg`、`connected-components`、阿里云抠图或本地键色等内部后处理模型。前端同时过滤历史项目中已持久化的“处理模型”字段;历史资源若只剩内部处理模型而无法恢复源生图模型,保留 `Model` 行并显示 `-`。画布 ZIP 的用户可见导出元数据使用同一过滤口径且保持既有结构。 + ## 后续扩展点 - 接入图片生成 / 修改计费、队列进度状态、OSS 落盘和更完整失败审计。 diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 993dbe364..400aa5ebf 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -1908,7 +1908,7 @@ pub(crate) async fn generate_editor_image_for_owner( let storage_profile = editor_generated_image_storage_profile(normalized_kind); let mut output_prompt = submitted_prompt.clone(); let mut output_actual_prompt = generated.actual_prompt.clone(); - let mut output_model = generation_options.model.to_string(); + let output_model = generation_options.model.to_string(); let mut output_provider = "VectorEngine".to_string(); let mut output_generation_inputs = generation_inputs.clone(); let source_record = if is_character_generation { @@ -2110,10 +2110,9 @@ pub(crate) async fn generate_editor_image_for_owner( image = restored_removal_image; output_prompt = "去除纯色背景".to_string(); output_actual_prompt = None; - output_model = removal.model; output_provider = removal_provider.to_string(); output_generation_inputs = - build_editor_derived_asset_generation_inputs("角色抠图", &output_model, &source_record); + build_editor_derived_asset_generation_inputs("角色抠图", &source_record); Some(source_record) } else { None @@ -2268,7 +2267,6 @@ pub(crate) fn apply_editor_screen_background_decision_to_generation_inputs( fn build_editor_derived_asset_generation_inputs( stage: &str, - model: &str, source: &EditorGeneratedAssetRecord, ) -> Option { let reference = source @@ -2299,7 +2297,6 @@ fn build_editor_derived_asset_generation_inputs( Some(json!({ "fields": [ { "title": "处理阶段", "value": stage }, - { "title": "处理模型", "value": model }, ], "references": reference.into_iter().collect::>(), })) @@ -4157,7 +4154,6 @@ async fn request_editor_background_removal_image_with_bgfilter_worker( pub(crate) struct EditorScreenBackgroundRemovalOutput { pub(crate) image: DownloadedOpenAiImage, pub(crate) provider: &'static str, - pub(crate) model: String, } pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter( @@ -4192,7 +4188,6 @@ pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter( mime_type: image.mime_type, }, provider: "BgFilter", - model: seg_model.to_string(), }); } Err(error) if !error.allows_flat_fallback() => { @@ -4247,7 +4242,6 @@ async fn fallback_editor_screen_background_removal( Ok(image) => Ok(EditorScreenBackgroundRemovalOutput { image, provider: "Aliyun Matting", - model: "segment-common-image".to_string(), }), Err(error) => { tracing::warn!( @@ -4285,7 +4279,6 @@ async fn fallback_editor_screen_background_removal( output.map(|image| EditorScreenBackgroundRemovalOutput { image, provider: "Genarrative Local", - model: "screen-color-keying".to_string(), }) } } @@ -4825,11 +4818,8 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( Some(editor_pixel_art_fallback_warning()), ); } - let matting_generation_inputs = build_editor_derived_asset_generation_inputs( - "图标图集抠图", - &removal.model, - &source_record, - ); + let matting_generation_inputs = + build_editor_derived_asset_generation_inputs("图标图集抠图", &source_record); let (spritesheet_width, spritesheet_height) = image::load_from_memory(image.bytes.as_slice()) .map(|image| (image.width(), image.height())) .unwrap_or((512, 512)); @@ -4864,7 +4854,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( height: spritesheet_height, prompt: "去除纯色背景".to_string(), actual_prompt: None, - model: removal.model.clone(), + model: generation_options.model.to_string(), provider: removal.provider.to_string(), task_id: generated.task_id.clone(), group_task_id: None, @@ -4902,11 +4892,10 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( group_task_id: None, prompt: "自动拆分图集".to_string(), actual_prompt: None, - model: "connected-components".to_string(), + model: generation_options.model.to_string(), provider: "Genarrative".to_string(), generation_inputs: build_editor_derived_asset_generation_inputs( "图集拆分", - "connected-components", &spritesheet_record, ), path_kind: "icon-spritesheet-assets", @@ -5588,7 +5577,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( ); } let matting_generation_inputs = - build_editor_derived_asset_generation_inputs("UI图集抠图", &removal.model, &source_record); + build_editor_derived_asset_generation_inputs("UI图集抠图", &source_record); let (spritesheet_width, spritesheet_height) = image::load_from_memory(image.bytes.as_slice()) .map(|image| (image.width(), image.height())) .unwrap_or((512, 512)); @@ -5623,7 +5612,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( height: spritesheet_height, prompt: "去除纯色背景".to_string(), actual_prompt: None, - model: removal.model.clone(), + model: generation_options.model.to_string(), provider: removal.provider.to_string(), task_id: generated.task_id.clone(), group_task_id: None, @@ -5662,11 +5651,10 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( group_task_id: None, prompt: "自动拆分图集".to_string(), actual_prompt: None, - model: "connected-components".to_string(), + model: generation_options.model.to_string(), provider: "Genarrative".to_string(), generation_inputs: build_editor_derived_asset_generation_inputs( "图集拆分", - "connected-components", &spritesheet_record, ), path_kind: "ui-design-assets", @@ -11891,6 +11879,7 @@ mod tests { "persist_editor_provider_source_resource", "build_multi_asset_canvas_layer_items", "if is_character_generation", + "let output_model = generation_options.model.to_string();", ], ); assert_function_contains_in_order( @@ -11916,6 +11905,7 @@ mod tests { "persist_editor_provider_source_resource", "slice_editor_icon_spritesheet_all", "persist_editor_spritesheet_slices", + "model: generation_options.model.to_string(),", ], ); assert_function_contains_in_order( @@ -11951,6 +11941,7 @@ mod tests { "EDITOR_BGFILTER_CROSS_CHECK_DISABLED", "persist_editor_provider_source_resource", "slice_generated_icon_spritesheet_all_by_connected_components", + "model: generation_options.model.to_string(),", ], ); assert_function_contains_in_order( diff --git a/src/components/image-editor/ImageCanvasEditorModel.test.ts b/src/components/image-editor/ImageCanvasEditorModel.test.ts index c613b81d7..1df6588a9 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.test.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.test.ts @@ -282,6 +282,8 @@ describe('ImageCanvasEditorModel', () => { zIndex: 9, sourceType: 'generated', objectKey: 'generated/object.png', + model: 'birefnet', + sourceResourceId: 'resource-provider-source', assetKind: 'character', generationInputs: { fields: [{ title: '角色设定', value: '骑士' }], @@ -307,6 +309,15 @@ describe('ImageCanvasEditorModel', () => { fields: [{ title: '角色设定', value: '骑士' }], references: [], }, + model: 'birefnet', + sourceResourceId: 'resource-provider-source', + }, + ], + [ + 'resource-provider-source', + { + imageSrc: '/read/provider-source.png', + model: 'gpt-image-2', }, ], ]), @@ -318,6 +329,7 @@ describe('ImageCanvasEditorModel', () => { sourceType: 'generated', assetKind: 'character', objectKey: 'generated/object.png', + model: 'gpt-image-2', locked: true, }); expect(hydrated?.generationInputs?.fields[0]?.value).toBe('骑士'); diff --git a/src/components/image-editor/ImageCanvasEditorModel.ts b/src/components/image-editor/ImageCanvasEditorModel.ts index bbc1db6b7..c906545ec 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.ts @@ -43,6 +43,24 @@ export const MINIMAP_SIZE = { width: 132, height: 84 }; export const MINIMAP_PADDING = 8; export const MINIMAP_DRAG_SENSITIVITY = 0.3; export const ASSET_DRAG_MIME_TYPE = 'application/x-genarrative-editor-asset'; +const INTERNAL_EDITOR_PROCESSING_MODELS = new Set([ + 'anime-seg', + 'bgfilter complex', + 'birefnet', + 'connected-components', + 'screen-color-keying', + 'segment-common-image', +]); + +export function isEditorInternalProcessingModel( + model: string | null | undefined, +) { + const normalizedModel = model?.trim().toLowerCase(); + return Boolean( + normalizedModel && INTERNAL_EDITOR_PROCESSING_MODELS.has(normalizedModel), + ); +} + export const MAX_HISTORY_STEPS = 60; export const CONTEXT_MENU_VIEWPORT_MARGIN = 8; export const CONTEXT_MENU_SIZE = { @@ -734,7 +752,7 @@ export function hydrateLayer( prompt: stringOrNull(snapshot.prompt) ?? stringOrNull(resource?.prompt), actualPrompt: stringOrNull(snapshot.actualPrompt) ?? stringOrNull(resource?.actualPrompt), - model: stringOrNull(snapshot.model) ?? stringOrNull(resource?.model), + model: resolveHydratedLayerModel(snapshot, resource, resourcesById), provider: stringOrNull(snapshot.provider) ?? stringOrNull(resource?.provider), taskId: stringOrNull(snapshot.taskId) ?? stringOrNull(resource?.taskId), objectKey: stringOrNull(snapshot.objectKey) ?? stringOrNull(resource?.objectKey), @@ -761,6 +779,33 @@ export function hydrateLayer( }; } +function resolveHydratedLayerModel( + snapshot: EditorProjectLayerSnapshot, + resource: CanvasLayerResourceMetadata | undefined, + resourcesById: Map, +) { + const persistedModel = + stringOrNull(snapshot.model) ?? stringOrNull(resource?.model); + if (!isEditorInternalProcessingModel(persistedModel)) { + return persistedModel; + } + + let sourceResourceId = + stringOrNull(snapshot.sourceResourceId) ?? + stringOrNull(resource?.sourceResourceId); + const visitedResourceIds = new Set(); + while (sourceResourceId && !visitedResourceIds.has(sourceResourceId)) { + visitedResourceIds.add(sourceResourceId); + const sourceResource = resourcesById.get(sourceResourceId); + const sourceModel = stringOrNull(sourceResource?.model); + if (sourceModel && !isEditorInternalProcessingModel(sourceModel)) { + return sourceModel; + } + sourceResourceId = stringOrNull(sourceResource?.sourceResourceId); + } + return persistedModel; +} + export function mapAssetLibrarySnapshot( library: EditorAssetLibrarySnapshot, ): { diff --git a/src/components/image-editor/ImageCanvasExportModel.test.ts b/src/components/image-editor/ImageCanvasExportModel.test.ts index 6a3eeeb06..c01a5481f 100644 --- a/src/components/image-editor/ImageCanvasExportModel.test.ts +++ b/src/components/image-editor/ImageCanvasExportModel.test.ts @@ -175,6 +175,7 @@ describe('ImageCanvasExportModel', () => { it('filters built-in prompts from exported visible generation inputs', () => { const metadata = buildLayerExportMetadata( buildLayer({ + model: 'gpt-image-2', generationInputs: { fields: [ { title: '生成提示词', value: 'AI 生成图片' }, @@ -195,6 +196,7 @@ describe('ImageCanvasExportModel', () => { { title: '用户输入', value: '用户填写的视觉要求' }, { title: '游戏分类', value: '游戏音效' }, { title: 'duration', value: '5秒' }, + { title: '处理模型', value: 'birefnet' }, ], references: [], }, @@ -210,6 +212,7 @@ describe('ImageCanvasExportModel', () => { ], references: [], }); + expect(metadata.visible.model).toBe('gpt-image-2'); }); it('builds a Spine JSON attachment timeline from image sequence frames', () => { diff --git a/src/components/image-editor/ImageCanvasExportModel.ts b/src/components/image-editor/ImageCanvasExportModel.ts index 3c064986d..521ca5467 100644 --- a/src/components/image-editor/ImageCanvasExportModel.ts +++ b/src/components/image-editor/ImageCanvasExportModel.ts @@ -8,7 +8,8 @@ import type { import { DEFAULT_ICON_DESCRIPTIONS, formatLayerImageType, - getEditorImageModelDisplayName, + getEditorLayerModelDisplayName, + isEditorUserVisibleGenerationInputField, UI_DESIGN_ASSET_EXTRACTION_PROMPT, } from './ImageCanvasGenerationModel'; import { formatCanvasDurationMetric } from './ImageCanvasMediaModel'; @@ -543,6 +544,7 @@ function isBuiltInGenerationInputField(field: CanvasGenerationInputField) { const builtInValues = BUILT_IN_GENERATION_INPUT_VALUES_BY_TITLE.get(normalizedTitle); return ( + !isEditorUserVisibleGenerationInputField(field) || BUILT_IN_GENERATION_INPUT_TITLES.has(normalizedTitle) || normalizedValue === UI_DESIGN_ASSET_EXTRACTION_PROMPT || Boolean(builtInValues?.has(normalizedValue)) @@ -572,7 +574,7 @@ export function buildLayerVisibleExportMetadata(layer: CanvasLayer) { const base = { type: formatLayerImageType(layer), generationInputs: buildVisibleGenerationInputs(layer), - model: layer.model ? getEditorImageModelDisplayName(layer.model) : '-', + model: getEditorLayerModelDisplayName(layer.model), task: formatTaskIdForDisplay(layer.taskId), object: layer.objectKey ?? layer.assetObjectId ?? '-', }; diff --git a/src/components/image-editor/ImageCanvasGenerationModel.ts b/src/components/image-editor/ImageCanvasGenerationModel.ts index 463def180..396d10029 100644 --- a/src/components/image-editor/ImageCanvasGenerationModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationModel.ts @@ -4,7 +4,10 @@ import type { EditorCharacterAnimationRatio, EditorCharacterAnimationResolution, } from '../../services/image-editor/editorProjectClient'; -import { isGeneratedLayer } from './ImageCanvasEditorModel'; +import { + isEditorInternalProcessingModel, + isGeneratedLayer, +} from './ImageCanvasEditorModel'; import type { CanvasGenerationDialogState, CanvasGenerationInputField, @@ -548,6 +551,21 @@ export function getEditorImageModelDisplayName( ); } +export function isEditorUserVisibleGenerationInputField( + field: CanvasGenerationInputField, +) { + return field.title.trim() !== '处理模型'; +} + +export function getEditorLayerModelDisplayName( + model: string | null | undefined, +) { + if (!model?.trim() || isEditorInternalProcessingModel(model)) { + return '-'; + } + return getEditorImageModelDisplayName(model); +} + export function getEditorGenerationModelDisplayName( model: string | null | undefined, ) { diff --git a/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx index 41ab7a9aa..d48df2de0 100644 --- a/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx +++ b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx @@ -58,6 +58,7 @@ describe('ImageCanvasMetadataModalView', () => { expect(within(dialog).getByText('参考图')).toBeTruthy(); expect(within(dialog).getByText('角色立绘')).toBeTruthy(); expect(within(dialog).getByText('项目资源 · resource-reference')).toBeTruthy(); + expect(within(dialog).getByText('Model')).toBeTruthy(); expect(within(dialog).getByText('gpt-image-2')).toBeTruthy(); expect(within(dialog).getByText('1024 x 768 px')).toBeTruthy(); expect(within(dialog).queryByText('Provider')).toBeNull(); @@ -66,6 +67,49 @@ describe('ImageCanvasMetadataModalView', () => { expect(within(dialog).getByText('123')).toBeTruthy(); }); + it('keeps the generation model while hiding internal processing models', () => { + const { rerender } = render( + , + ); + + const dialog = screen.getByRole('dialog', { name: '图片信息' }); + + expect(within(dialog).getByText('处理阶段')).toBeTruthy(); + expect(within(dialog).getByText('角色抠图')).toBeTruthy(); + expect(within(dialog).queryByText('处理模型')).toBeNull(); + expect(within(dialog).getByText('Model')).toBeTruthy(); + expect(within(dialog).getByText('gpt-image-2')).toBeTruthy(); + expect(within(dialog).queryByText('birefnet')).toBeNull(); + + rerender( + , + ); + + expect(within(dialog).getByText('Model')).toBeTruthy(); + expect(within(dialog).queryByText('birefnet')).toBeNull(); + }); + it('renders nanobanana2 instead of the raw Gemini image model id', () => { render( {formatLayerImageType(layer)}
生成输入
- {layer.generationInputs?.fields.length || - layer.generationInputs?.references.length ? ( + {visibleGenerationInputFields.length || + generationInputReferences.length ? ( <> - {layer.generationInputs.fields.map((field) => ( + {visibleGenerationInputFields.map((field) => (
{field.value}
))} - {layer.generationInputs.references.length ? ( + {generationInputReferences.length ? (
- {layer.generationInputs.references.map((reference) => ( + {generationInputReferences.map((reference) => (
Model
-
{layer.model ? getEditorImageModelDisplayName(layer.model) : '-'}
+
{getEditorLayerModelDisplayName(layer.model)}
{layer.mediaType === 'audio' ? ( <>
时长
-- 2.52.0 From 4670632919641c7cb4cee7594016b4f6cd43a49d Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 30 Jul 2026 12:10:40 +0000 Subject: [PATCH 02/11] =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=8A=A0=E5=9B=BE?= =?UTF-8?q?=E5=AE=9E=E9=99=85=E6=A8=A1=E5=9E=8B=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 generationInputs 顶层记录实际抠图供应商与模型 覆盖 BgFilter、阿里云和本地键色及三条派生资产持久化链路 保持源生图模型与图片信息展示契约不变 补充后端前端测试并同步数据契约和决策文档 --- .../shared-memory/decision-log.md | 10 + ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 1 + ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 1 + .../crates/api-server/src/editor_project.rs | 204 ++++++++++++++++-- .../ImageCanvasMetadataModalView.test.tsx | 19 +- 5 files changed, 206 insertions(+), 29 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 11bc64ec6..acd8d380d 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,16 @@ --- +## 2026-07-30 抠图实际后端作为 generationInputs 顶层内部元数据保存 + +- 背景:角色、图标图集和 UI 图集抠图派生资产需要保留最终实际执行的处理后端,供后台诊断 BgFilter、阿里云通用抠图和本地键色的降级结果;把抠图模型写成 `generationInputs.fields` 的“处理模型”会进入图片信息,与用户可见输入快照语义冲突,而覆盖正式资产 `model` 又会丢失源生图模型。 +- 决策:继续使用现有 `generation_inputs_json` JSON 包络,不修改 SpacetimeDB schema。`fields` / `references` 只保存用户可见生成输入;仿照顶层 `screenColorHex`,抠图派生资产在顶层写入 `mattingProvider` / `mattingModel`。BgFilter 记录本次实际 `seg_model`;阿里云记录 `Aliyun Matting / segment-common-image`;本地键色记录 `Genarrative Local / screen-color-keying`。三条链路的正式资产 `model` 继续继承源生图模型,图片信息不读取顶层内部字段;这些字段不是保密字段,原始管理接口仍可按既有权限读取。角色动作逐帧可能混用多个 fallback,本次不把单帧结果提升为整组动画模型。 +- 影响范围:`server-rs/crates/api-server/src/editor_project.rs` 的抠图结果和角色 / 图标 / UI 派生资产持久化、图片信息兼容测试、后端数据契约与图片画布 MVP 文档;不修改前端生产展示逻辑、请求 DTO、SpacetimeDB 表、迁移或生成绑定。 +- 验证方式:后端单测覆盖三种实际结果映射、顶层元数据不改写 `fields`,结构断言覆盖三条派生资产持久化链路仍保留源生图模型;前端测试覆盖顶层字段不在图片信息出现。运行 `cargo test -p api-server editor_project::tests --manifest-path server-rs/Cargo.toml`、图片信息定向前端测试、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 + +--- + ## 2026-07-29 图集切片必须受前置容量和有界 CPU 保护 - 背景:图标与 UI 图集的 alpha 连通域识别会在 async handler 上同步执行;原始连通域合并采用全量两两比较,`64` 个输出限制又晚于排序、裁剪和 PNG 编码。碎块或噪点图会放大 CPU 与内存成本,手动拆分、图标自动拆分和 UI 提取都受影响。另一方面,图标与 UI 的 Alpha 尺寸恢复、provider 原图回读或透明图解码失败此前只记日志,仍会把不可信透明图持久化并拆分。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 835477ab7..59ab83ab7 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -172,6 +172,7 @@ - “我的”页项目入口能进入 `/project`;项目页能列出工程、重命名 / 删除单个工程、批量选择和批量删除;点击工程后进入 `/editor/canvas?projectid=` 并按 query 加载该工程。 - 用户可见的 `generationInputs.fields` 不得写入或展示“处理模型”;角色抠图、图标 / UI 图集抠图及自动拆分形成的派生资产,其 `Model` 继续继承并展示源生图模型,不得改写为 `birefnet`、`anime-seg`、`connected-components`、阿里云抠图或本地键色等内部后处理模型。前端同时过滤历史项目中已持久化的“处理模型”字段;历史资源若只剩内部处理模型而无法恢复源生图模型,保留 `Model` 行并显示 `-`。画布 ZIP 的用户可见导出元数据使用同一过滤口径且保持既有结构。 +- `generationInputs` 整体是可扩展 JSON 包络,不等同于全量用户可见快照;图片信息只消费 `fields` / `references`。后端可仿照 `screenColorHex` 在顶层保存 `mattingProvider` / `mattingModel`,记录角色、图标图集和 UI 图集抠图实际成功的 BgFilter、阿里云或本地键色结果;前端规范化和图片信息展示不得把这些顶层内部字段映射成可见字段,正式 `Model` 仍取源生图模型。 ## 后续扩展点 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 8ed03321b..8c0389077 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -576,6 +576,7 @@ npm run check:server-rs-ddd - Rust 结构体:`EditorProjectResource` - 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` - 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind`、`generation_inputs_json` 和历史 `public_showcase_enabled`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。`public_showcase_enabled` 只保留旧接口兼容,不再作为 `/creation` 的 `陶泥儿精选` 事实源;精选公开改由账号级生成素材提交 `editor_showcase_asset` 审核决定。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存稳定 `resource_id` 布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot`、`source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 layout 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。账号级素材删除不级联删除该表,避免历史画布丢图。结构化 canvas 的几何、层级、分组和资源引用以 `editor_canvas_layer` 为权威,生成器对象以 `editor_canvas_generation_dialog` 为权威;legacy canvas 才在 2 MiB 上限内从 `editor_canvas.layers_json` 兼容读取。新写入不再把素材生成输入快照作为图层布局真相保存。历史普通图层缺资源只能由 migration operator 调用 `repair_editor_canvas_resources_and_return` 定向修复:procedure 每次只处理一个尚无迁移记录的 legacy canvas,校验 owner/project、revision、canvas/project 两份 raw layout SHA-256、精确 layer/resource/sourceResourceId、同工程替换资源与 private asset_object 谱系;图片只替换引用,音频只恢复经核验的 `420x120` 项目资源行。运维入口 `npm run spacetime:editor-canvas-resources:repair` 默认 dry-run,apply 必须绑定 plan SHA-256 并在成功后自动复核 already-repaired,禁止手工 SQL 绕过事务 guard。 +- `generation_inputs_json` 包络契约:`fields` / `references` 是图片信息读取的用户可见生成输入快照;顶层允许保存后端内部结果扩展。现有 `screenColorHex` 保存实际背景色,角色、图标图集和 UI 图集抠图派生资产使用 `mattingProvider` / `mattingModel` 保存实际成功的处理后端与模型。BgFilter 保存本次 `seg_model`,阿里云通用抠图保存 `Aliyun Matting / segment-common-image`,本地键色保存 `Genarrative Local / screen-color-keying`。这些顶层字段不写入 `fields`、不在图片信息展示,也不具有保密边界,原始管理接口按既有权限仍可读取。 - 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。 ### `editor_asset_folder` diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 2111c8e19..b004ef20c 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -152,6 +152,11 @@ const EDITOR_LEGACY_GREEN_SCREEN_SOURCE_ASSET_KIND: &str = "editor_green_screen_ const EDITOR_PROVIDER_SOURCE_SLOT: &str = "provider_source"; pub(crate) const EDITOR_BGFILTER_DEFAULT_SEG_MODEL: &str = "birefnet"; const EDITOR_BGFILTER_SEG_MODEL_ANIME_SEG: &str = "anime-seg"; +const EDITOR_BGFILTER_MATTING_PROVIDER: &str = "BgFilter"; +const EDITOR_ALIYUN_MATTING_PROVIDER: &str = "Aliyun Matting"; +const EDITOR_ALIYUN_MATTING_MODEL: &str = "segment-common-image"; +const EDITOR_LOCAL_MATTING_PROVIDER: &str = "Genarrative Local"; +const EDITOR_LOCAL_MATTING_MODEL: &str = "screen-color-keying"; const EDITOR_MATTING_SOURCE_URL_EXPIRE_SECONDS: u64 = 600; pub(crate) const EDITOR_BGFILTER_CROSS_CHECK_ENABLED: bool = true; pub(crate) const EDITOR_BGFILTER_CROSS_CHECK_DISABLED: bool = false; @@ -2030,6 +2035,7 @@ pub(crate) async fn generate_editor_image_for_owner( } }; let removal_provider = removal.provider; + let removal_model = removal.model.clone(); let (restored_removal_image, postprocess_dimension_error, pixel_art_error) = if image_style == EditorImageGenerationStyle::PixelArt { @@ -2115,8 +2121,11 @@ pub(crate) async fn generate_editor_image_for_owner( output_prompt = "去除纯色背景".to_string(); output_actual_prompt = None; output_provider = removal_provider.to_string(); - output_generation_inputs = - build_editor_derived_asset_generation_inputs("角色抠图", &source_record); + output_generation_inputs = apply_editor_matting_metadata_to_generation_inputs( + build_editor_derived_asset_generation_inputs("角色抠图", &source_record), + removal_provider, + removal_model.as_str(), + ); Some(source_record) } else { None @@ -2269,6 +2278,23 @@ pub(crate) fn apply_editor_screen_background_decision_to_generation_inputs( Some(value) } +fn apply_editor_matting_metadata_to_generation_inputs( + generation_inputs: Option, + provider: &str, + model: &str, +) -> Option { + let mut value = match generation_inputs { + Some(value) if value.is_object() => value, + Some(other) => return Some(other), + None => json!({ "fields": [], "references": [] }), + }; + // 抠图供应商和模型是内部实际执行结果,不属于用户可见的生成输入快照; + // 仿照 screenColorHex 只写顶层扩展,图片信息仍只读取 fields / references。 + value["mattingProvider"] = Value::String(provider.to_string()); + value["mattingModel"] = Value::String(model.to_string()); + Some(value) +} + fn build_editor_derived_asset_generation_inputs( stage: &str, source: &EditorGeneratedAssetRecord, @@ -4165,6 +4191,33 @@ async fn request_editor_background_removal_image_with_bgfilter_worker( pub(crate) struct EditorScreenBackgroundRemovalOutput { pub(crate) image: DownloadedOpenAiImage, pub(crate) provider: &'static str, + pub(crate) model: String, +} + +impl EditorScreenBackgroundRemovalOutput { + fn bgfilter(image: DownloadedOpenAiImage, seg_model: &str) -> Self { + Self { + image, + provider: EDITOR_BGFILTER_MATTING_PROVIDER, + model: seg_model.to_string(), + } + } + + fn aliyun(image: DownloadedOpenAiImage) -> Self { + Self { + image, + provider: EDITOR_ALIYUN_MATTING_PROVIDER, + model: EDITOR_ALIYUN_MATTING_MODEL.to_string(), + } + } + + fn local(image: DownloadedOpenAiImage) -> Self { + Self { + image, + provider: EDITOR_LOCAL_MATTING_PROVIDER, + model: EDITOR_LOCAL_MATTING_MODEL.to_string(), + } + } } pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter( @@ -4192,14 +4245,14 @@ pub(crate) async fn remove_editor_generated_screen_background_with_bgfilter( .await { Ok(image) => { - return Ok(EditorScreenBackgroundRemovalOutput { - image: DownloadedOpenAiImage { + return Ok(EditorScreenBackgroundRemovalOutput::bgfilter( + DownloadedOpenAiImage { bytes: image.bytes, extension: image.extension, mime_type: image.mime_type, }, - provider: "BgFilter", - }); + seg_model, + )); } Err(error) if !error.allows_flat_fallback() => { return Err(error.into_app_error()); @@ -4250,10 +4303,7 @@ async fn fallback_editor_screen_background_removal( Err(error) => Err(error), }; match aliyun_result { - Ok(image) => Ok(EditorScreenBackgroundRemovalOutput { - image, - provider: "Aliyun Matting", - }), + Ok(image) => Ok(EditorScreenBackgroundRemovalOutput::aliyun(image)), Err(error) => { tracing::warn!( provider = "aliyun-matting", @@ -4287,10 +4337,7 @@ async fn fallback_editor_screen_background_removal( let output = remove_editor_generated_green_screen_background(&source_image, screen_color); drop(source_image); - output.map(|image| EditorScreenBackgroundRemovalOutput { - image, - provider: "Genarrative Local", - }) + output.map(EditorScreenBackgroundRemovalOutput::local) } } } @@ -4781,6 +4828,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( } }; let removal_provider = removal.provider; + let removal_model = removal.model.clone(); let (image, postprocess_dimension_error, pixel_art_error) = if image_style == EditorImageGenerationStyle::PixelArt { @@ -4858,8 +4906,11 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( Some(editor_pixel_art_fallback_warning()), ); } - let matting_generation_inputs = - build_editor_derived_asset_generation_inputs("图标图集抠图", &source_record); + let matting_generation_inputs = apply_editor_matting_metadata_to_generation_inputs( + build_editor_derived_asset_generation_inputs("图标图集抠图", &source_record), + removal_provider, + removal_model.as_str(), + ); let (spritesheet_width, spritesheet_height) = decode_editor_provider_source_dimensions( &image, "editor-icon-spritesheet", @@ -4877,7 +4928,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( "icon-spritesheets", "spritesheet", "spritesheet", - removal.provider, + removal_provider, ) .await?; let spritesheet_image_src = @@ -4897,7 +4948,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( prompt: "去除纯色背景".to_string(), actual_prompt: None, model: generation_options.model.to_string(), - provider: removal.provider.to_string(), + provider: removal_provider.to_string(), task_id: generated.task_id.clone(), group_task_id: None, group_task_expected_asset_count: None, @@ -5703,6 +5754,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( } }; let removal_provider = removal.provider; + let removal_model = removal.model.clone(); let (image, postprocess_dimension_error) = apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original( state, @@ -5753,8 +5805,11 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( }, )); } - let matting_generation_inputs = - build_editor_derived_asset_generation_inputs("UI图集抠图", &source_record); + let matting_generation_inputs = apply_editor_matting_metadata_to_generation_inputs( + build_editor_derived_asset_generation_inputs("UI图集抠图", &source_record), + removal_provider, + removal_model.as_str(), + ); let (spritesheet_width, spritesheet_height) = decode_editor_provider_source_dimensions( &image, "editor-ui-design-asset-extraction", @@ -5772,7 +5827,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( "ui-design-spritesheets", "spritesheet", "spritesheet", - removal.provider, + removal_provider, ) .await?; let spritesheet_image_src = @@ -5792,7 +5847,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( prompt: "去除纯色背景".to_string(), actual_prompt: None, model: generation_options.model.to_string(), - provider: removal.provider.to_string(), + provider: removal_provider.to_string(), task_id: generated.task_id.clone(), group_task_id: None, group_task_expected_asset_count: None, @@ -8763,6 +8818,80 @@ mod tests { assert_eq!(updated["screenColorHex"], json!("#CFEFFF")); } + #[test] + fn matting_metadata_records_internal_result_without_touching_visible_fields() { + let updated = apply_editor_matting_metadata_to_generation_inputs( + Some(json!({ + "fields": [{ "title": "处理阶段", "value": "角色抠图" }], + "references": [], + "screenColorHex": "#CFEFFF", + })), + EDITOR_BGFILTER_MATTING_PROVIDER, + EDITOR_BGFILTER_SEG_MODEL_ANIME_SEG, + ) + .expect("matting metadata should be recorded"); + + assert_eq!( + updated["fields"], + json!([{ "title": "处理阶段", "value": "角色抠图" }]) + ); + assert_eq!(updated["references"], json!([])); + assert_eq!(updated["screenColorHex"], json!("#CFEFFF")); + assert_eq!(updated["mattingProvider"], json!("BgFilter")); + assert_eq!(updated["mattingModel"], json!("anime-seg")); + assert!( + updated["fields"] + .as_array() + .expect("fields should remain an array") + .iter() + .all(|field| field["title"] != "处理模型") + ); + + let created = apply_editor_matting_metadata_to_generation_inputs( + None, + EDITOR_LOCAL_MATTING_PROVIDER, + EDITOR_LOCAL_MATTING_MODEL, + ) + .expect("missing inputs should receive an internal metadata envelope"); + assert_eq!( + created, + json!({ + "fields": [], + "references": [], + "mattingProvider": "Genarrative Local", + "mattingModel": "screen-color-keying", + }) + ); + } + + fn test_screen_background_removal_image() -> DownloadedOpenAiImage { + DownloadedOpenAiImage { + bytes: vec![1, 2, 3], + extension: "png".to_string(), + mime_type: "image/png".to_string(), + } + } + + #[test] + fn screen_background_removal_outputs_record_each_actual_matting_backend() { + let bgfilter = EditorScreenBackgroundRemovalOutput::bgfilter( + test_screen_background_removal_image(), + EDITOR_BGFILTER_SEG_MODEL_ANIME_SEG, + ); + assert_eq!(bgfilter.provider, "BgFilter"); + assert_eq!(bgfilter.model, "anime-seg"); + + let aliyun = + EditorScreenBackgroundRemovalOutput::aliyun(test_screen_background_removal_image()); + assert_eq!(aliyun.provider, "Aliyun Matting"); + assert_eq!(aliyun.model, "segment-common-image"); + + let local = + EditorScreenBackgroundRemovalOutput::local(test_screen_background_removal_image()); + assert_eq!(local.provider, "Genarrative Local"); + assert_eq!(local.model, "screen-color-keying"); + } + fn apply_editor_canvas_generation_completion( layers: Value, completion: &EditorCanvasGenerationCompletionRequest, @@ -12157,6 +12286,7 @@ mod tests { assert_eq!(removed.image.mime_type, "image/png"); assert_eq!(removed.image.extension, "png"); assert_eq!(removed.provider, "BgFilter"); + assert_eq!(removed.model, "birefnet"); } #[tokio::test] @@ -12279,8 +12409,17 @@ mod tests { "build_multi_asset_canvas_layer_items", "if is_character_generation", "let output_model = generation_options.model.to_string();", + "apply_editor_matting_metadata_to_generation_inputs", + "build_editor_derived_asset_generation_inputs(\"角色抠图\"", + "generation_inputs: output_generation_inputs", ], ); + assert_function_not_contains( + source, + "pub(crate) async fn generate_editor_image_for_owner", + "fn normalize_editor_image_generation_size", + &["{ \"title\": \"处理模型\""], + ); assert_function_contains_in_order( source, "pub(crate) async fn generate_editor_image_for_owner", @@ -12305,8 +12444,17 @@ mod tests { "slice_editor_icon_spritesheet_all", "persist_editor_spritesheet_slices", "model: generation_options.model.to_string(),", + "apply_editor_matting_metadata_to_generation_inputs", + "build_editor_derived_asset_generation_inputs(\"图标图集抠图\"", + "generation_inputs: matting_generation_inputs", ], ); + assert_function_not_contains( + source, + "pub(crate) async fn generate_editor_icon_spritesheet_for_owner", + "pub async fn extract_editor_ui_design_assets", + &["{ \"title\": \"处理模型\""], + ); assert_function_contains_in_order( source, "pub(crate) async fn generate_editor_icon_spritesheet_for_owner", @@ -12341,8 +12489,17 @@ mod tests { "persist_editor_provider_source_resource", "slice_editor_icon_spritesheet_all", "model: generation_options.model.to_string(),", + "apply_editor_matting_metadata_to_generation_inputs", + "build_editor_derived_asset_generation_inputs(\"UI图集抠图\"", + "generation_inputs: matting_generation_inputs", ], ); + assert_function_not_contains( + source, + "pub(crate) async fn extract_editor_ui_design_assets_for_owner", + "pub(crate) fn editor_project_payload_from_record", + &["{ \"title\": \"处理模型\""], + ); assert_function_contains_in_order( source, "pub(crate) async fn extract_editor_ui_design_assets_for_owner", @@ -12363,6 +12520,7 @@ mod tests { "crate::bgfilter_worker::BgfilterBackgroundMode::Flat", "editor_bgfilter_flat_deadline_reserve", "state.config.aliyun_matting_request_timeout_ms", + "EditorScreenBackgroundRemovalOutput::bgfilter", "error.allows_flat_fallback()", "fallback_editor_screen_background_removal", ], @@ -12385,9 +12543,11 @@ mod tests { "fn sign_editor_private_object_read_url", &[ "segment_image_url_with_aliyun_matting", + "EditorScreenBackgroundRemovalOutput::aliyun", "download_editor_persisted_image_object", "remove_editor_generated_green_screen_background", "drop(source_image)", + "output.map(EditorScreenBackgroundRemovalOutput::local)", ], ); assert_function_contains( diff --git a/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx index d48df2de0..e47cddfde 100644 --- a/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx +++ b/src/components/image-editor/ImageCanvasMetadataModalView.test.tsx @@ -68,17 +68,20 @@ describe('ImageCanvasMetadataModalView', () => { }); it('keeps the generation model while hiding internal processing models', () => { + const generationInputsWithInternalMattingMetadata = { + fields: [ + { title: '处理阶段', value: '角色抠图' }, + { title: '处理模型', value: 'birefnet' }, + ], + references: [], + mattingProvider: 'Aliyun Matting', + mattingModel: 'segment-common-image', + }; const { rerender } = render( , @@ -92,6 +95,8 @@ describe('ImageCanvasMetadataModalView', () => { expect(within(dialog).getByText('Model')).toBeTruthy(); expect(within(dialog).getByText('gpt-image-2')).toBeTruthy(); expect(within(dialog).queryByText('birefnet')).toBeNull(); + expect(within(dialog).queryByText('Aliyun Matting')).toBeNull(); + expect(within(dialog).queryByText('segment-common-image')).toBeNull(); rerender( Date: Thu, 30 Jul 2026 13:22:11 +0000 Subject: [PATCH 03/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=87=AA=E4=B8=BB?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=AD=89=E5=BE=85=E6=B5=8B=E8=AF=95=E7=AB=9E?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用待处理任务夹具替代会启动后台 drain 的高层入口 避免 planning 与 waiting-for-manifest-tasks 并发写入同一 run 定向测试连续运行五十次通过 --- .../agent/runtime_driver/main_loop_tests.rs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs index ac1183d0c..0bd6ac827 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs @@ -317,24 +317,26 @@ fn autonomous_manifest_waiting_context_persists_without_finishing_parent_run() { let root = temporary.path().join("project"); init_local_game_project_at(&root, "manifest-waiting-project", TASK) .expect("init manifest waiting project"); - start_game_creator_supervisor_background_task_for_session_at( + let session_id = resolve_agent_conversation_session_id_at( &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, None, + true, + ) + .expect("resolve autonomous parent session"); + let task_record = append_unique_game_creator_agent_runtime_pending_task( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &session_id, TASK, RUN_ID, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, - AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, ) .expect("queue autonomous parent task"); update_manifest_task_status_at(&root, "design-director", GameCreationAppTaskStatus::Running) .expect("start first manifest task"); - let task_record = read_latest_game_creator_agent_runtime_task_by_run_id( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - RUN_ID, - ) - .expect("read autonomous parent task") - .expect("autonomous parent task exists"); let mut runtime = agent_runtime_state_from_task_record(&task_record); let mut observations = Vec::new(); let continuation = AgentRuntimeContinuationContext::default(); -- 2.52.0 From 6170a91992ae14af8098338db89b4e0931002332 Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 30 Jul 2026 13:52:19 +0000 Subject: [PATCH 04/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8C=BF=E5=90=8D?= =?UTF-8?q?=E7=B2=BE=E9=80=89=E5=86=85=E9=83=A8=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=B3=84=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 匿名精选响应省略 provider,并清理 generationInputs 中的内部抠图字段。 保留素材 owner、精选提交响应和后台管理接口的原始审计信息。 补充公开边界回归测试并同步后端契约、前端说明和项目决策。 --- .../shared-memory/decision-log.md | 4 +- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 2 +- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 2 +- .../crates/api-server/src/editor_project.rs | 68 +++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 332fc6e73..e823c05c6 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -19,9 +19,9 @@ ## 2026-07-30 抠图实际后端作为 generationInputs 顶层内部元数据保存 - 背景:角色、图标图集和 UI 图集抠图派生资产需要保留最终实际执行的处理后端,供后台诊断 BgFilter、阿里云通用抠图和本地键色的降级结果;把抠图模型写成 `generationInputs.fields` 的“处理模型”会进入图片信息,与用户可见输入快照语义冲突,而覆盖正式资产 `model` 又会丢失源生图模型。 -- 决策:继续使用现有 `generation_inputs_json` JSON 包络,不修改 SpacetimeDB schema。`fields` / `references` 只保存用户可见生成输入;仿照顶层 `screenColorHex`,抠图派生资产在顶层写入 `mattingProvider` / `mattingModel`。BgFilter 记录本次实际 `seg_model`;阿里云记录 `Aliyun Matting / segment-common-image`;本地键色记录 `Genarrative Local / screen-color-keying`。三条链路的正式资产 `model` 继续继承源生图模型,图片信息不读取顶层内部字段;这些字段不是保密字段,原始管理接口仍可按既有权限读取。角色动作逐帧可能混用多个 fallback,本次不把单帧结果提升为整组动画模型。 +- 决策:继续使用现有 `generation_inputs_json` JSON 包络,不修改 SpacetimeDB schema。`fields` / `references` 只保存用户可见生成输入;仿照顶层 `screenColorHex`,抠图派生资产在顶层写入 `mattingProvider` / `mattingModel`。BgFilter 记录本次实际 `seg_model`;阿里云记录 `Aliyun Matting / segment-common-image`;本地键色记录 `Genarrative Local / screen-color-keying`。三条链路的正式资产 `model` 继续继承源生图模型,图片信息不读取顶层内部字段。`screenColorHex`、`mattingProvider`、`mattingModel` 和素材顶层 `provider` 属于内部执行信息:素材 owner、精选提交响应和后台管理接口仍按既有权限读取原始值,匿名 `GET /api/editor/showcase/resources` 必须在 public payload 边界删除三个 `generationInputs` 顶层内部键并省略 `provider`,历史精选快照不做数据迁移。角色动作逐帧可能混用多个 fallback,本次不把单帧结果提升为整组动画模型。 - 影响范围:`server-rs/crates/api-server/src/editor_project.rs` 的抠图结果和角色 / 图标 / UI 派生资产持久化、图片信息兼容测试、后端数据契约与图片画布 MVP 文档;不修改前端生产展示逻辑、请求 DTO、SpacetimeDB 表、迁移或生成绑定。 -- 验证方式:后端单测覆盖三种实际结果映射、顶层元数据不改写 `fields`,结构断言覆盖三条派生资产持久化链路仍保留源生图模型;前端测试覆盖顶层字段不在图片信息出现。运行 `cargo test -p api-server editor_project::tests --manifest-path server-rs/Cargo.toml`、图片信息定向前端测试、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。 +- 验证方式:后端单测覆盖三种实际结果映射、顶层元数据不改写 `fields`,结构断言覆盖三条派生资产持久化链路仍保留源生图模型;public mapper 测试同时证明匿名响应省略 `provider` 和三个内部键、owner 精选快照仍保留原始值;前端测试覆盖顶层字段不在图片信息出现。运行 `cargo test -p api-server editor_project::tests --manifest-path server-rs/Cargo.toml`、图片信息定向前端测试、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 --- diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 59ab83ab7..504cdf427 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -172,7 +172,7 @@ - “我的”页项目入口能进入 `/project`;项目页能列出工程、重命名 / 删除单个工程、批量选择和批量删除;点击工程后进入 `/editor/canvas?projectid=` 并按 query 加载该工程。 - 用户可见的 `generationInputs.fields` 不得写入或展示“处理模型”;角色抠图、图标 / UI 图集抠图及自动拆分形成的派生资产,其 `Model` 继续继承并展示源生图模型,不得改写为 `birefnet`、`anime-seg`、`connected-components`、阿里云抠图或本地键色等内部后处理模型。前端同时过滤历史项目中已持久化的“处理模型”字段;历史资源若只剩内部处理模型而无法恢复源生图模型,保留 `Model` 行并显示 `-`。画布 ZIP 的用户可见导出元数据使用同一过滤口径且保持既有结构。 -- `generationInputs` 整体是可扩展 JSON 包络,不等同于全量用户可见快照;图片信息只消费 `fields` / `references`。后端可仿照 `screenColorHex` 在顶层保存 `mattingProvider` / `mattingModel`,记录角色、图标图集和 UI 图集抠图实际成功的 BgFilter、阿里云或本地键色结果;前端规范化和图片信息展示不得把这些顶层内部字段映射成可见字段,正式 `Model` 仍取源生图模型。 +- `generationInputs` 整体是可扩展 JSON 包络,不等同于全量用户可见快照;图片信息只消费 `fields` / `references`。后端可仿照 `screenColorHex` 在顶层保存 `mattingProvider` / `mattingModel`,记录角色、图标图集和 UI 图集抠图实际成功的 BgFilter、阿里云或本地键色结果;前端规范化和图片信息展示不得把这些顶层内部字段映射成可见字段,正式 `Model` 仍取源生图模型。匿名精选资源响应不返回素材顶层 `provider`,也不返回 `screenColorHex`、`mattingProvider`、`mattingModel`;owner 与后台审计读取不受影响。 ## 后续扩展点 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 8313efa46..81668b304 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -618,7 +618,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - Rust 结构体:`EditorProjectResource` - 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` - 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind`、`generation_inputs_json` 和历史 `public_showcase_enabled`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。`public_showcase_enabled` 只保留旧接口兼容,不再作为 `/creation` 的 `陶泥儿精选` 事实源;精选公开改由账号级生成素材提交 `editor_showcase_asset` 审核决定。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存稳定 `resource_id` 布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot`、`source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 layout 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。账号级素材删除不级联删除该表,避免历史画布丢图。结构化 canvas 的几何、层级、分组和资源引用以 `editor_canvas_layer` 为权威,生成器对象以 `editor_canvas_generation_dialog` 为权威;legacy canvas 才在 2 MiB 上限内从 `editor_canvas.layers_json` 兼容读取。新写入不再把素材生成输入快照作为图层布局真相保存。历史普通图层缺资源只能由 migration operator 调用 `repair_editor_canvas_resources_and_return` 定向修复:procedure 每次只处理一个尚无迁移记录的 legacy canvas,校验 owner/project、revision、canvas/project 两份 raw layout SHA-256、精确 layer/resource/sourceResourceId、同工程替换资源与 private asset_object 谱系;图片只替换引用,音频只恢复经核验的 `420x120` 项目资源行。运维入口 `npm run spacetime:editor-canvas-resources:repair` 默认 dry-run,apply 必须绑定 plan SHA-256 并在成功后自动复核 already-repaired,禁止手工 SQL 绕过事务 guard。 -- `generation_inputs_json` 包络契约:`fields` / `references` 是图片信息读取的用户可见生成输入快照;顶层允许保存后端内部结果扩展。现有 `screenColorHex` 保存实际背景色,角色、图标图集和 UI 图集抠图派生资产使用 `mattingProvider` / `mattingModel` 保存实际成功的处理后端与模型。BgFilter 保存本次 `seg_model`,阿里云通用抠图保存 `Aliyun Matting / segment-common-image`,本地键色保存 `Genarrative Local / screen-color-keying`。这些顶层字段不写入 `fields`、不在图片信息展示,也不具有保密边界,原始管理接口按既有权限仍可读取。 +- `generation_inputs_json` 包络契约:`fields` / `references` 是图片信息读取的用户可见生成输入快照;顶层允许保存后端内部结果扩展。现有 `screenColorHex` 保存实际背景色,角色、图标图集和 UI 图集抠图派生资产使用 `mattingProvider` / `mattingModel` 保存实际成功的处理后端与模型。BgFilter 保存本次 `seg_model`,阿里云通用抠图保存 `Aliyun Matting / segment-common-image`,本地键色保存 `Genarrative Local / screen-color-keying`。这些顶层字段不写入 `fields`、不在图片信息展示,原始素材 owner、精选提交响应和后台管理接口按既有权限仍可读取;匿名 `GET /api/editor/showcase/resources` 必须在 public payload 边界删除 `screenColorHex`、`mattingProvider`、`mattingModel`,同时省略素材顶层 `provider`。过滤只作用于公开响应,不修改素材或精选快照,因此历史数据无需迁移。 - 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。 ### `editor_asset_folder` diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 1fd186264..306f84447 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -899,6 +899,7 @@ pub struct EditorProjectResourcePayload { prompt: Option, actual_prompt: Option, model: Option, + #[serde(skip_serializing_if = "Option::is_none")] provider: Option, task_id: Option, source_resource_id: Option, @@ -6338,9 +6339,22 @@ fn editor_project_resource_public_payload_from_record( resolve_editor_project_resource_public_author(state, payload.owner_user_id.as_str()); payload.author_display_name = display_name; payload.author_public_user_code = public_user_code; + payload.provider = None; + payload.generation_inputs = payload + .generation_inputs + .map(sanitize_editor_public_generation_inputs); payload } +fn sanitize_editor_public_generation_inputs(mut value: Value) -> Value { + if let Some(object) = value.as_object_mut() { + for field in ["screenColorHex", "mattingProvider", "mattingModel"] { + object.remove(field); + } + } + value +} + fn resolve_editor_project_resource_public_author( state: &AppState, owner_user_id: &str, @@ -9079,6 +9093,60 @@ mod tests { assert_eq!(value["authorPublicUserCode"], json!("SY-00000042")); } + #[test] + fn editor_project_resource_public_payload_removes_internal_generation_metadata() { + let state = AppState::new(AppConfig::default()).expect("state should build"); + let record = EditorShowcaseAssetRecord { + provider: Some("BgFilter".to_string()), + generation_inputs: Some(json!({ + "fields": [{"title": "处理阶段", "value": "角色抠图"}], + "references": [{"refType": "asset", "refId": "asset-source"}], + "screenColorHex": "#CFEFFF", + "mattingProvider": "BgFilter", + "mattingModel": "birefnet" + })), + ..test_editor_showcase_asset_record("user-author-1") + }; + + let owner_payload = serde_json::to_value( + editor_project_resource_payload_from_showcase_record(record.clone()), + ) + .expect("owner payload should serialize"); + assert_eq!(owner_payload["provider"], json!("BgFilter")); + assert_eq!( + owner_payload["generationInputs"]["screenColorHex"], + json!("#CFEFFF") + ); + assert_eq!( + owner_payload["generationInputs"]["mattingProvider"], + json!("BgFilter") + ); + assert_eq!( + owner_payload["generationInputs"]["mattingModel"], + json!("birefnet") + ); + + let public_payload = serde_json::to_value( + editor_project_resource_public_payload_from_record(&state, record), + ) + .expect("public payload should serialize"); + assert!(public_payload.get("provider").is_none()); + let generation_inputs = public_payload["generationInputs"] + .as_object() + .expect("generation inputs should stay object"); + assert!(!generation_inputs.contains_key("screenColorHex")); + assert!(!generation_inputs.contains_key("mattingProvider")); + assert!(!generation_inputs.contains_key("mattingModel")); + assert_eq!( + generation_inputs["fields"], + json!([{"title": "处理阶段", "value": "角色抠图"}]) + ); + assert_eq!( + generation_inputs["references"], + json!([{"refType": "asset", "refId": "asset-source"}]) + ); + } + #[test] fn editor_project_resource_public_payload_falls_back_to_public_user_code() { let state = AppState::new(AppConfig::default()).expect("state should build"); -- 2.52.0 From 915eb4b739f5be22058c4fb66a81d0b3a30d262e Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 31 Jul 2026 02:43:05 +0000 Subject: [PATCH 05/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E5=86=85=E9=83=A8=E6=A8=A1=E5=9E=8B=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 素材与图层搜索排除内部处理模型和 Provider。 补充模型层与侧栏回归测试,确认审计元数据不被删除。 同步图片画布搜索契约与项目决策记录。 --- .../shared-memory/decision-log.md | 10 ++ ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 4 +- .../ImageCanvasAssetLibraryModel.test.ts | 75 +++++++++++++++ .../ImageCanvasAssetLibraryModel.ts | 14 ++- .../ImageCanvasSidebarView.test.tsx | 96 +++++++++++++++++++ 5 files changed, 193 insertions(+), 6 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e823c05c6..c28d9948e 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -26,6 +26,16 @@ --- +## 2026-07-30 图片画布搜索不得索引内部模型和 Provider + +- 背景:素材和图层详情虽已把内部处理模型显示为 `-`,搜索仍索引原始 `model` 与 `provider`,导致 `BgFilter complex`、`segment-common-image`、`BgFilter` 或 `Aliyun Matting` 等隐藏信息可被查询命中,并出现“命中但无可见匹配字段”的异常体验。 +- 决策:素材与图层搜索只索引用户可见生成模型;统一复用 `isEditorInternalProcessingModel(...)` 排除内部处理模型,并完全排除 `provider`。该规则只约束前端临时搜索值,不删除或改写素材、图层及后端保存的原始审计字段;`gpt-image-2`、`audio1.0` 等正常模型继续支持搜索。 +- 影响范围:`ImageCanvasAssetLibraryModel.ts` 的素材与图层搜索值、图片画布素材 / 图层侧栏搜索和对应前端架构文档;不改变持久化、详情展示、删除、移动或画布保存行为。 +- 验证方式:模型单测覆盖内部模型和 Provider 不命中、正常模型继续命中且原对象元数据不变;侧栏交互测试覆盖素材与图层两类入口。运行对应 Vitest、`npm run typecheck`、`npm run lint:eslint`、`npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 + +--- + ## 2026-07-29 图集切片必须受前置容量和有界 CPU 保护 - 背景:图标与 UI 图集的 alpha 连通域识别会在 async handler 上同步执行;原始连通域合并采用全量两两比较,`64` 个输出限制又晚于排序、裁剪和 PNG 编码。碎块或噪点图会放大 CPU 与内存成本,手动拆分、图标自动拆分和 UI 提取都受影响。另一方面,图标与 UI 的 Alpha 尺寸恢复、provider 原图回读或透明图解码失败此前只记日志,仍会把不可信透明图持久化并拆分。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 504cdf427..e6e1161da 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -164,8 +164,8 @@ - 音频 / 视频素材卡和画布媒体图层必须提供稳定的非文字视觉预览:优先使用 `thumbnailSrc` / 视频 `poster`,没有真实首帧或音频封面时使用由媒体类型、素材名和地址派生的确定性视觉底图。视频图层使用原生 `