收紧图片编辑主来源契约
将站内与 External v1 图片编辑请求统一为必填 sourceReferenceId 按业务引用解析权威对象和类型并校验目标图层绑定 使用版本化队列快照并在 worker 执行前复核身份与类型漂移 安全迁移历史队列载荷且不再按 objectKey 反查或信任旧 assetKind 让画布 Agent 使用 ImageMetadata.reference_id 并拒绝未登记图片 同步 OpenAPI 400 条件、异步语义与契约测试
This commit is contained in:
@@ -1065,6 +1065,7 @@
|
||||
],
|
||||
"operationId": "editExternalEditorImage",
|
||||
"summary": "重绘/调整编辑器图片素材",
|
||||
"description": "主来源只接受当前账号已登记的项目资源 ID 或素材 ID(sourceReferenceId);objectKey、URL、Data URL 与 Blob URL 即使归属当前账号也返回 400。服务端从命中的业务记录派生 canonical objectKey、assetObjectId 与权威类型,只允许普通静态图片(类型为 null)、spec、character、icon-spritesheet、icon-spec、publication-material 或 ui-design,其他及未知类型返回 400。提供 targetLayerId 时必须同时提供 projectId,目标图层必须关联有效项目资源;双方都有 assetObjectId 时必须相同,否则回退比较 canonical bucket/objectKey。同一对象的来源默认类型与目标资源默认类型冲突、目标有效类型或媒体类型不允许、来源或目标不存在/越权/缺少对象时均返回 400,任务不会入队。referenceImageSrcs 仍只作为辅助参考图。",
|
||||
"security": [
|
||||
{
|
||||
"ExternalApiKey": []
|
||||
@@ -3338,28 +3339,24 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"prompt",
|
||||
"sourceImageSrc"
|
||||
"sourceReferenceId"
|
||||
],
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sourceImageSrc": {
|
||||
"sourceReferenceId": {
|
||||
"type": "string",
|
||||
"description": "待重绘/调整图片的稳定引用:当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。"
|
||||
"minLength": 1,
|
||||
"description": "待编辑主来源的业务 ID,只接受当前账号已登记的项目资源 ID 或素材 ID。objectKey、普通 URL、签名 URL、Data URL、Blob URL 和未登记上传对象均返回 400;上传对象必须先登记为项目资源或素材。服务端从命中记录派生 canonical objectKey、assetObjectId 与权威类型。"
|
||||
},
|
||||
"projectId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"assetKind": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"description": "项目上下文。提供 targetLayerId 时必须同时提供非空 projectId,否则返回 400。"
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
@@ -3376,18 +3373,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sourceResourceId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"targetLayerId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "带 projectId 且未提供 canvasCompletion 时,服务端用生成结果替换该画布图层。"
|
||||
"description": "目标画布图层。提供时必须同时提供 projectId,且图层必须关联有效项目资源。来源与目标都有 assetObjectId 时按 ID 比较;任一缺失时回退比较 canonical bucket/objectKey。来源记录默认类型必须与目标资源默认类型一致,最终类型取 assetKindOverride 或目标资源类型,且媒体类型必须为图片;违反任一条件返回 400。未提供 canvasCompletion 时,生成结果替换该图层。"
|
||||
},
|
||||
"size": {
|
||||
"type": "string"
|
||||
|
||||
@@ -2142,7 +2142,7 @@ mod tests {
|
||||
let app = build_router(state);
|
||||
let request_body = serde_json::json!({
|
||||
"prompt": "快速编辑图片",
|
||||
"sourceImageSrc": "data:image/png;base64,AAAA",
|
||||
"sourceReferenceId": "data:image/png;base64,AAAA",
|
||||
"size": "1024x1024",
|
||||
"model": "gpt-image-2"
|
||||
})
|
||||
@@ -2170,7 +2170,7 @@ mod tests {
|
||||
.to_bytes();
|
||||
let body_text = String::from_utf8_lossy(&body);
|
||||
assert!(
|
||||
body_text.contains("先上传 OSS"),
|
||||
body_text.contains("只接受已登记的项目资源 ID 或素材 ID"),
|
||||
"handler should reject inline editor edit sources: {body_text}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub fn build_tool_context(document: &EditorAgentConversationMessagesDocument) ->
|
||||
let image_id = ImageId::from_data_key(&data_key);
|
||||
let metadata = ImageMetadata {
|
||||
data_key,
|
||||
reference_id: Some(a.reference_id.clone()),
|
||||
image_src: a.image_src.clone(),
|
||||
object_key: a.object_key.clone(),
|
||||
thumbnail_src: a.thumbnail_src.clone(),
|
||||
@@ -29,6 +30,7 @@ pub fn build_tool_context(document: &EditorAgentConversationMessagesDocument) ->
|
||||
let image_id = ImageId::from_data_key(&data_key);
|
||||
let metadata = ImageMetadata {
|
||||
data_key,
|
||||
reference_id: img.resource_id.clone(),
|
||||
image_src: img.image_src.clone(),
|
||||
object_key: img.object_key.clone(),
|
||||
thumbnail_src: img.thumbnail_src.clone(),
|
||||
|
||||
@@ -660,27 +660,30 @@ impl EditorAgentTool for EditImageTool {
|
||||
) -> Result<PreparedEditorAgentJob, EditorAgentToolError> {
|
||||
let price_mud_points = self.pricing(context.pricing, args)?;
|
||||
let args: EditImageToolArgs = parse_invalid_args(Self::NAME, args)?;
|
||||
let source_image_src = self
|
||||
let source_reference_id = self
|
||||
.context
|
||||
.image_data_key(&args.object_image_id)
|
||||
.ok_or_else(|| EditorAgentToolError::invalid_args("object image not found"))?
|
||||
.image_metadata(&args.object_image_id)
|
||||
.and_then(|metadata| metadata.reference_id.as_deref())
|
||||
.ok_or_else(|| {
|
||||
EditorAgentToolError::invalid_args(
|
||||
"object image is not a registered resource or asset",
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
let reference_image_srcs = resolve_image_ids(&args.reference_image_ids, &self.context)?;
|
||||
let title = args.prompt.clone();
|
||||
let payload = EditorImageEditRequest {
|
||||
prompt: args.prompt,
|
||||
source_image_src,
|
||||
source_reference_id,
|
||||
size: None,
|
||||
model: Some(args.model),
|
||||
aspect_ratio: None,
|
||||
image_size: None,
|
||||
reference_image_srcs: Some(reference_image_srcs),
|
||||
project_id: Some(context.conversation.project_id.clone()),
|
||||
asset_kind: None,
|
||||
generation_inputs: context.generation_inputs("用户指令", &title),
|
||||
asset_folder_id: Some("project".to_string()),
|
||||
asset_label: Some(title.clone()),
|
||||
source_resource_id: None,
|
||||
target_layer_id: None,
|
||||
canvas_completion: Some(build_editor_agent_canvas_completion(
|
||||
context.project,
|
||||
@@ -1184,6 +1187,7 @@ mod tests {
|
||||
},
|
||||
ImageMetadata {
|
||||
data_key: format!("generated/{image_id}.png"),
|
||||
reference_id: Some(format!("resource-{image_id}")),
|
||||
image_src: format!("/api/assets/read/generated/{image_id}.png"),
|
||||
object_key: Some(format!("generated/{image_id}.png")),
|
||||
thumbnail_src: Some(format!("/api/assets/read/generated/{image_id}-thumb.png")),
|
||||
@@ -1528,7 +1532,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_job_uses_empty_asset_kind_for_ordinary_static_image() {
|
||||
fn edit_image_job_uses_registered_reference_id_only() {
|
||||
let pricing = pricing();
|
||||
let conversation = conversation();
|
||||
let project = project();
|
||||
@@ -1554,11 +1558,55 @@ mod tests {
|
||||
.expect("edit job should prepare");
|
||||
|
||||
assert_eq!(job.job_kind, EDITOR_IMAGE_EDIT_JOB_KIND);
|
||||
assert!(job.payload["assetKind"].is_null());
|
||||
assert_eq!(job.payload["sourceReferenceId"], "resource-image-1");
|
||||
assert!(job.payload.get("sourceImageSrc").is_none());
|
||||
assert!(job.payload.get("sourceResourceId").is_none());
|
||||
assert!(job.payload.get("assetKind").is_none());
|
||||
assert_eq!(job.payload["projectId"], "project-1");
|
||||
assert!(job.payload["targetLayerId"].is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_job_rejects_image_without_registered_reference_id() {
|
||||
let pricing = pricing();
|
||||
let conversation = conversation();
|
||||
let project = project();
|
||||
let mut context = context_with_image("image-1");
|
||||
context
|
||||
.images
|
||||
.values_mut()
|
||||
.next()
|
||||
.expect("test image should exist")
|
||||
.reference_id = None;
|
||||
let tool =
|
||||
editor_agent_tool(EditImageTool::NAME, &context).expect("edit tool should resolve");
|
||||
let args = tool
|
||||
.validate_args(&json!({
|
||||
"object_image_id": "image-1",
|
||||
"prompt": "改成蓝色"
|
||||
}))
|
||||
.expect("edit args should validate");
|
||||
|
||||
let error = match tool.prepare_job(
|
||||
&args,
|
||||
&EditorAgentPrepareJobContext {
|
||||
conversation: &conversation,
|
||||
project: &project,
|
||||
message_id: 7,
|
||||
pricing: &pricing,
|
||||
},
|
||||
) {
|
||||
Ok(_) => panic!("unregistered image should fail closed"),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
assert!(error.is_invalid_args());
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"object image is not a registered resource or asset"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dyn_job_preparation_keeps_queue_contract_and_defaults() {
|
||||
let pricing = pricing();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
|
||||
@@ -40,13 +40,13 @@ use crate::{
|
||||
EDITOR_UI_DESIGN_ASSET_EXTRACTION_JOB_KIND, EDITOR_VIDEO_GENERATION_JOB_KIND,
|
||||
},
|
||||
editor_project::{
|
||||
EDITOR_AGENT_LEGACY_IMAGE_EDIT_ASSET_KIND, EDITOR_GENERATION_MULTIPLE_WARNINGS_CODE,
|
||||
EDITOR_GENERATION_MULTIPLE_WARNINGS_CODE, EDITOR_IMAGE_EDIT_QUEUE_PAYLOAD_VERSION,
|
||||
EditorBackgroundRemovalRequest, EditorGenerationCaller, EditorGenerationPhaseReporter,
|
||||
EditorIconSpritesheetGenerationRequest, EditorImageEditRequest,
|
||||
EditorImageGenerationRequest, EditorUiDesignAssetExtractionRequest,
|
||||
edit_editor_image_for_owner, extract_editor_ui_design_assets_for_owner,
|
||||
generate_editor_icon_spritesheet_for_owner, generate_editor_image_for_owner,
|
||||
remove_editor_image_background_for_owner,
|
||||
EditorIconSpritesheetGenerationRequest, EditorImageEditQueuePayload,
|
||||
EditorImageEditRequest, EditorImageEditResolvedSource, EditorImageGenerationRequest,
|
||||
EditorUiDesignAssetExtractionRequest, edit_editor_image_for_owner_with_source_snapshot,
|
||||
extract_editor_ui_design_assets_for_owner, generate_editor_icon_spritesheet_for_owner,
|
||||
generate_editor_image_for_owner, remove_editor_image_background_for_owner,
|
||||
},
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
@@ -802,11 +802,12 @@ async fn process_external_generation_job_once(
|
||||
}
|
||||
};
|
||||
let request_context = worker_request_context(&job, provider_deadline);
|
||||
match edit_editor_image_for_owner(
|
||||
match edit_editor_image_for_owner_with_source_snapshot(
|
||||
&state,
|
||||
&request_context,
|
||||
editor_generation_worker_caller(&worker_id, &job)?,
|
||||
payload,
|
||||
payload.request,
|
||||
payload.source.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1138,19 +1139,78 @@ fn worker_request_context(
|
||||
.with_external_call_deadline(provider_deadline)
|
||||
}
|
||||
|
||||
struct ParsedEditorImageEditWorkerPayload {
|
||||
request: EditorImageEditRequest,
|
||||
source: Option<EditorImageEditResolvedSource>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LegacyEditorImageEditWorkerPayload {
|
||||
prompt: String,
|
||||
source_image_src: String,
|
||||
size: Option<String>,
|
||||
model: Option<String>,
|
||||
aspect_ratio: Option<String>,
|
||||
image_size: Option<String>,
|
||||
reference_image_srcs: Option<Vec<String>>,
|
||||
project_id: Option<String>,
|
||||
generation_inputs: Option<Value>,
|
||||
asset_folder_id: Option<String>,
|
||||
asset_label: Option<String>,
|
||||
source_resource_id: Option<String>,
|
||||
target_layer_id: Option<String>,
|
||||
canvas_completion: Option<shared_contracts::assets::EditorCanvasGenerationCompletionPayload>,
|
||||
}
|
||||
|
||||
fn parse_editor_image_edit_worker_payload(
|
||||
job: &ExternalGenerationJobRecord,
|
||||
) -> Result<EditorImageEditRequest, String> {
|
||||
let mut payload: EditorImageEditRequest =
|
||||
) -> Result<ParsedEditorImageEditWorkerPayload, String> {
|
||||
if let Ok(payload) =
|
||||
serde_json::from_str::<EditorImageEditQueuePayload>(job.request_payload_json.as_str())
|
||||
{
|
||||
if payload.version != EDITOR_IMAGE_EDIT_QUEUE_PAYLOAD_VERSION {
|
||||
return Err(format!(
|
||||
"图片画布改图任务载荷版本不受支持:{}",
|
||||
payload.version
|
||||
));
|
||||
}
|
||||
return Ok(ParsedEditorImageEditWorkerPayload {
|
||||
request: payload.request,
|
||||
source: Some(payload.source),
|
||||
});
|
||||
}
|
||||
let legacy: LegacyEditorImageEditWorkerPayload =
|
||||
serde_json::from_str(job.request_payload_json.as_str())
|
||||
.map_err(|error| format!("图片画布改图任务参数解析失败:{error}"))?;
|
||||
if is_editor_agent_generation_job(job)
|
||||
&& payload.asset_kind.as_deref().map(str::trim)
|
||||
== Some(EDITOR_AGENT_LEGACY_IMAGE_EDIT_ASSET_KIND)
|
||||
{
|
||||
payload.asset_kind = None;
|
||||
let source_reference_id = legacy
|
||||
.source_resource_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| legacy.source_image_src.trim())
|
||||
.to_string();
|
||||
if source_reference_id.is_empty() {
|
||||
return Err("历史图片画布改图任务缺少可迁移的业务引用 ID".to_string());
|
||||
}
|
||||
Ok(payload)
|
||||
Ok(ParsedEditorImageEditWorkerPayload {
|
||||
request: EditorImageEditRequest {
|
||||
prompt: legacy.prompt,
|
||||
source_reference_id,
|
||||
size: legacy.size,
|
||||
model: legacy.model,
|
||||
aspect_ratio: legacy.aspect_ratio,
|
||||
image_size: legacy.image_size,
|
||||
reference_image_srcs: legacy.reference_image_srcs,
|
||||
project_id: legacy.project_id,
|
||||
generation_inputs: legacy.generation_inputs,
|
||||
asset_folder_id: legacy.asset_folder_id,
|
||||
asset_label: legacy.asset_label,
|
||||
target_layer_id: legacy.target_layer_id,
|
||||
canvas_completion: legacy.canvas_completion,
|
||||
},
|
||||
source: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn editor_generation_worker_caller(
|
||||
@@ -1755,39 +1815,48 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_agent_image_edit_payload_reaches_worker_gate_with_empty_asset_kind() {
|
||||
fn editor_image_edit_worker_parses_versioned_snapshot_payload() {
|
||||
let mut job = external_generation_job_record_fixture(Some("lease-1"));
|
||||
job.job_kind = EDITOR_IMAGE_EDIT_JOB_KIND.to_string();
|
||||
job.dedupe_key = "editor-agent:conversation-1:7:edit-image".to_string();
|
||||
job.request_payload_json = json!({
|
||||
"prompt": "改成蓝色",
|
||||
"sourceImageSrc": "users/user-1/source.png",
|
||||
"projectId": "project-1",
|
||||
"assetKind": null,
|
||||
"generationInputs": { "source": "editor-agent" }
|
||||
"version": 1,
|
||||
"request": {
|
||||
"prompt": "改成蓝色",
|
||||
"sourceReferenceId": "resource-source"
|
||||
},
|
||||
"source": {
|
||||
"referenceKind": "project-resource",
|
||||
"sourceReferenceId": "resource-source",
|
||||
"resourceId": "resource-source",
|
||||
"assetId": null,
|
||||
"assetObjectId": "object-source",
|
||||
"bucket": "bucket",
|
||||
"objectKey": "generated/source.png",
|
||||
"sourceAssetKind": "character",
|
||||
"effectiveAssetKind": "character",
|
||||
"mediaType": "image",
|
||||
"targetResourceId": null
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let payload = parse_editor_image_edit_worker_payload(&job)
|
||||
.expect("新 Agent 图片编辑 payload 应由 worker 解析");
|
||||
assert!(payload.asset_kind.is_none());
|
||||
assert!(
|
||||
crate::editor_project::ensure_editor_image_edit_source_kind_allowed(
|
||||
payload.asset_kind.as_deref(),
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
.expect("versioned image edit payload should parse");
|
||||
assert_eq!(payload.request.source_reference_id, "resource-source");
|
||||
assert_eq!(
|
||||
payload.source.expect("snapshot should exist").object_key,
|
||||
"generated/source.png"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_editor_agent_image_edit_kind_is_normalized_only_for_trusted_job() {
|
||||
fn legacy_editor_image_edit_payload_migrates_only_business_ids() {
|
||||
let mut job = external_generation_job_record_fixture(Some("lease-1"));
|
||||
job.job_kind = EDITOR_IMAGE_EDIT_JOB_KIND.to_string();
|
||||
job.dedupe_key = "editor-agent:conversation-1:7:edit-image".to_string();
|
||||
job.request_payload_json = json!({
|
||||
"prompt": "改成蓝色",
|
||||
"sourceImageSrc": "users/user-1/source.png",
|
||||
"sourceImageSrc": "raw/generated/source.png",
|
||||
"sourceResourceId": "resource-source",
|
||||
"projectId": "project-1",
|
||||
"assetKind": "editor_agent_edit_image",
|
||||
"generationInputs": { "source": "editor-agent" }
|
||||
@@ -1796,30 +1865,25 @@ mod tests {
|
||||
|
||||
let legacy_agent_payload = parse_editor_image_edit_worker_payload(&job)
|
||||
.expect("历史 Agent 图片编辑 payload 应由 worker 解析");
|
||||
assert!(legacy_agent_payload.asset_kind.is_none());
|
||||
assert!(
|
||||
crate::editor_project::ensure_editor_image_edit_source_kind_allowed(
|
||||
legacy_agent_payload.asset_kind.as_deref(),
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
job.dedupe_key = "editor:image-edit:project-1:request-1".to_string();
|
||||
let untrusted_payload = parse_editor_image_edit_worker_payload(&job)
|
||||
.expect("普通画布图片编辑 payload 应由 worker 解析");
|
||||
assert_eq!(
|
||||
untrusted_payload.asset_kind.as_deref(),
|
||||
Some(EDITOR_AGENT_LEGACY_IMAGE_EDIT_ASSET_KIND)
|
||||
legacy_agent_payload.request.source_reference_id,
|
||||
"resource-source"
|
||||
);
|
||||
assert!(
|
||||
crate::editor_project::ensure_editor_image_edit_source_kind_allowed(
|
||||
untrusted_payload.asset_kind.as_deref(),
|
||||
None,
|
||||
)
|
||||
.is_err(),
|
||||
"普通请求不得用历史 Agent synthetic assetKind 绕过白名单"
|
||||
assert!(legacy_agent_payload.source.is_none());
|
||||
|
||||
job.request_payload_json = json!({
|
||||
"prompt": "改成蓝色",
|
||||
"sourceImageSrc": "raw/generated/source.png",
|
||||
"assetKind": "character"
|
||||
})
|
||||
.to_string();
|
||||
let raw_only_payload = parse_editor_image_edit_worker_payload(&job)
|
||||
.expect("raw-only legacy payload should migrate without object-key lookup");
|
||||
assert_eq!(
|
||||
raw_only_payload.request.source_reference_id,
|
||||
"raw/generated/source.png"
|
||||
);
|
||||
assert!(raw_only_payload.source.is_none());
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
|
||||
@@ -39,6 +39,7 @@ impl Display for ImageId {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImageMetadata {
|
||||
pub data_key: String,
|
||||
pub reference_id: Option<String>,
|
||||
pub image_src: String,
|
||||
pub object_key: Option<String>,
|
||||
pub thumbnail_src: Option<String>,
|
||||
|
||||
@@ -257,6 +257,7 @@ mod tests {
|
||||
reference_image_id.clone(),
|
||||
ImageMetadata {
|
||||
data_key: "asset://reference-image".to_string(),
|
||||
reference_id: Some("resource-reference-image".to_string()),
|
||||
image_src: "asset://reference-image".to_string(),
|
||||
object_key: None,
|
||||
thumbnail_src: None,
|
||||
|
||||
Reference in New Issue
Block a user