diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0371e37b8..f8d6fa58f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -24,7 +24,9 @@ - 影响范围:`server-rs/crates/api-server/src/editor_project.rs` 的角色与图标像素规整降级路径;不改变成功路径、OSS PUT 次数、资源类型、画布项或前端契约,OSS GET 仍只在尺寸漂移时发生。 - 验证方式:`pixel_art_degrade_paths_guard_postprocessed_delivery_dimensions` 结构断言固定"降级分支不得返回 `(postprocessed, None, …)`"与守卫的委托实现;运行 `cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml`、`npm run check:rustfmt`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:本文件「2026-07-29 角色带背景原图与透明图统一交付尺寸」与「2026-07-28 图片生成风格使用可扩展字段并以纯内存像素规整首发」。 -- 补充(同日):守卫的回读必须分两类处理,否则会把「最多增加一次 OSS GET」放大成两次、且第二次无界。已取得 provider 原图的四条降级分支(permit 获取失败、worker 内 deadline、join 异常、worker 超时)改走纯内存守卫 `degrade_editor_pixel_art_with_provider_source`,零额外 GET;尚未取得原图的三条分支(进函数即预算耗尽、第一次回读失败、第一次回读超时)才走会回读的守卫,且该次 GET 以**外层请求 deadline**(而非已耗尽的像素预算)为绝对上界——用像素预算绑会让修复必然失败,不绑则突破预算。`apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original` 因此新增可选 `download_deadline`,非像素路径传 `None` 保持既有语义不变。计数断言固定「回读守卫 3 处、内存守卫 4 处」,防止后续新增分支时误用回读版本。 +- 补充(同日):守卫的回读必须分两类处理。已取得 provider 原图的四条降级分支(permit 获取失败、worker 内 deadline、join 异常、worker 超时)改走纯内存守卫 `degrade_editor_pixel_art_with_provider_source`,零额外 GET;尚未取得原图的三条分支(进函数即预算耗尽、第一次回读失败、第一次回读超时)才走会回读的守卫。计数断言固定「回读守卫 3 处、内存守卫 4 处」,防止后续新增分支时误用回读版本。 +- OSS 回读口径(修正此前「最多增加一次 OSS GET」的措辞):约束是**不重复读取已经成功取得的对象**,而不是"整个请求至多一次 GET"。仅在尺寸漂移且尚未持有原图时才发起最多一次修复性回读,失败后不再重试;因此第一次回读失败或被像素预算掐断时,允许存在第二次、也是最后一次尝试——第一次超时往往并非 OSS 异常,而是被 30 秒像素预算切断,此时对象通常可正常读取,放弃修复反而会让角色更频繁地退化为原图单产物。 +- 回读上界:修复性回读必须始终有绝对 deadline。优先取外层 `request_deadline`,但它只在队列 worker 路径上有值——inline HTTP 请求的 `RequestContext` 默认 `external_call_deadline = None`,此时守卫自行以 `Instant::now() + EDITOR_PIXEL_ART_MAX_PROCESSING_DURATION` 重新计时派生上界,不得退化为无界 `download.await`。`apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original` 的可选 `download_deadline` 只对像素守卫传值,非像素路径继续传 `None` 保持既有语义不变。结构断言固定守卫内必须同时出现 `request_deadline.unwrap_or_else(` 与 `EDITOR_PIXEL_ART_MAX_PROCESSING_DURATION`,防止兜底上界被移除后静默退回无界。 --- diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index e694ce906..993dbe364 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -3263,8 +3263,16 @@ async fn degrade_editor_pixel_art_to_postprocessed_with_dimension_guard( reason: String, ) -> (DownloadedOpenAiImage, Option, Option) { // 中文注释:只有尚未成功取得 provider 原图的降级分支才会走到这里。像素预算此时 - // 多半已经耗尽,不能拿它去绑这次回读(否则必然失败、守卫形同虚设),改用外层 - // 请求 deadline 作为绝对上界,保证这唯一一次 GET 有界。 + // 多半已经耗尽,不能拿它去绑这次回读(否则必然失败、守卫形同虚设)。优先用外层 + // 请求 deadline,但它只在队列 worker 路径上有值——inline HTTP 请求的 + // RequestContext 默认没有 external_call_deadline,此时必须自行重新计时派生一个 + // 上界,绝不允许出现无界 GET。 + let repair_deadline = request_deadline.unwrap_or_else(|| { + let started_at = Instant::now(); + started_at + .checked_add(EDITOR_PIXEL_ART_MAX_PROCESSING_DURATION) + .unwrap_or(started_at) + }); let (image, dimension_error) = apply_editor_postprocessed_alpha_from_persisted_provider_source_or_original( state, @@ -3272,7 +3280,7 @@ async fn degrade_editor_pixel_art_to_postprocessed_with_dimension_guard( delivery_width, delivery_height, postprocessed, - request_deadline, + Some(repair_deadline), ) .await; (image, dimension_error, Some(reason)) @@ -9423,12 +9431,19 @@ mod tests { "async fn apply_editor_postprocessed_alpha_and_pixel_art_from_persisted_provider_source_or_original", &["download_editor_persisted_image_object"], ); - // 会回读原图的守卫必须带绝对 deadline 参数,不允许无界 GET。 + // 会回读原图的守卫必须带绝对 deadline,且在外层 request_deadline 缺失时 + // (inline HTTP 请求的 RequestContext 默认就没有)必须自行派生兜底上界, + // 不允许退化成无界 GET。 assert_function_contains_in_order( source, "async fn degrade_editor_pixel_art_to_postprocessed_with_dimension_guard", "fn degrade_editor_pixel_art_with_provider_source", - &["request_deadline: Option", "request_deadline,"], + &[ + "request_deadline: Option", + "request_deadline.unwrap_or_else(", + "EDITOR_PIXEL_ART_MAX_PROCESSING_DURATION", + "Some(repair_deadline),", + ], ); // 守卫必须复用非像素路径的同一套尺寸比对与 alpha 回贴,保证尺寸一致时 // 不产生额外 OSS GET,漂移修不好时返回尺寸错误。