修复手动去背景重复全量读取

合并手动去背景的 object key、所有权与源模型解析,复用同一轮项目和素材快照。
保留 asset_object 定向授权兜底,并新增快照读取次数回归测试。
同步图片画布技术方案与项目避坑记录。
This commit is contained in:
2026-07-31 07:42:14 +00:00
parent 353a7bb164
commit 2e0f1f41ad
3 changed files with 154 additions and 40 deletions
@@ -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
+152 -38
View File
@@ -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",
],