取消手动去背景Data URL兼容
手动去背景在HTTP入队前拒绝Data URL。 worker执行前重复校验,防止历史任务和内部调用绕过。 保留objectKey、resourceId和assetId输入并补充定向测试与架构文档。
This commit is contained in:
@@ -242,7 +242,7 @@ npm run check:server-rs-ddd
|
||||
- 图片生成:VectorEngine `gpt-image-2` 图片 provider 归属 `platform-image`,密钥只在后端环境变量中;`api-server` 内的 `openai_image_generation.rs` 只是兼容调用面和外部失败审计桥接,不再承载 provider 协议实现。实际外部生成运行记录统一落 `tracking_event`,`event_key = external_generation_run`,metadata 记录开始 / 结束时间、耗时、状态、成功标记、失败原因、provider task id 和结果摘要,不再写回过时的 `ai_task`。DashScope 只按仍在使用的历史能力单独处理,不作为 GPT-image-2 兜底。VectorEngine `/v1/images/generations` 和 `/v1/images/edits` 上游 POST 使用 `libcurl` 发送;`reqwest` 只保留给参考图 URL 下载和响应中图片 URL 下载。`/v1/images/edits` 的 multipart 参考图必须作为 libcurl 文件上传 part 发送,字段名为 `image`,实现上使用 `Form::buffer(file_name, bytes)` 并设置 `Content-Type`;不能只用 `contents(...).filename(...)`,否则上游会把请求转码为缺少图片并返回 `image is required`。`request_send` 阶段的 curl timeout / connect error 按可重试传输错误处理,最多尝试 5 次,并使用指数退避加短抖动;排障时优先看 `attempt`、`max_attempts`、`retry_delay_ms`、`reference_image_bytes_total` 和 `request_params`,不要把 `SendRequest` 当成上游业务错误。
|
||||
- 生成后抠图以内存中的 `DownloadedImage` → 私有 OSS owned 上传作为带背景原图生命周期边界:上传消费字节所有权,完成后不保留原图缓冲。BgFilter 必须签发 600 秒 GET URL 并通过 multipart `image_url` 提交,不下载对象或用 `file` 重传;进入阿里云 fallback 时由 `platform-matting` URL 接口单独下载并上传 `AuthorizeFileUpload` 临时对象,在推理前释放下载缓冲;继续 fallback 到本地键色时再单独下载一次原图,本地产出后释放本次原图下载缓冲。签名 URL 不得写入日志、审计或持久化。
|
||||
- 阿里云通用抠图的非上海地域输入不得使用 `viapiutils/GetOssStsToken`、固定 `viapi-customer-temp` 或 OSS V1 PUT。`platform-matting` 必须按官方新版 SDK Advance 协议调用 `AuthorizeFileUpload`,使用动态返回的单对象 Policy 执行 multipart POST,再把临时上海 OSS URL 交给 `SegmentCommonImage`;输入归一化、结果下载与原尺寸 Alpha 回贴继续留在同一适配器内。该协议仍上传图片字节,不等同于阿里云服务端直接抓取任意公网 URL,也不改变上层 BgFilter → 阿里云 → 本地降级顺序。
|
||||
- 编辑器抠图服务:手动 `POST /api/editor/images/background-removals` 与角色形象生成、图标 spritesheet 生成、UI 设计图素材提取、角色动作抽帧后的透明化统一走 BgFilter,配置为 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN` 和 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,默认请求超时为 `180000ms`(BgFilter 当前为 CPU 推理,单次抠图较慢,必须留足超时);旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 BgFilter token 的兼容回退别名,原手动去背景专用 base URL / timeout 配置已经删除。手动去背景固定传 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传 `screen_color`;标准纯色背景四条链路固定传 `background_mode=flat`,并显式传 `screen_color=<screenColor>`、`seg_model=<segModel>` 和 `cross_check=<on|off>`,其中角色形象生成和角色动作逐帧去背传 `cross_check=on`,图标 spritesheet 生成和 UI 设计图素材提取传 `cross_check=off`。前端用户路径不展示抠图模型、模式或 cross-check,固定提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`;这些参数只属于后端内部供应商策略,不进入前端或外部 OpenAPI。标准纯色背景 BgFilter 调用失败,或连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD`(默认 `3`)并在 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS`(默认 `300`)内打开熔断时,继续复用“阿里云通用抠图 → 本地 `editor_green_screen` 键色扣除”兜底链,熔断期不得直接退化到本地兜底。角色动作视频生成的背景色已与生图链路统一:`screenColor=auto` 时由视觉 LLM(`gpt-5-mini`,Responses 协议、low 推理档)读源角色图自动决策,并经硬过滤器剔除与前景 / 皮肤撞色的候选,手动 hex 则尊重用户选择;透明源角色图在提交 Ark 图生视频前先合成到选定背景色实色,使视频背景等于抠图键色;抽帧后逐帧固定使用 `seg_model=birefnet`、`cross_check=on` 进入上述三段式链路。阿里云通用抠图配置为 `GENARRATIVE_ALIYUN_MATTING_ENABLED`、`GENARRATIVE_ALIYUN_MATTING_ENDPOINT`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET` 和 `GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS`;未配置专用 AK/SK 时可复用 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`,默认 endpoint 为 `imageseg.cn-shanghai.aliyuncs.com`。标准纯色背景链路的 BgFilter 与阿里云抠图失败都写入 `external_api_call_failure` 审计。
|
||||
- 编辑器抠图服务:手动 `POST /api/editor/images/background-removals` 与角色形象生成、图标 spritesheet 生成、UI 设计图素材提取、角色动作抽帧后的透明化统一走 BgFilter,配置为 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL`、`GENARRATIVE_EDITOR_BGFILTER_TOKEN` 和 `GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS`,默认 base URL 为 `http://58.87.105.82/bgfilter`,默认请求超时为 `180000ms`(BgFilter 当前为 CPU 推理,单次抠图较慢,必须留足超时);旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 BgFilter token 的兼容回退别名,原手动去背景专用 base URL / timeout 配置已经删除。手动去背景固定传 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传 `screen_color`;该 API 只接受已登记的 `objectKey`、`resourceId` 或 `assetId`,BFF 入队前和 worker 执行前都拒绝 Data URL。标准纯色背景四条链路固定传 `background_mode=flat`,并显式传 `screen_color=<screenColor>`、`seg_model=<segModel>` 和 `cross_check=<on|off>`,其中角色形象生成和角色动作逐帧去背传 `cross_check=on`,图标 spritesheet 生成和 UI 设计图素材提取传 `cross_check=off`。前端用户路径不展示抠图模型、模式或 cross-check,固定提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`;这些参数只属于后端内部供应商策略,不进入前端或外部 OpenAPI。标准纯色背景 BgFilter 调用失败,或连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD`(默认 `3`)并在 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS`(默认 `300`)内打开熔断时,继续复用“阿里云通用抠图 → 本地 `editor_green_screen` 键色扣除”兜底链,熔断期不得直接退化到本地兜底。角色动作视频生成的背景色已与生图链路统一:`screenColor=auto` 时由视觉 LLM(`gpt-5-mini`,Responses 协议、low 推理档)读源角色图自动决策,并经硬过滤器剔除与前景 / 皮肤撞色的候选,手动 hex 则尊重用户选择;透明源角色图在提交 Ark 图生视频前先合成到选定背景色实色,使视频背景等于抠图键色;抽帧后逐帧固定使用 `seg_model=birefnet`、`cross_check=on` 进入上述三段式链路。阿里云通用抠图配置为 `GENARRATIVE_ALIYUN_MATTING_ENABLED`、`GENARRATIVE_ALIYUN_MATTING_ENDPOINT`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_ID`、`GENARRATIVE_ALIYUN_MATTING_ACCESS_KEY_SECRET` 和 `GENARRATIVE_ALIYUN_MATTING_REQUEST_TIMEOUT_MS`;未配置专用 AK/SK 时可复用 `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`,默认 endpoint 为 `imageseg.cn-shanghai.aliyuncs.com`。标准纯色背景链路的 BgFilter 与阿里云抠图失败都写入 `external_api_call_failure` 审计。
|
||||
- BgFilter 连接复用、重试与动作帧流水线:api-server 必须在 `AppState` 复用同一个 BgFilter HTTP Client 及 keep-alive 连接池。`GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS` 是所有路径的基准请求超时;角色动作逐帧 BgFilter 的每一次 HTTP attempt 使用“基准超时 + `2000ms × 本次实际帧数`”,默认 `32 / 40 / 48` 帧分别为 `244000 / 260000 / 276000ms`,角色形象单图、图标、UI 和手动去背景仍使用基准值。api-server 只在共享 Client 的单次 RequestBuilder 上覆盖该值;它覆盖从请求发起到响应体读取完成,是单次 attempt 的总 deadline,不是整批帧或 worker job 超时,重试会重新获得同样的 deadline,整项任务仍受 worker long-job 预算约束。flat 与 complex 请求首次失败后都立即重试 `1` 次;标准纯色背景 flat 请求第二次仍失败才进入“阿里云通用抠图 → 本地键色”降级链,手动 complex 请求第二次仍失败则返回最终错误,不接入依赖纯色键值的降级链,也不改变 flat 路径的熔断状态。角色动作全部 `32 / 40 / 48` 帧按“单帧绿幕源图先落 OSS → BgFilter/降级 → 透明帧落 OSS”独立流水化,使用覆盖本次全部帧的无序在途集合连续发射,不在 api-server 增加供应商进程锁或固定小并发窗口;返回结果携带原始帧序并在收口时排序。任一帧最终失败时必须先排空全部已启动 Future,再让整个动作任务失败退款,不能发布缺帧动画。
|
||||
- Match3D 物品 sheet:关卡整图完成后走 VectorEngine `/v1/images/edits` multipart `image`,模型为 `gpt-image-2`,`2K 1:1` 输出 `10*10` spritesheet;物品 sheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG,并把透明整图写入 `itemSpritesheetImageSrc/itemSpritesheetImageObjectKey`。后端优先按透明 alpha 连通域从该 sheet 识别真实素材矩形并持久化 20 个物品、每个 5 个形态;识别数量不足时才回退 `10*10` 固定网格。通用系列素材图集的行列索引按每行 2 个物品计算,必须落在 `1..=10`,难度只决定运行态加载 3 / 9 / 15 / 20 种。
|
||||
- Match3D UI spritesheet 和背景派生图:关卡整图作为参考图并发生成 `1K 1:1` UI spritesheet 与 `1K 9:16` 背景图,模型均为 `gpt-image-2`。UI spritesheet prompt 固定要求单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;背景图必须合成为全画幅不透明 PNG。
|
||||
|
||||
@@ -2908,6 +2908,7 @@ pub async fn remove_editor_image_background(
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Json(payload): Json<EditorBackgroundRemovalRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
ensure_editor_background_removal_source_is_not_data_url(payload.source_image_src.as_str())?;
|
||||
let caller = EditorGenerationCaller::from_authenticated(&authenticated);
|
||||
let source_entity_id = editor_generation_source_entity_id(
|
||||
payload.project_id.as_deref(),
|
||||
@@ -2932,6 +2933,20 @@ pub async fn remove_editor_image_background(
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_editor_background_removal_source_is_not_data_url(source: &str) -> Result<(), AppError> {
|
||||
let prefix = source.trim_start().as_bytes().get(..5);
|
||||
if prefix.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"data:")) {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-background-removal",
|
||||
"field": "sourceImageSrc",
|
||||
"message": "手动去背景图片必须先上传 OSS,并提交已登记的 objectKey、resourceId 或 assetId,不能提交 Data URL。",
|
||||
})),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
@@ -2939,6 +2954,7 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
payload: EditorBackgroundRemovalRequest,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let started_at = Instant::now();
|
||||
ensure_editor_background_removal_source_is_not_data_url(payload.source_image_src.as_str())?;
|
||||
caller.report_processing_phase(state).await?;
|
||||
let source_image = parse_editor_reference_image(
|
||||
state,
|
||||
@@ -9914,6 +9930,48 @@ mod tests {
|
||||
assert!(normalize_icon_descriptions(too_many).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_background_removal_rejects_data_url_before_queue_and_worker_execution() {
|
||||
for source in [
|
||||
"data:image/png;base64,AAAA",
|
||||
" \nDaTa:image/webp;base64,BBBB",
|
||||
] {
|
||||
let error = ensure_editor_background_removal_source_is_not_data_url(source)
|
||||
.expect_err("手动去背景不应继续兼容 Data URL");
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert!(error.body_text().contains("不能提交 Data URL"));
|
||||
}
|
||||
for source in [
|
||||
"generated-character-drafts/editor/source.png",
|
||||
"resource-1",
|
||||
"asset-1",
|
||||
] {
|
||||
ensure_editor_background_removal_source_is_not_data_url(source)
|
||||
.expect("已登记的 OSS 对象引用应继续进入后续归属校验");
|
||||
}
|
||||
|
||||
let source = include_str!("editor_project.rs");
|
||||
assert_function_contains_in_order(
|
||||
source,
|
||||
"pub async fn remove_editor_image_background(",
|
||||
"pub(crate) async fn remove_editor_image_background_for_owner",
|
||||
&[
|
||||
"ensure_editor_background_removal_source_is_not_data_url",
|
||||
"enqueue_editor_generation_job",
|
||||
],
|
||||
);
|
||||
assert_function_contains_in_order(
|
||||
source,
|
||||
"pub(crate) async fn remove_editor_image_background_for_owner",
|
||||
"struct EditorBackgroundRemovalImage",
|
||||
&[
|
||||
"ensure_editor_background_removal_source_is_not_data_url",
|
||||
"caller.report_processing_phase(state).await?",
|
||||
"parse_editor_reference_image",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_reference_image_data_url_stays_compatible() {
|
||||
let parsed = parse_editor_reference_image_data_url("data:image/png;base64,AAAA")
|
||||
|
||||
Reference in New Issue
Block a user