收紧图片编辑主来源契约

将站内与 External v1 图片编辑请求统一为必填 sourceReferenceId
按业务引用解析权威对象和类型并校验目标图层绑定
使用版本化队列快照并在 worker 执行前复核身份与类型漂移
安全迁移历史队列载荷且不再按 objectKey 反查或信任旧 assetKind
让画布 Agent 使用 ImageMetadata.reference_id 并拒绝未登记图片
同步 OpenAPI 400 条件、异步语义与契约测试
This commit is contained in:
2026-08-07 14:03:55 +08:00
parent b200899883
commit 0cca0adcb1
9 changed files with 873 additions and 193 deletions
@@ -662,8 +662,9 @@ pub async fn edit_external_editor_image(
Extension(request_context): Extension<RequestContext>,
Extension(principal): Extension<ExternalApiPrincipal>,
headers: HeaderMap,
Json(payload): Json<EditorImageEditRequest>,
payload: Result<Json<EditorImageEditRequest>, JsonRejection>,
) -> Result<Response, AppError> {
let Json(payload) = parse_editor_generation_json_payload(payload)?;
require_scope(&principal, SCOPE_EDITOR_IMAGE_GENERATE)?;
let idempotency_key = require_idempotency_key(&headers)?;
let project_id = payload.project_id.clone();
@@ -1560,7 +1561,8 @@ mod tests {
assert!(
parsed["components"]["schemas"]["EditorImageEditRequest"]["required"]
.as_array()
.is_some_and(|required| !required.contains(&json!("priceMudPoints")))
.is_some_and(|required| required.contains(&json!("sourceReferenceId"))
&& !required.contains(&json!("priceMudPoints")))
);
assert!(
parsed["components"]["schemas"]["EditorImageEditRequest"]["properties"]
@@ -1572,6 +1574,38 @@ mod tests {
.get("targetLayerId")
.is_some()
);
let image_edit_schema =
&parsed["components"]["schemas"]["EditorImageEditRequest"]["properties"];
for legacy_field in ["sourceImageSrc", "sourceResourceId", "assetKind"] {
assert!(
image_edit_schema.get(legacy_field).is_none(),
"{legacy_field}"
);
}
assert!(
image_edit_schema["sourceReferenceId"]["description"]
.as_str()
.is_some_and(|description| description.contains("项目资源 ID 或素材 ID")
&& description.contains("objectKey")
&& description.contains("未登记上传对象")
&& description.contains("400"))
);
assert_eq!(
parsed["components"]["schemas"]["EditorImageEditRequest"]["additionalProperties"],
json!(false)
);
let image_edit_operation = &parsed["paths"]["/api/external/v1/editor/images/edits"]["post"];
assert!(
image_edit_operation["description"]
.as_str()
.is_some_and(|description| description.contains("targetLayerId")
&& description.contains("sourceReferenceId")
&& description.contains("assetObjectId")
&& description.contains("bucket/objectKey")
&& description.contains("未知类型")
&& description.contains("返回 400"))
);
assert!(image_edit_operation["responses"].get("400").is_some());
for (schema, max_items) in [
("EditorImageEditRequest", 8),
("EditorIconSpritesheetGenerationRequest", 8),