diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 61eb43946..4e64b88ae 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6605,6 +6605,7 @@ - 2026-08-08 修订:`scene` 是单张静态图片素材,加入前端快速编辑正向白名单和 api-server 权威来源白名单;编辑结果继续保留 `scene`。用户标签覆盖侧的前端菜单与 SpacetimeDB 结构化布局白名单也必须显式覆盖 `scene`。通用图片生成接口仍拒绝 `scene`,避免绕过结构化场景生成契约。 - 2026-08-08 修订:普通静态图片的正式语义值固定为 `assetKind=null/None`;`"image"` 只属于前端派生 `CanvasMediaType`、MIME / content type 和生成参考媒体类型,不是 `CanvasAssetKind`,不得由上传、生成、资源 / 素材写入或 read model 生产。API 与 SpacetimeDB storage 的读写边界把短期误写的历史 `assetKind="image"` 原位归一为空;快速编辑白名单继续直接允许空类型,不新增 `"image"` 分支。 - 2026-08-08 修订:画布 Agent 的 `edit_image` 工具内部可先形成待确认的 `EditorImageEditRequest`,但确认接口必须在通用入队前复用站内图片编辑的来源解析与目标预检,写入 `{ version, request, source }` 服务端快照。worker 只解析正式 versioned payload 与既有历史 payload,不接受当前 direct request 作为 fallback;这样未上线的 Agent 路径在生产端原位修正,不扩大消费端协议。 +- 2026-08-08 修订:图片编辑业务引用按 `objectKey` 找到权威 `asset_object` 后,若资源或素材记录同时保存了 `assetObjectId`,必须验证两者指向同一对象;不一致时在 SpacetimeDB resolver 边界失败关闭,不能把未验证的记录 ID 与已验证的对象路径组合进 snapshot。缺少 ID 的历史记录继续以 canonical `(bucket, objectKey)` 作为对象身份。 - 验证:模型测试覆盖允许与拒绝类型,工具栏和两类右键菜单测试覆盖单个拆分图标、角色动作及音频不展示,提交工作流测试覆盖单个拆分图标和角色动作绕过入口时仍拒绝;后端表驱动测试覆盖全部现役素材 / 媒体类型与未知类型,锁定图片编辑端点失败关闭。 - 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts`、`ImageCanvasSelectedLayerToolbarView.tsx`、`ImageCanvasContextMenusView.tsx`、`useImageCanvasGenerationWorkflow.ts`、`useImageCanvasGenerationSubmissionWorkflow.ts`。 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 2fec0ef5d..65db8399f 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -7680,7 +7680,14 @@ fn resolve_editor_reference_resource( resource.owner_user_id.as_str(), resource.object_key.as_deref(), )?; - verify_editor_reference_asset_object(ctx, bucket, owner_user_id, object_key.as_str())?; + verify_editor_reference_asset_object( + ctx, + "资源", + bucket, + owner_user_id, + resource.asset_object_id.as_deref(), + object_key.as_str(), + )?; let mut snapshot = resource_snapshot_from_row(resource.clone()); snapshot.object_key = Some(object_key); Ok(ResolvedEditorReference::ProjectResource(snapshot)) @@ -7698,7 +7705,14 @@ fn resolve_editor_reference_asset( asset.owner_user_id.as_str(), asset.object_key.as_deref(), )?; - verify_editor_reference_asset_object(ctx, bucket, owner_user_id, object_key.as_str())?; + verify_editor_reference_asset_object( + ctx, + "素材", + bucket, + owner_user_id, + asset.asset_object_id.as_deref(), + object_key.as_str(), + )?; let mut snapshot = asset_snapshot_from_row(ctx, asset); snapshot.object_key = Some(object_key); Ok(ResolvedEditorReference::Asset(snapshot)) @@ -7723,8 +7737,10 @@ fn validate_editor_reference_record_fields( fn verify_editor_reference_asset_object( ctx: &ReducerContext, + record_label: &str, bucket: &str, owner_user_id: &str, + recorded_asset_object_id: Option<&str>, object_key: &str, ) -> Result<(), String> { let asset_object = crate::find_asset_object_by_location( @@ -7738,6 +7754,29 @@ fn verify_editor_reference_asset_object( if asset_object.owner_user_id.as_deref().map(str::trim) != Some(owner_user_id) { return Err("编辑器引用对应的 asset_object 不属于当前用户".to_string()); } + ensure_editor_reference_asset_object_identity( + record_label, + recorded_asset_object_id, + asset_object.asset_object_id.as_str(), + ) +} + +fn ensure_editor_reference_asset_object_identity( + record_label: &str, + recorded_asset_object_id: Option<&str>, + verified_asset_object_id: &str, +) -> Result<(), String> { + let Some(recorded_asset_object_id) = recorded_asset_object_id + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + return Ok(()); + }; + if recorded_asset_object_id != verified_asset_object_id.trim() { + return Err(format!( + "编辑器{record_label}引用的 asset_object_id 与 object_key 对应对象不一致" + )); + } Ok(()) } @@ -18762,6 +18801,28 @@ mod tests { ); } + #[test] + fn editor_reference_asset_object_identity_rejects_mismatched_id_and_key_binding() { + assert!( + ensure_editor_reference_asset_object_identity( + "资源", + Some("asset-object-a"), + "asset-object-a", + ) + .is_ok() + ); + assert!( + ensure_editor_reference_asset_object_identity("素材", None, "asset-object-b").is_ok() + ); + let error = ensure_editor_reference_asset_object_identity( + "资源", + Some("asset-object-a"), + "asset-object-b", + ) + .expect_err("recorded asset object id must match the object verified by object key"); + assert!(error.contains("asset_object_id 与 object_key 对应对象不一致")); + } + #[test] fn editor_reference_resolution_uses_both_primary_keys_and_fails_on_collision() { let source = include_str!("editor_project_storage.rs");