修复手动去背景重复全量读取
合并手动去背景的 object key、所有权与源模型解析,复用同一轮项目和素材快照。 保留 asset_object 定向授权兜底,并新增快照读取次数回归测试。 同步图片画布技术方案与项目避坑记录。
This commit is contained in:
@@ -260,7 +260,7 @@
|
||||
|
||||
- 现象:画板生成、快速编辑、图标素材或 UI 素材提取如果允许直接提交 generated objectKey,用户只要知道其他账号的私有 objectKey,就可能让 api-server 签名读取并送给外部生成供应商。
|
||||
- 原因:Data URL/Blob URL 只允许停留在浏览器临时态,正式编辑器引用必须先上传并经统一 resolver 校验归属。
|
||||
- 处理:所有私有对象引用在读取字节或签发 URL 前统一走 `resolve_editor_reference_object_key_for_owner(state, owner_user_id, source)`,先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key。只有确实需要图片字节的入口(生成 / 重绘 / 图标 / UI 提取等交给 provider 的路径)再走 `parse_editor_reference_image`(内部仍先 resolve,再下载 OSS 字节);手动去背景等只签发短期 URL 的入口不要 `parse` 整图。图标素材等额外参考图必须真实传到 provider,不只写 metadata;图片快速编辑当前不开放额外参考图,若后续重开入口也必须沿用同一归属校验。
|
||||
- 处理:所有私有对象引用在读取字节或签发 URL 前统一走 `resolve_editor_reference_object_key_for_owner(state, owner_user_id, source)`,先在当前账号的项目资源、素材库资产或 `asset_object` 中匹配 owner / bucket / key。只有确实需要图片字节的入口(生成 / 重绘 / 图标 / UI 提取等交给 provider 的路径)再走 `parse_editor_reference_image`(内部仍先 resolve,再下载 OSS 字节);手动去背景等只签发短期 URL 的入口不要 `parse` 整图。手动去背景还要恢复源模型时,object key 解析、所有权校验和源模型回溯必须复用同一轮账号项目 / 素材快照,项目与素材快照各最多读取一次;不得先走通用 resolver 全量读取,再为模型回溯重复拉取完整画布和素材库。图标素材等额外参考图必须真实传到 provider,不只写 metadata;图片快速编辑当前不开放额外参考图,若后续重开入口也必须沿用同一归属校验。
|
||||
- 验证:`cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_reference`,并用前端 workflow 测试覆盖 `referenceImageSrcs` 进入图标生成请求;若快速编辑重开额外参考图,再补对应请求覆盖。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/spacetime-client/src/assets.rs`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`。
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3950,22 +3950,15 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
sanitize_editor_client_generation_inputs(payload.generation_inputs.take());
|
||||
let started_at = Instant::now();
|
||||
caller.report_processing_phase(state).await?;
|
||||
let source_object_key = resolve_editor_reference_object_key_for_owner(
|
||||
state,
|
||||
caller.owner_user_id.as_str(),
|
||||
payload.source_image_src.as_str(),
|
||||
)
|
||||
.await?;
|
||||
let source_model = resolve_editor_background_removal_source_model(
|
||||
let source = resolve_editor_background_removal_source(
|
||||
state,
|
||||
caller.owner_user_id.as_str(),
|
||||
payload.project_id.as_deref(),
|
||||
payload.source_resource_id.as_deref(),
|
||||
payload.source_image_src.as_str(),
|
||||
source_object_key.as_str(),
|
||||
)
|
||||
.await?;
|
||||
validate_editor_background_removal_source(state, source_object_key.as_str()).await?;
|
||||
validate_editor_background_removal_source(state, source.object_key.as_str()).await?;
|
||||
let matting_audit = crate::external_api_audit::ExternalApiAuditContext {
|
||||
user_id: caller.audit_subject_user_id.clone(),
|
||||
profile_id: caller
|
||||
@@ -3977,7 +3970,7 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
};
|
||||
let removed = request_editor_background_removal_image_with_bgfilter_worker(
|
||||
state,
|
||||
source_object_key.as_str(),
|
||||
source.object_key.as_str(),
|
||||
&matting_audit,
|
||||
)
|
||||
.await?;
|
||||
@@ -4019,7 +4012,7 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
height: removed.height,
|
||||
prompt: "remove background".to_string(),
|
||||
actual_prompt: None,
|
||||
model: source_model.unwrap_or_default(),
|
||||
model: source.model.unwrap_or_default(),
|
||||
provider: "BgFilter".to_string(),
|
||||
task_id: task_id.clone(),
|
||||
group_task_id: None,
|
||||
@@ -8655,14 +8648,28 @@ pub(crate) async fn resolve_editor_reference_object_key_for_owner(
|
||||
Ok(object_key)
|
||||
}
|
||||
|
||||
async fn resolve_editor_background_removal_source_model(
|
||||
struct ResolvedEditorBackgroundRemovalSource {
|
||||
object_key: String,
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
async fn resolve_editor_background_removal_source(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
project_id: Option<&str>,
|
||||
source_resource_id: Option<&str>,
|
||||
source_reference: &str,
|
||||
source_object_key: &str,
|
||||
) -> Result<Option<String>, AppError> {
|
||||
) -> Result<ResolvedEditorBackgroundRemovalSource, AppError> {
|
||||
ensure_editor_reference_image_source_is_stable(
|
||||
source_reference,
|
||||
"editor-reference-image",
|
||||
"sourceImageSrc",
|
||||
"图片参考图",
|
||||
)?;
|
||||
let owner_user_id = owner_user_id.trim();
|
||||
if owner_user_id.is_empty() {
|
||||
return Err(editor_reference_image_forbidden(source_reference.trim()));
|
||||
}
|
||||
let projects = state
|
||||
.spacetime_client()
|
||||
.list_editor_projects(owner_user_id.to_string())
|
||||
@@ -8673,14 +8680,39 @@ async fn resolve_editor_background_removal_source_model(
|
||||
.get_editor_asset_library(owner_user_id.to_string(), current_utc_micros())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
resolve_editor_background_removal_source_model_from_records(
|
||||
let source_object_key = match normalize_editor_reference_object_key(source_reference) {
|
||||
Ok(object_key) => object_key,
|
||||
Err(error) => find_editor_reference_object_key_by_registered_id_from_records(
|
||||
projects.as_slice(),
|
||||
library.assets.as_slice(),
|
||||
source_reference.trim(),
|
||||
)
|
||||
.ok_or(error)?,
|
||||
};
|
||||
if !editor_reference_object_key_is_registered_for_owner(
|
||||
projects.as_slice(),
|
||||
library.assets.as_slice(),
|
||||
source_object_key.as_str(),
|
||||
) {
|
||||
ensure_editor_reference_asset_object_owned(
|
||||
state,
|
||||
owner_user_id,
|
||||
source_object_key.as_str(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let model = resolve_editor_background_removal_source_model_from_records(
|
||||
projects.as_slice(),
|
||||
library.assets.as_slice(),
|
||||
project_id,
|
||||
source_resource_id,
|
||||
source_reference,
|
||||
source_object_key,
|
||||
)
|
||||
source_object_key.as_str(),
|
||||
)?;
|
||||
Ok(ResolvedEditorBackgroundRemovalSource {
|
||||
object_key: source_object_key,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_editor_background_removal_source_model_from_records(
|
||||
@@ -8891,10 +8923,8 @@ async fn find_editor_reference_object_key_by_registered_id(
|
||||
.list_editor_projects(owner_user_id.to_string())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
if let Some(object_key) = projects
|
||||
.iter()
|
||||
.flat_map(|project| project.resources.iter())
|
||||
.find_map(|resource| editor_reference_resource_object_key(resource, source))
|
||||
if let Some(object_key) =
|
||||
find_editor_reference_resource_object_key_by_registered_id(projects.as_slice(), source)
|
||||
{
|
||||
return Ok(Some(object_key));
|
||||
}
|
||||
@@ -8903,10 +8933,38 @@ async fn find_editor_reference_object_key_by_registered_id(
|
||||
.get_editor_asset_library(owner_user_id.to_string(), current_utc_micros())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
Ok(library
|
||||
.assets
|
||||
Ok(find_editor_reference_asset_object_key_by_registered_id(
|
||||
library.assets.as_slice(),
|
||||
source,
|
||||
))
|
||||
}
|
||||
|
||||
fn find_editor_reference_object_key_by_registered_id_from_records(
|
||||
projects: &[EditorProjectRecord],
|
||||
assets: &[EditorAssetRecord],
|
||||
source: &str,
|
||||
) -> Option<String> {
|
||||
find_editor_reference_resource_object_key_by_registered_id(projects, source)
|
||||
.or_else(|| find_editor_reference_asset_object_key_by_registered_id(assets, source))
|
||||
}
|
||||
|
||||
fn find_editor_reference_resource_object_key_by_registered_id(
|
||||
projects: &[EditorProjectRecord],
|
||||
source: &str,
|
||||
) -> Option<String> {
|
||||
projects
|
||||
.iter()
|
||||
.find_map(|asset| editor_reference_asset_object_key(asset, source)))
|
||||
.flat_map(|project| project.resources.iter())
|
||||
.find_map(|resource| editor_reference_resource_object_key(resource, source))
|
||||
}
|
||||
|
||||
fn find_editor_reference_asset_object_key_by_registered_id(
|
||||
assets: &[EditorAssetRecord],
|
||||
source: &str,
|
||||
) -> Option<String> {
|
||||
assets
|
||||
.iter()
|
||||
.find_map(|asset| editor_reference_asset_object_key(asset, source))
|
||||
}
|
||||
|
||||
fn editor_reference_resource_object_key(
|
||||
@@ -8957,15 +9015,7 @@ async fn ensure_editor_reference_object_key_owned(
|
||||
.list_editor_projects(owner_user_id.to_string())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
if projects.iter().any(|project| {
|
||||
project.resources.iter().any(|resource| {
|
||||
editor_record_object_key_matches(
|
||||
resource.object_key.as_deref(),
|
||||
resource.image_src.as_str(),
|
||||
object_key,
|
||||
)
|
||||
})
|
||||
}) {
|
||||
if editor_reference_object_key_is_registered_in_projects(projects.as_slice(), object_key) {
|
||||
return Ok(());
|
||||
}
|
||||
let library = state
|
||||
@@ -8973,15 +9023,54 @@ async fn ensure_editor_reference_object_key_owned(
|
||||
.get_editor_asset_library(owner_user_id.to_string(), current_utc_micros())
|
||||
.await
|
||||
.map_err(map_editor_project_error)?;
|
||||
if library.assets.iter().any(|asset| {
|
||||
if editor_reference_object_key_is_registered_in_assets(library.assets.as_slice(), object_key) {
|
||||
return Ok(());
|
||||
}
|
||||
ensure_editor_reference_asset_object_owned(state, owner_user_id, object_key).await
|
||||
}
|
||||
|
||||
fn editor_reference_object_key_is_registered_for_owner(
|
||||
projects: &[EditorProjectRecord],
|
||||
assets: &[EditorAssetRecord],
|
||||
object_key: &str,
|
||||
) -> bool {
|
||||
editor_reference_object_key_is_registered_in_projects(projects, object_key)
|
||||
|| editor_reference_object_key_is_registered_in_assets(assets, object_key)
|
||||
}
|
||||
|
||||
fn editor_reference_object_key_is_registered_in_projects(
|
||||
projects: &[EditorProjectRecord],
|
||||
object_key: &str,
|
||||
) -> bool {
|
||||
projects.iter().any(|project| {
|
||||
project.resources.iter().any(|resource| {
|
||||
editor_record_object_key_matches(
|
||||
resource.object_key.as_deref(),
|
||||
resource.image_src.as_str(),
|
||||
object_key,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn editor_reference_object_key_is_registered_in_assets(
|
||||
assets: &[EditorAssetRecord],
|
||||
object_key: &str,
|
||||
) -> bool {
|
||||
assets.iter().any(|asset| {
|
||||
editor_record_object_key_matches(
|
||||
asset.object_key.as_deref(),
|
||||
asset.image_src.as_str(),
|
||||
object_key,
|
||||
)
|
||||
}) {
|
||||
return Ok(());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_editor_reference_asset_object_owned(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
object_key: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let Some(oss_client) = state.oss_client() else {
|
||||
return Err(editor_reference_image_forbidden(object_key));
|
||||
};
|
||||
@@ -13680,6 +13769,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_manual_background_removal_reuses_single_account_snapshot_pair() {
|
||||
let source = include_str!("editor_project.rs");
|
||||
assert_function_not_contains(
|
||||
source,
|
||||
"pub(crate) async fn remove_editor_image_background_for_owner",
|
||||
"struct EditorBackgroundRemovalImage",
|
||||
&["resolve_editor_reference_object_key_for_owner"],
|
||||
);
|
||||
assert_function_occurrence_count(
|
||||
source,
|
||||
"async fn resolve_editor_background_removal_source(",
|
||||
"fn resolve_editor_background_removal_source_model_from_records(",
|
||||
".list_editor_projects(",
|
||||
1,
|
||||
);
|
||||
assert_function_occurrence_count(
|
||||
source,
|
||||
"async fn resolve_editor_background_removal_source(",
|
||||
"fn resolve_editor_background_removal_source_model_from_records(",
|
||||
".get_editor_asset_library(",
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_canvas_screen_background_generation_uses_bgfilter_postprocess() {
|
||||
let source = include_str!("editor_project.rs");
|
||||
@@ -13760,7 +13874,7 @@ mod tests {
|
||||
"struct EditorBackgroundRemovalImage",
|
||||
&[
|
||||
"caller.report_processing_phase(state).await?",
|
||||
"resolve_editor_reference_object_key_for_owner",
|
||||
"resolve_editor_background_removal_source",
|
||||
"validate_editor_background_removal_source",
|
||||
"request_editor_background_removal_image_with_bgfilter_worker",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user