撤销画布 Agent 图片编辑门禁改动
撤销基于调用来源全面绕过图片编辑素材白名单的实现。 为改用普通图片空素材类型及有限历史兼容恢复基线。
This commit is contained in:
@@ -6499,7 +6499,6 @@
|
||||
- 背景:画布生成结果统一显示快速编辑,但部分角色动作 / 序列帧和音频结果会进入不受支持的图片编辑链路;不同入口各自判断时也容易继续漂移。
|
||||
- 决策:快速编辑只支持普通静态图片、角色图、规范图、完整图标图集、UI 设计图、宣发图和视频。单个拆分图标、角色动作 / 序列帧、音效与背景音乐不支持;新增媒体或素材类型默认不开放。浮动工具栏、图层右键菜单、独立图片菜单、打开面板入口和提交门禁统一调用同一个正向白名单;后端图片编辑 BFF 基于目标图层的有效素材类型与媒体类型执行同一正向门禁,视频快速编辑只走视频生成接口。
|
||||
- 边界:角色动作继续通过对应的动作生成链路处理,不再把当前帧当作可快速编辑图片。
|
||||
- 2026-08-06 修订:画布 Agent 的 `edit_image` 是服务端确认后生成的独立内部工具调用,不属于画布快速编辑入口。worker 以服务端生成的 `editor-agent:` 队列 dedupe namespace 恢复可信调用来源并允许其无 `targetLayerId` 执行;普通画布与 External API 调用继续按真实目标图层失败关闭,不能通过伪造 `assetKind="editor_agent_edit_image"` 或 `generationInputs.source` 绕过白名单。
|
||||
- 验证:模型测试覆盖允许与拒绝类型,工具栏和两类右键菜单测试覆盖单个拆分图标、角色动作及音频不展示,提交工作流测试覆盖单个拆分图标和角色动作绕过入口时仍拒绝;后端表驱动测试覆盖全部现役素材 / 媒体类型与未知类型,锁定图片编辑端点失败关闭。
|
||||
- 关联:`src/components/image-editor/ImageCanvasGenerationModel.ts`、`ImageCanvasSelectedLayerToolbarView.tsx`、`ImageCanvasContextMenusView.tsx`、`useImageCanvasGenerationWorkflow.ts`、`useImageCanvasGenerationSubmissionWorkflow.ts`。
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -449,18 +449,11 @@ pub struct EditorUiDesignAssetExtractionRequest {
|
||||
pub(crate) canvas_completion: Option<EditorCanvasGenerationCompletionRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum EditorGenerationCallerKind {
|
||||
Api,
|
||||
EditorAgent,
|
||||
}
|
||||
|
||||
pub(crate) struct EditorGenerationCaller {
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) audit_subject_user_id: Option<String>,
|
||||
pub(crate) audit_project_id: Option<String>,
|
||||
pub(crate) phase_reporter: Option<EditorGenerationPhaseReporter>,
|
||||
pub(crate) kind: EditorGenerationCallerKind,
|
||||
}
|
||||
|
||||
impl EditorGenerationCaller {
|
||||
@@ -471,7 +464,6 @@ impl EditorGenerationCaller {
|
||||
owner_user_id,
|
||||
audit_project_id: None,
|
||||
phase_reporter: None,
|
||||
kind: EditorGenerationCallerKind::Api,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2878,17 +2870,6 @@ fn ensure_editor_image_edit_source_kind_allowed(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_editor_image_edit_source_kind_allowed_for_caller(
|
||||
caller_kind: EditorGenerationCallerKind,
|
||||
asset_kind: Option<&str>,
|
||||
media_type: Option<&str>,
|
||||
) -> Result<(), AppError> {
|
||||
if caller_kind == EditorGenerationCallerKind::EditorAgent {
|
||||
return Ok(());
|
||||
}
|
||||
ensure_editor_image_edit_source_kind_allowed(asset_kind, media_type)
|
||||
}
|
||||
|
||||
fn resolve_editor_image_edit_target_layer_source_kind<'a>(
|
||||
layers: &'a Value,
|
||||
resources: &'a [EditorProjectResourcePayload],
|
||||
@@ -2949,7 +2930,6 @@ fn resolve_editor_image_edit_target_layer_source_kind<'a>(
|
||||
async fn ensure_editor_image_edit_source_allowed(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
caller_kind: EditorGenerationCallerKind,
|
||||
payload: &EditorImageEditRequest,
|
||||
) -> Result<(), AppError> {
|
||||
let Some(project_id) = payload
|
||||
@@ -2958,11 +2938,7 @@ async fn ensure_editor_image_edit_source_allowed(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return ensure_editor_image_edit_source_kind_allowed_for_caller(
|
||||
caller_kind,
|
||||
payload.asset_kind.as_deref(),
|
||||
None,
|
||||
);
|
||||
return ensure_editor_image_edit_source_kind_allowed(payload.asset_kind.as_deref(), None);
|
||||
};
|
||||
let Some(target_layer_id) = payload
|
||||
.target_layer_id
|
||||
@@ -2970,11 +2946,7 @@ async fn ensure_editor_image_edit_source_allowed(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return ensure_editor_image_edit_source_kind_allowed_for_caller(
|
||||
caller_kind,
|
||||
payload.asset_kind.as_deref(),
|
||||
None,
|
||||
);
|
||||
return ensure_editor_image_edit_source_kind_allowed(payload.asset_kind.as_deref(), None);
|
||||
};
|
||||
let project = state
|
||||
.spacetime_client()
|
||||
@@ -2990,8 +2962,7 @@ async fn ensure_editor_image_edit_source_allowed(
|
||||
&project.resources,
|
||||
target_layer_id,
|
||||
)?;
|
||||
ensure_editor_image_edit_source_kind_allowed_for_caller(
|
||||
caller_kind,
|
||||
ensure_editor_image_edit_source_kind_allowed(
|
||||
asset_kind.or(payload.asset_kind.as_deref()),
|
||||
media_type,
|
||||
)
|
||||
@@ -4171,13 +4142,7 @@ pub(crate) async fn enqueue_editor_image_edit_for_owner(
|
||||
"referenceImageSrcs",
|
||||
"修改参考图",
|
||||
)?;
|
||||
ensure_editor_image_edit_source_allowed(
|
||||
state,
|
||||
caller.owner_user_id.as_str(),
|
||||
caller.kind,
|
||||
&payload,
|
||||
)
|
||||
.await?;
|
||||
ensure_editor_image_edit_source_allowed(state, caller.owner_user_id.as_str(), &payload).await?;
|
||||
let generation_options = normalize_editor_image_edit_generation_options(
|
||||
payload.model.as_deref(),
|
||||
payload.aspect_ratio.as_deref(),
|
||||
@@ -4239,13 +4204,7 @@ pub(crate) async fn edit_editor_image_for_owner(
|
||||
"referenceImageSrcs",
|
||||
"修改参考图",
|
||||
)?;
|
||||
ensure_editor_image_edit_source_allowed(
|
||||
state,
|
||||
caller.owner_user_id.as_str(),
|
||||
caller.kind,
|
||||
&payload,
|
||||
)
|
||||
.await?;
|
||||
ensure_editor_image_edit_source_allowed(state, caller.owner_user_id.as_str(), &payload).await?;
|
||||
let prompt = payload.prompt.trim().to_string();
|
||||
if prompt.is_empty() {
|
||||
return Err(
|
||||
|
||||
@@ -31,9 +31,9 @@ use crate::{
|
||||
EDITOR_ASSET_FOLDER_ID_PREFIX, EDITOR_ASSET_ID_PREFIX, EDITOR_PROJECT_DEFAULT_TITLE,
|
||||
EDITOR_PROJECT_ID_PREFIX, EDITOR_RESOURCE_ID_PREFIX, EditorAssetFolderPayload,
|
||||
EditorAssetLibraryPayload, EditorAssetPayload, EditorCanvasViewportPayload,
|
||||
EditorGenerationCaller, EditorGenerationCallerKind, EditorIconSpritesheetGenerationRequest,
|
||||
EditorImageEditRequest, EditorImageGenerationRequest, EditorProjectPayload,
|
||||
EditorProjectResourcePayload, EditorUiDesignAssetExtractionRequest, current_utc_micros,
|
||||
EditorGenerationCaller, EditorIconSpritesheetGenerationRequest, EditorImageEditRequest,
|
||||
EditorImageGenerationRequest, EditorProjectPayload, EditorProjectResourcePayload,
|
||||
EditorUiDesignAssetExtractionRequest, current_utc_micros,
|
||||
editor_asset_folder_payload_from_record, editor_asset_library_payload_from_record,
|
||||
editor_asset_payload_from_record, editor_project_payload_from_record,
|
||||
editor_project_resource_payload_from_record,
|
||||
@@ -955,7 +955,6 @@ fn editor_generation_caller(
|
||||
audit_subject_user_id: Some(principal.owner_user_id().to_string()),
|
||||
audit_project_id: normalize_optional_string(project_id),
|
||||
phase_reporter: None,
|
||||
kind: EditorGenerationCallerKind::Api,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ use crate::{
|
||||
},
|
||||
editor_project::{
|
||||
EDITOR_GENERATION_MULTIPLE_WARNINGS_CODE, EditorBackgroundRemovalRequest,
|
||||
EditorGenerationCaller, EditorGenerationCallerKind, EditorGenerationPhaseReporter,
|
||||
EditorGenerationCaller, EditorGenerationPhaseReporter,
|
||||
EditorIconSpritesheetGenerationRequest, EditorImageEditRequest,
|
||||
EditorImageGenerationRequest, EditorUiDesignAssetExtractionRequest,
|
||||
edit_editor_image_for_owner, extract_editor_ui_design_assets_for_owner,
|
||||
@@ -794,9 +794,12 @@ async fn process_external_generation_job_once(
|
||||
}
|
||||
}
|
||||
EDITOR_IMAGE_EDIT_JOB_KIND => {
|
||||
let payload = match parse_editor_image_edit_worker_payload(&job) {
|
||||
let payload = match serde_json::from_str::<EditorImageEditRequest>(
|
||||
job.request_payload_json.as_str(),
|
||||
) {
|
||||
Ok(payload) => payload,
|
||||
Err(message) => {
|
||||
Err(error) => {
|
||||
let message = format!("图片画布改图任务参数解析失败:{error}");
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
return Err(message);
|
||||
}
|
||||
@@ -1138,13 +1141,6 @@ fn worker_request_context(
|
||||
.with_external_call_deadline(provider_deadline)
|
||||
}
|
||||
|
||||
fn parse_editor_image_edit_worker_payload(
|
||||
job: &ExternalGenerationJobRecord,
|
||||
) -> Result<EditorImageEditRequest, String> {
|
||||
serde_json::from_str(job.request_payload_json.as_str())
|
||||
.map_err(|error| format!("图片画布改图任务参数解析失败:{error}"))
|
||||
}
|
||||
|
||||
fn editor_generation_worker_caller(
|
||||
worker_id: &str,
|
||||
job: &ExternalGenerationJobRecord,
|
||||
@@ -1154,11 +1150,6 @@ fn editor_generation_worker_caller(
|
||||
audit_subject_user_id: Some(job.owner_user_id.clone()),
|
||||
audit_project_id: Some(job.source_entity_id.clone()),
|
||||
phase_reporter: Some(editor_generation_phase_reporter(worker_id, job)?),
|
||||
kind: if is_editor_agent_generation_job(job) {
|
||||
EditorGenerationCallerKind::EditorAgent
|
||||
} else {
|
||||
EditorGenerationCallerKind::Api
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1245,7 +1236,15 @@ fn is_external_api_generation_job(job: &ExternalGenerationJobRecord) -> bool {
|
||||
}
|
||||
|
||||
fn is_editor_agent_generation_job(job: &ExternalGenerationJobRecord) -> bool {
|
||||
job.dedupe_key.trim().starts_with("editor-agent:")
|
||||
serde_json::from_str::<Value>(job.request_payload_json.as_str())
|
||||
.ok()
|
||||
.is_some_and(|payload| {
|
||||
payload
|
||||
.pointer("/generationInputs/source")
|
||||
.and_then(Value::as_str)
|
||||
// TODO extract const
|
||||
.is_some_and(|source| source.trim() == "editor-agent")
|
||||
})
|
||||
}
|
||||
|
||||
fn compact_editor_generation_result(mut result: Value) -> Value {
|
||||
@@ -1751,50 +1750,6 @@ mod tests {
|
||||
assert!(caller.phase_reporter.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_agent_image_edit_payload_reaches_worker_gate_as_trusted_caller() {
|
||||
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": "editor_agent_edit_image",
|
||||
"generationInputs": { "source": "editor-agent" }
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let payload = parse_editor_image_edit_worker_payload(&job)
|
||||
.expect("Agent 图片编辑 payload 应由 worker 解析");
|
||||
let caller = editor_generation_worker_caller("worker-a", &job)
|
||||
.expect("Agent 图片编辑任务应恢复可信调用来源");
|
||||
|
||||
assert_eq!(caller.kind, EditorGenerationCallerKind::EditorAgent);
|
||||
assert!(
|
||||
crate::editor_project::ensure_editor_image_edit_source_kind_allowed_for_caller(
|
||||
caller.kind,
|
||||
payload.asset_kind.as_deref(),
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
job.dedupe_key = "editor:image-edit:project-1:request-1".to_string();
|
||||
let untrusted_caller = editor_generation_worker_caller("worker-a", &job)
|
||||
.expect("普通画布任务应恢复 API 调用来源");
|
||||
assert_eq!(untrusted_caller.kind, EditorGenerationCallerKind::Api);
|
||||
assert!(
|
||||
crate::editor_project::ensure_editor_image_edit_source_kind_allowed_for_caller(
|
||||
untrusted_caller.kind,
|
||||
payload.asset_kind.as_deref(),
|
||||
None,
|
||||
)
|
||||
.is_err(),
|
||||
"相同 payload 不得仅凭 assetKind 或 generationInputs 冒充 Agent"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
#[test]
|
||||
fn worker_write_guard_requires_claimed_job_lease_token() {
|
||||
@@ -2016,7 +1971,6 @@ mod tests {
|
||||
#[test]
|
||||
fn editor_agent_result_payload_keeps_compact_response() {
|
||||
let mut job = external_generation_job_record_fixture(Some("lease-1"));
|
||||
job.dedupe_key = "editor-agent:conversation-1:7:generate-image".to_string();
|
||||
job.request_payload_json = json!({
|
||||
"generationInputs": { "source": "editor-agent" },
|
||||
})
|
||||
@@ -2118,7 +2072,6 @@ mod tests {
|
||||
#[test]
|
||||
fn editor_agent_spritesheet_result_keeps_all_persisted_slices() {
|
||||
let mut job = external_generation_job_record_fixture(Some("lease-1"));
|
||||
job.dedupe_key = "editor-agent:conversation-1:7:generate-icon-spritesheet".to_string();
|
||||
job.request_payload_json = json!({
|
||||
"generationInputs": { "source": "editor-agent" },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user