diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 01eed9173..cd7fcc2f7 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6795,7 +6795,7 @@ - `editor_project_icon.rs` 独立承接图标规范、图标 spritesheet、自动切片和手工拆分。普通图片模块只暴露可复用的 prompt builder 执行入口与强类型 provider 请求分流;nanobanana、无参考图 generation、有参考图 edit 的选择集中在同一个 helper,图标图集复用该 helper,不复制 provider 分支。 - 图标规范生成的可选参考图与图标图集的必选主规范统一使用 `referenceId`,只接受当前 owner 的项目资源 ID 或账号素材 ID,不接受 objectKey、URL 或临时 key。SpacetimeDB `resolve_editor_reference_and_return` 因而只接收 `reference_id` 并按两张表主键查询,删除 `image_src` 二级索引。图标图集的普通附加参考图 `referenceImageSrcs` 仍允许 owned objectKey、项目资源 ID 或素材 ID,并继续走既有 owner 校验与真实图片下载链。 - External v1 图标图集请求与 OpenAPI 同步改为必填 `referenceId`。前端和 Editor Agent 优先传正式 resource ID,其次传 asset ID;本地临时 ID、objectKey 和图片地址不得回退成主规范引用,未登记时明确失败并要求先上传或登记。 -- `resolve_editor_reference_and_return` 的歧义只在当前 owner 范围内判断:两张表先按 `owner_user_id` 过滤,同名但属于其它账号的行不阻断合法引用。记录存在 `asset_object_id` 时按该 ID 定位并同时核对 bucket、object key 与 owner,只有缺少 ID 的兼容旧行才按位置查询。图标规范入队 / inline 预检只验证这组行与对象元数据,最终共享图片执行器再下载一次参考图正文,避免同一引用在入队、worker 预检和生成阶段重复下载。 +- `resolve_editor_reference_and_return` 的歧义只在当前 owner 范围内判断:两张表先按 `owner_user_id` 过滤,同名但属于其它账号的行不阻断合法引用。记录存在 `asset_object_id` 时按该 ID 定位并同时核对 bucket、object key 与 owner,只有缺少 ID 的兼容旧行才按位置查询。图标规范入队 / inline 预检只验证这组行与对象元数据;inline 与 worker 共用的最终执行入口还必须在把 `referenceId` 转入通用图片请求前再次执行同一 ID / owner 校验。最终共享图片执行器再下载一次参考图正文,避免同一引用在入队、worker 预检和生成阶段重复下载。 - 关联:`server-rs/crates/api-server/src/editor_project_icon.rs`、`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`docs/openapi/genarrative-external-v1.openapi.json`。 ## 2026-08-06 音效与背景音乐恢复共享音频 Composer diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 01c83e2b5..3690fa81b 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -16204,6 +16204,22 @@ mod tests { ); } + #[test] + fn icon_spec_execution_validates_id_reference_before_shared_image_dispatch() { + let source = include_str!("editor_project_icon.rs"); + + assert_function_contains_in_order( + source, + "pub(crate) async fn generate_icon_spec_for_owner", + "fn validate_icon_spec_request_fields", + &[ + "validate_icon_spec_request_fields(", + "validate_icon_spec_reference_for_owner(", + "build_icon_spec_image_request(", + ], + ); + } + #[test] fn image_generation_preflight_checks_references_provider_and_pricing() { let source = concat!( diff --git a/server-rs/crates/api-server/src/editor_project_icon.rs b/server-rs/crates/api-server/src/editor_project_icon.rs index d84d77041..78aa5dd26 100644 --- a/server-rs/crates/api-server/src/editor_project_icon.rs +++ b/server-rs/crates/api-server/src/editor_project_icon.rs @@ -551,6 +551,7 @@ pub(crate) async fn generate_icon_spec_for_owner( mut payload: EditorIconSpecGenerationRequest, ) -> Result, AppError> { validate_icon_spec_request_fields(&mut payload)?; + validate_icon_spec_reference_for_owner(state, &caller, &payload).await?; let request_param = payload.request_param.clone(); let has_reference_image = payload.reference_id.is_some(); let generation_inputs = payload.generation_inputs.clone(); diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts index dc2516774..7cd0757be 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts @@ -1040,8 +1040,14 @@ describe('ImageCanvasGenerationDialogModel', () => { action: 'audio.sound-effect.generate', fields: [ { id: 'prompt', title: '非 prompt 标题', value: '短促点击音' }, - { id: 'model', title: '非 model 标题', value: 'audio1.0' }, + { + id: 'model', + title: '非 model 标题', + value: 'eleven_text_to_sound_v2', + }, + { id: 'durationMode', title: '非模式标题', value: 'manual' }, { id: 'durationSeconds', title: '非 duration 标题', value: 7 }, + { id: 'loop', title: '非 Loop 标题', value: true }, ], references: [], }, @@ -1050,8 +1056,10 @@ describe('ImageCanvasGenerationDialogModel', () => { ).toMatchObject({ mode: 'audio-sound-effect', prompt: '短促点击音', - soundModel: 'audio1.0', + soundModel: 'eleven_text_to_sound_v2', + soundDurationMode: 'manual', soundDurationSeconds: 7, + soundLoop: true, }); });