From 70fef2e75399a741708977b4ab34069b010da433 Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 15 Jul 2026 06:25:25 +0000 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E7=94=A8OSS=E7=AD=BE=E5=90=8DURL?= =?UTF-8?q?=E8=B0=83=E7=94=A8BgFilter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用生成原图object key并签发十分钟读取地址 移除BgFilter请求中的图片文件重传并保留原有fallback顺序 补充签名URL脱敏测试及架构运维文档 --- .../shared-memory/decision-log.md | 7 ++ ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 1 + ...发运维】本地开发验证与生产运维-2026-05-15.md | 2 + .../crates/api-server/src/editor_project.rs | 109 ++++++++++++++---- 4 files changed, 94 insertions(+), 25 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index f1c23e1bf..3595be133 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,13 @@ --- +## 2026-07-15 BgFilter 输入改用私有 OSS 短期签名 URL + +- 背景:角色形象、图标图集和 UI 素材图集在调用 BgFilter 前已经把带背景原图持久化到私有 OSS;继续由 api-server 把同一图片作为 multipart `file` 再上传一次,会重复传输图片字节并占用 API 进程网络带宽。 +- 决策:上述生成后抠图链路统一复用已持久化原图的 object key,签发 600 秒 OSS GET URL,并通过 BgFilter multipart 的 `image_url` 字段提交;请求中不再携带 `file`。签名 URL 只交给 BgFilter,不写日志或持久化。BgFilter 失败或熔断打开后才使用已保留的图片字节进入原有“阿里云通用抠图 → 本地键色”兜底链。 +- 影响范围:仅 `api-server/editor_project` 的 BgFilter 请求输入与相关文档;不改变 BgFilter endpoint、鉴权、`screen_color`、`seg_model`、输出校验、熔断规则、阿里云上传协议或降级顺序。 +- 验证方式:定向测试必须断言 BgFilter 请求函数包含 `image_url` 与 600 秒 OSS 换签,不包含 multipart `file` 或源图字节读取;随后运行 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`。 + ## 2026-07-15 阿里云通用抠图上传切换到 AuthorizeFileUpload 正式链路 - 背景:`platform-matting` 原先通过 `viapiutils/GetOssStsToken` 获取临时 AK/SK,再向固定 `viapi-customer-temp` 共享桶执行 OSS V1 PUT。阿里云官方文档将该显式生成 URL 的共享临时桶通道标记为不保证 SLA、仅便于调试且不推荐生产使用;动作视频逐帧抠图会把这条风险放大到每任务 32 至 48 次。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 77abc3a45..a15052ec9 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -241,6 +241,7 @@ npm run check:server-rs-ddd - LLM:通用 LLM 门面继续使用 `GENARRATIVE_LLM_*`;创意 Agent `gpt-5.4-mini` Chat Completions 文本链路已于 2026-06 从 APIMart 迁移到 VectorEngine,使用 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 构造 OpenAI-compatible client,`api-server` 会把未带 `/v1` 的 VectorEngine base URL 规范化到 `/v1` 后请求 `/chat/completions`。通用 `/api/llm/chat/completions` 代理使用 `GENARRATIVE_LLM_PROVIDER=openai-compatible`、`GENARRATIVE_LLM_BASE_URL=https://api.vectorengine.cn/v1`、`GENARRATIVE_LLM_MODEL=gpt-5.4-mini`;未单独配置 `GENARRATIVE_LLM_API_KEY` 时可复用 `VECTOR_ENGINE_API_KEY`。`APIMART_BASE_URL` / `APIMART_API_KEY` 只作为历史残留,不再作为创意 Agent gpt-5.4-mini 客户端来源;后续排障时优先确认 VectorEngine `/v1/models`、`/v1/chat/completions` 和 `/v1/responses` 可用性。 - 图片生成: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` 当成上游业务错误。 - 编辑器抠图服务:手动 `POST /api/editor/images/background-removals` 继续代理独立 BiRefNet 服务,配置为 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_BASE_URL`、`GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 和 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_REQUEST_TIMEOUT_MS`。角色形象生成、图标 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 推理,单次抠图较慢,必须留足超时),token 未配置时复用 BiRefNet token。BgFilter 请求必须显式传 `screen_color=` 和 `seg_model=`;前端用户路径不展示抠图模型选择并固定提交默认 `birefnet`,后端仍识别内部保留的 `anime-seg`,其中 `birefnet` 只表示 BgFilter 管线内部后端,不等同于手动去背景的独立 BiRefNet 服务。BgFilter 调用失败,或连续失败达到 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD`(默认 `3`)并在 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS`(默认 `300`)内打开熔断时,均跳过或结束 BgFilter 调用后复用同一兜底链:先调用阿里云通用抠图,阿里云失败才使用本地 `editor_green_screen` 键色扣除;熔断期不得直接退化到本地兜底。角色动作视频生成的背景色已与生图链路统一:`screenColor=auto` 时由视觉 LLM(`gpt-5-mini`,Responses 协议、low 推理档)读源角色图自动决策,并经硬过滤器剔除与前景 / 皮肤撞色的候选,手动 hex 则尊重用户选择;透明源角色图在提交 Ark 图生视频前先合成到选定背景色实色,使视频背景等于抠图键色。抽帧后逐帧优先走阿里云通用抠图,失败时降级本地 `editor_green_screen` 键色兜底(按生成时选定的背景色,而非固定 `#00FF00`)。阿里云通用抠图配置为 `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 处理已经持久化到私有 OSS 的带背景原图时,`api-server` 必须签发 600 秒 GET URL 并通过 multipart `image_url` 提交,不再下载对象或用 `file` 重传图片字节;签名 URL 不得写入日志、审计或持久化。只有 BgFilter 失败或熔断打开进入 fallback 后,才允许使用图片字节继续调用阿里云通用抠图或本地键色。 - 阿里云通用抠图的非上海地域输入不得使用 `viapiutils/GetOssStsToken`、固定 `viapi-customer-temp` 或 OSS V1 PUT。`platform-matting` 必须按官方新版 SDK Advance 协议调用 `AuthorizeFileUpload`,使用动态返回的单对象 Policy 执行 multipart POST,再把临时上海 OSS URL 交给 `SegmentCommonImage`;输入归一化、结果下载与原尺寸 Alpha 回贴继续留在同一适配器内。该协议仍上传图片字节,不等同于阿里云服务端直接抓取任意公网 URL,也不改变上层 BgFilter → 阿里云 → 本地降级顺序。 - 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。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 6eb4c8f96..315174a3e 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -67,6 +67,8 @@ lease 过期后不代表任务一定再次执行:claim transaction 只有在 ` 阿里云通用抠图的非上海地域输入使用 `AuthorizeFileUpload → Policy POST → SegmentCommonImage` 正式链路,上传 Bucket / Endpoint / ObjectKey 由阿里云动态返回;不得恢复 `GetOssStsToken`、固定 `viapi-customer-temp`、临时 AK/SK 或 OSS V1 PUT。该切换不新增环境变量;真实链路冒烟可运行 `cargo run -p platform-matting --example segment_smoke --manifest-path server-rs/Cargo.toml -- <图片路径>`,预期日志中的输入 host 为授权响应返回的上海 OSS host,并完成结果下载。图片字节仍经过执行任务的 api-server / worker,排障时不要把 Advance 路径误判为阿里云直接抓取任意公网 URL。 +BgFilter 对已经落入私有 OSS 的生成原图直接使用 600 秒签名 URL:`api-server` 的 multipart 只提交 `image_url`、`screen_color` 和 `seg_model`,不再提交 `file`,也不会在 BgFilter 调用前重新下载 OSS 对象。排障日志只应出现 object key 与签名有效期,不得记录带 `x-oss-*` 查询参数的完整 URL;BgFilter 失败或熔断打开后仍按既有顺序进入阿里云通用抠图和本地键色 fallback。 + `我的` 页签或排障面板展示队列等待时,只读取 BFF 队列接口:`GET /api/runtime/external-generation/queue-overview` 查看当前用户可见队列概览,`GET /api/runtime/external-generation/jobs/{jobId}` 查看单 job 状态。生成页 / 进度页不承接队列概览,只展示当前玩法业务进度;队列接口只提供等待 / 运行 / 失败 / 完成状态补充,最终草稿、作品和结果页仍要轮询对应玩法 session/detail 接口收敛到 ready 或 failed;不要直接查询 `external_generation_job` private table,也不要把 worker 内部 payload 暴露到前端。 外部生成任务摘要投影与历史 payload 维护使用 `npm run spacetime:external-generation:maintain -- ...`,且只能由已授权 migration operator 的 SpacetimeDB CLI 登录态执行。脚本默认 dry-run、每次只处理一批,绝不自动循环全表;`--apply` 才写入。先发布包含 `external_generation_job_summary` 与 cursor 索引的 SpacetimeDB 模块,在维护模式内对事故时间以前的编辑器终态任务执行小批 dry-run,例如 `npm run spacetime:external-generation:maintain -- --database --server-url --limit 5 --completed-before-micros `;核对 `matched_count`、`before_bytes`、`after_bytes` 和 `inline_media_count` 后,保持本批输入 cursor 不变并追加 `--apply` 重跑同一批,即使最后一批 `has_more = false`,只要 dry-run 仍有 `matched_count` / `selected_count` 也必须 apply;只有 apply 成功后才使用它返回的 `next_cursor_job_id` 继续。B-tree cursor 的选择阶段最多反序列化 `limit + 1` 行,apply 会再按主键逐条读取选中行但不会同时保留整批 payload;如怀疑存在单行异常巨型历史 JSON,先用 `--limit 1`。payload 压缩硬限制 `source_module = editor-canvas`;终态压缩完成后,用 `--backfill-summaries` 先 dry-run、再 `--apply` 分批补齐仍缺失的活动任务或无内联媒体历史任务摘要,直到 `has_more = false`,最后再切换使用 summary procedure 的 api-server。Stdb 构建 artifact 和完整 release 包都必须包含 `scripts/spacetime-maintain-external-generation-jobs.mjs` 与 `scripts/spacetime-migration-common.mjs`。首次上线不得让 Full Build 从 Stdb 自动直落 API:`STDB_API_ROLLOUT_MODE` 默认 fail-closed 为 `pause-after-stdb`,必须填写受限的 `STDB_API_ROLLOUT_APPROVERS`;Stdb Publish 通过 `KEEP_MAINTENANCE_MODE` 保持维护文件并停止旧 API/controller/worker,暂停点最多等待 4 小时,完成上述维护并确认无后续批次后才由指定审批人放行 API。定时构建缺少审批人时必须在发布前失败,不能静默退回 `normal`;也可分开运行 Stdb publish、维护、API deploy 三个受控 Job。任一批次都不得处理 pending / running payload;不要用 runtime writer、bootstrap secret 或匿名 identity 代替 migration operator,也不要在未核对 dry-run 时直接 apply。 diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index ac2b2d93f..344193301 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -126,6 +126,7 @@ const EDITOR_LEGACY_GREEN_SCREEN_SOURCE_ASSET_KIND: &str = "editor_green_screen_ const EDITOR_PROVIDER_SOURCE_SLOT: &str = "provider_source"; const EDITOR_BGFILTER_DEFAULT_SEG_MODEL: &str = "birefnet"; const EDITOR_BGFILTER_SEG_MODEL_ANIME_SEG: &str = "anime-seg"; +const EDITOR_BGFILTER_IMAGE_URL_EXPIRE_SECONDS: u64 = 600; const EDITOR_PUBLICATION_MATERIAL_ASSET_KIND: &str = "editor_publication_material"; const EDITOR_LEGACY_INLINE_IMAGE_ASSET_KIND: &str = "editor_legacy_inline_image"; @@ -1630,6 +1631,7 @@ pub(crate) async fn generate_editor_image_for_owner( "character-image", ) .await?; + let source_object_key = source_persisted.object_key.clone(); let source_record = persist_editor_provider_source_resource( state, source_persisted, @@ -1667,6 +1669,7 @@ pub(crate) async fn generate_editor_image_for_owner( }; let removal = remove_editor_generated_screen_background_with_bgfilter( state, + source_object_key.as_str(), &image, screen_color.expect("character generation should have screen color"), seg_model.expect("character generation should have BgFilter seg model"), @@ -2755,7 +2758,8 @@ struct EditorScreenBackgroundRemovalOutput { async fn remove_editor_generated_screen_background_with_bgfilter( state: &AppState, - image: &DownloadedOpenAiImage, + source_object_key: &str, + fallback_image: &DownloadedOpenAiImage, screen_color: EditorScreenBackgroundColor, seg_model: &str, audit: &crate::external_api_audit::ExternalApiAuditContext, @@ -2770,12 +2774,18 @@ async fn remove_editor_generated_screen_background_with_bgfilter( cooldown_ms = remaining.as_millis() as u64, "editor_bgfilter_circuit_open_fallback_to_aliyun_matting" ); - return fallback_editor_screen_background_removal(state, image, screen_color, audit).await; + return fallback_editor_screen_background_removal( + state, + fallback_image, + screen_color, + audit, + ) + .await; } match request_editor_generated_screen_background_with_bgfilter( state, - image, + source_object_key, screen_color, seg_model, ) @@ -2813,7 +2823,13 @@ async fn remove_editor_generated_screen_background_with_bgfilter( crate::external_api_audit::matting_failure_audit_raw_excerpt(&error), ) .await; - fallback_editor_screen_background_removal(state, image, screen_color, audit).await + fallback_editor_screen_background_removal( + state, + fallback_image, + screen_color, + audit, + ) + .await } } } @@ -2926,28 +2942,34 @@ fn reset_editor_bgfilter_circuit_for_tests() { async fn request_editor_generated_screen_background_with_bgfilter( state: &AppState, - image: &DownloadedOpenAiImage, + source_object_key: &str, screen_color: EditorScreenBackgroundColor, seg_model: &str, ) -> Result { let url = editor_bgfilter_endpoint(state)?; + let oss_client = state.oss_client().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({ + "provider": "aliyun-oss", + "reason": "OSS 未完成环境变量配置", + })) + })?; + let signed = oss_client + .sign_get_object_url(OssSignedGetObjectUrlRequest { + object_key: source_object_key.to_string(), + expire_seconds: Some(EDITOR_BGFILTER_IMAGE_URL_EXPIRE_SECONDS), + }) + .map_err(|error| map_oss_error(error, "aliyun-oss"))?; + let signed_image_url = signed.signed_url; let call_id = format!("bgfilter-call-{}", current_utc_micros()); let request_started_at = Instant::now(); let timeout_ms = state.config.editor_bgfilter_request_timeout_ms.max(1); - let input_bytes = image.bytes.len(); - let source_mime_type = image.mime_type.clone(); - let source_file_name = format!( - "editor-generated-source.{}", - image.extension.trim_start_matches('.') - ); tracing::info!( %call_id, upstream_url = %url, - file_name = %source_file_name, - mime_type = %source_mime_type, + source_object_key, + image_url_expire_seconds = EDITOR_BGFILTER_IMAGE_URL_EXPIRE_SECONDS, screen_color = screen_color.hex, seg_model, - input_bytes, timeout_ms, "editor_bgfilter_request_start" ); @@ -2960,17 +2982,8 @@ async fn request_editor_generated_screen_background_with_bgfilter( "message": format!("创建 BgFilter HTTP 客户端失败:{error}"), })) })?; - let file_part = reqwest::multipart::Part::bytes(image.bytes.clone()) - .file_name(source_file_name) - .mime_str(source_mime_type.as_str()) - .map_err(|error| { - AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ - "provider": "bgfilter", - "message": format!("图片 MIME 类型无效:{error}"), - })) - })?; let form = reqwest::multipart::Form::new() - .part("file", file_part) + .text("image_url", signed_image_url.clone()) .text("screen_color", screen_color.hex.to_string()) .text("seg_model", seg_model.to_string()); let mut request = http_client.post(url.as_str()).multipart(form); @@ -3000,6 +3013,7 @@ async fn request_editor_generated_screen_background_with_bgfilter( .text() .await .unwrap_or_else(|error| format!("读取上游错误失败:{error}")); + let message = sanitize_editor_bgfilter_upstream_message(&message, &signed_image_url); tracing::warn!( %call_id, upstream_status = status.as_u16(), @@ -3071,7 +3085,6 @@ async fn request_editor_generated_screen_background_with_bgfilter( seg_model, upstream_screen_color = upstream_screen_color.as_deref().unwrap_or(""), upstream_seg_model = upstream_seg_model.as_deref().unwrap_or(""), - input_bytes, output_bytes = image.bytes.len(), elapsed_ms = request_started_at.elapsed().as_millis() as u64, upstream_elapsed_ms, @@ -3244,6 +3257,17 @@ fn editor_bgfilter_endpoint(state: &AppState) -> Result { )) } +fn sanitize_editor_bgfilter_upstream_message(message: &str, signed_image_url: &str) -> String { + let sanitized = message.replace(signed_image_url, "[signed OSS URL redacted]"); + if sanitized.to_ascii_lowercase().contains("x-oss-") { + return "BgFilter 服务返回错误(OSS 签名 URL 已脱敏)".to_string(); + } + sanitized + .chars() + .take(500) + .collect() +} + fn parse_editor_bgfilter_seg_model(value: Option<&str>) -> Result<&'static str, AppError> { let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else { return Ok(EDITOR_BGFILTER_DEFAULT_SEG_MODEL); @@ -3698,6 +3722,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( "spritesheet", ) .await?; + let source_object_key = source_persisted.object_key.clone(); let source_record = persist_editor_provider_source_resource( state, source_persisted, @@ -3729,6 +3754,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( }; let removal = remove_editor_generated_screen_background_with_bgfilter( state, + source_object_key.as_str(), &image, screen_color, seg_model, @@ -4307,6 +4333,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( "spritesheet", ) .await?; + let source_object_key = source_persisted.object_key.clone(); let source_record = persist_editor_provider_source_resource( state, source_persisted, @@ -4338,6 +4365,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( }; let removal = remove_editor_generated_screen_background_with_bgfilter( state, + source_object_key.as_str(), &image, screen_color, seg_model, @@ -7022,6 +7050,28 @@ mod tests { ); } + #[test] + fn bgfilter_upstream_message_redacts_signed_oss_url() { + let signed_url = "https://dev-bucket.oss-cn-beijing.aliyuncs.com/generated/input.png?x-oss-signature=secret&x-oss-expires=600"; + let message = format!("invalid image_url: {signed_url}; code=fetch_failed"); + + let sanitized = sanitize_editor_bgfilter_upstream_message(&message, signed_url); + + assert!(!sanitized.contains("x-oss-signature")); + assert!(!sanitized.contains("secret")); + assert!(sanitized.contains("[signed OSS URL redacted]")); + assert!(sanitized.contains("fetch_failed")); + + let escaped_message = format!( + "invalid image_url: {}", + signed_url.replace('&', "\\u0026") + ); + let escaped_sanitized = + sanitize_editor_bgfilter_upstream_message(&escaped_message, signed_url); + assert!(!escaped_sanitized.contains("x-oss-")); + assert!(!escaped_sanitized.contains("secret")); + } + #[test] fn bgfilter_body_read_timeout_maps_to_gateway_timeout_and_transport() { let error = editor_image_removal_body_read_error( @@ -9405,12 +9455,21 @@ mod tests { "async fn request_editor_background_removal_image", &[ "editor_bgfilter_endpoint", + "sign_get_object_url", + "EDITOR_BGFILTER_IMAGE_URL_EXPIRE_SECONDS", + "\"image_url\"", "editor_bgfilter_request_timeout_ms.max(1)", "\"screen_color\"", "\"seg_model\"", "seg_model.to_string()", ], ); + assert_function_not_contains( + source, + "async fn request_editor_generated_screen_background_with_bgfilter", + "async fn request_editor_background_removal_image", + &[".part(\"file\"", "reqwest::multipart::Part::bytes"], + ); assert_function_contains( source, "async fn request_editor_background_removal_image",