修复 Rust 代码格式

格式化 api-server 抠图错误映射代码

格式化编辑器绿幕与背景决策代码

格式化编辑器背景筛选代码

格式化抠图冒烟示例

格式化抠图平台库代码

格式化充值过期订阅客户端代码
This commit is contained in:
2026-07-15 08:26:22 +00:00
parent 49879f39a7
commit 3aa61d61c8
7 changed files with 165 additions and 106 deletions
@@ -118,22 +118,36 @@ mod tests {
error.message()
);
let details = mapped.details().expect("details present");
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false));
assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false));
assert_eq!(
details.get("transport").and_then(|v| v.as_bool()),
Some(false)
);
assert_eq!(
details.get("timeout").and_then(|v| v.as_bool()),
Some(false)
);
}
}
#[test]
fn upstream_transport_failure_maps_to_retryable_transport() {
let error =
MattingError::upstream_transport_error("通用抠图请求失败:dns error".to_string(), false);
let error = MattingError::upstream_transport_error(
"通用抠图请求失败:dns error".to_string(),
false,
);
let mapped = aliyun_matting_failure_to_app_error(&error, 12);
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
let details = mapped.details().expect("details present");
// 无 HTTP 状态的传输层失败标记为可重试 transport 故障,且不带 upstreamStatus。
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true));
assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(false));
assert_eq!(
details.get("transport").and_then(|v| v.as_bool()),
Some(true)
);
assert_eq!(
details.get("timeout").and_then(|v| v.as_bool()),
Some(false)
);
assert!(details.get("upstreamStatus").is_some_and(|v| v.is_null()));
}
@@ -146,7 +160,10 @@ mod tests {
assert_eq!(mapped.status_code(), StatusCode::GATEWAY_TIMEOUT);
let details = mapped.details().expect("details present");
assert_eq!(details.get("timeout").and_then(|v| v.as_bool()), Some(true));
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(true));
assert_eq!(
details.get("transport").and_then(|v| v.as_bool()),
Some(true)
);
}
#[test]
@@ -159,7 +176,13 @@ mod tests {
assert!(crate::external_api_audit::matting_failure_external_call_attempted(&mapped));
let details = mapped.details().expect("details present");
assert_eq!(details.get("transport").and_then(|v| v.as_bool()), Some(false));
assert_eq!(details.get("upstreamStatus").and_then(|v| v.as_u64()), Some(429));
assert_eq!(
details.get("transport").and_then(|v| v.as_bool()),
Some(false)
);
assert_eq!(
details.get("upstreamStatus").and_then(|v| v.as_u64()),
Some(429)
);
}
}
@@ -109,7 +109,6 @@ pub(crate) fn default_editor_screen_background_color() -> EditorScreenBackground
EDITOR_SCREEN_BACKGROUND_COLORS[0]
}
pub(crate) fn parse_editor_screen_background_color(
value: Option<&str>,
) -> Result<EditorScreenBackgroundColor, AppError> {
@@ -177,16 +177,15 @@ pub(crate) async fn resolve_editor_screen_background_color(
let mut last_error: Option<String> = None;
for attempt in 1..=EDITOR_SCREEN_BACKGROUND_DECISION_MAX_ATTEMPTS {
let user_message = match source_image_data_url {
Some(image_url) => {
LlmMessage::user(user_prompt.as_str()).with_image_url(image_url)
}
Some(image_url) => LlmMessage::user(user_prompt.as_str()).with_image_url(image_url),
None => LlmMessage::user(user_prompt.as_str()),
};
// 预算要够推理模型(如 gpt-5-mini)先花几百 token 推理、再吐 JSON 答案;
// 实测 low 档推理约 320~384 token,取 1024 留足余量。降级客户端遇 stop 提前结束,不会多花。
let mut request = LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message])
.with_max_tokens(1024)
.with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS);
let mut request =
LlmTextRequest::new(vec![LlmMessage::system(system_prompt), user_message])
.with_max_tokens(1024)
.with_request_timeout_ms(EDITOR_SCREEN_BACKGROUND_DECISION_TIMEOUT_MS);
if let Some(decision_model) = decision_model {
// gpt-5-mini 是推理模型(有图视觉档 / 无图文本档均适用):走 Responses 协议并压到 low
// 推理档,否则默认档会把预算全烧在推理上、返回空答案。
@@ -296,8 +295,7 @@ async fn record_editor_screen_background_decision_llm_error(
prompt_chars: usize,
reference_image_count: usize,
) {
let (failure_stage, status_code, timeout, retryable, error_source, raw_excerpt) = match error
{
let (failure_stage, status_code, timeout, retryable, error_source, raw_excerpt) = match error {
LlmError::InvalidConfig(_) | LlmError::InvalidRequest(_) => return,
LlmError::Timeout { .. } => ("request_timeout", None, true, true, None, None),
LlmError::Connectivity { message, .. } => (
@@ -329,14 +327,9 @@ async fn record_editor_screen_background_decision_llm_error(
),
LlmError::EmptyResponse => ("missing_response", Some(200), false, false, None, None),
LlmError::StreamUnavailable => ("response_body", Some(200), false, true, None, None),
LlmError::Transport(message) => (
"transport",
None,
false,
true,
Some(message.as_str()),
None,
),
LlmError::Transport(message) => {
("transport", None, false, true, Some(message.as_str()), None)
}
};
record_editor_screen_background_decision_failure(
audit,
@@ -754,8 +747,7 @@ mod tests {
request_id: Some("request-1".to_string()),
},
);
let tracking =
crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
assert_eq!(audit.provider, "vector-engine");
assert_eq!(audit.endpoint, "https://vector.example/v1/responses");
@@ -809,8 +801,7 @@ mod tests {
1,
&ExternalApiAuditContext::default(),
);
let tracking =
crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(&audit);
assert_eq!(audit.failure_stage, "request_timeout");
assert_eq!(audit.status_code, None);
@@ -897,7 +888,8 @@ mod tests {
let (k, v) = trimmed.split_once('=').unwrap();
let v = v.trim().trim_matches('"').trim_matches('\'');
// 先出现的文件优先(.env.local > .env.secrets.local > .env),与服务端 dotenv 顺序一致。
map.entry(k.trim().to_string()).or_insert_with(|| v.to_string());
map.entry(k.trim().to_string())
.or_insert_with(|| v.to_string());
}
}
std::env::var(key).ok().or_else(|| map.get(key).cloned())
@@ -934,7 +926,10 @@ mod tests {
let image = RgbaImage::from_pixel(64, 64, Rgba([120, 180, 120, 255]));
let mut bytes = Vec::new();
image::DynamicImage::ImageRgba8(image)
.write_to(&mut std::io::Cursor::new(&mut bytes), image::ImageFormat::Png)
.write_to(
&mut std::io::Cursor::new(&mut bytes),
image::ImageFormat::Png,
)
.expect("test image should encode");
format!(
"data:image/png;base64,{}",
@@ -966,7 +961,11 @@ mod tests {
eprintln!(
"[live 无图] mode={:?} hex={} label={} attempts={} fallback={}",
decision.mode, decision.color.hex, decision.color.label, decision.attempts, decision.fallback
decision.mode,
decision.color.hex,
decision.color.label,
decision.attempts,
decision.fallback
);
assert_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto);
assert!(
@@ -1000,7 +999,11 @@ mod tests {
eprintln!(
"[live 有图] mode={:?} hex={} label={} attempts={} fallback={}",
decision.mode, decision.color.hex, decision.color.label, decision.attempts, decision.fallback
decision.mode,
decision.color.hex,
decision.color.label,
decision.attempts,
decision.fallback
);
assert_eq!(decision.mode, EditorScreenBackgroundDecisionMode::Auto);
assert!(
@@ -203,11 +203,12 @@ impl ForegroundHistogram {
.filter(|bin| bin.mass >= mass_floor)
.filter_map(|bin| {
let lab = bin.mean();
(lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B)
.then(|| SkinReference {
(lab[0] > SKIN_MIN_LIGHTNESS && lab[1] > SKIN_MIN_A && lab[2] > SKIN_MIN_B).then(
|| SkinReference {
lab,
rgb: bin.mean_rgb(),
})
},
)
})
.max_by(|left, right| {
left.lab[0]
@@ -481,12 +482,18 @@ mod tests {
let report = report_for(&transparent_image_with_center_block([250, 224, 200]), true);
assert!(
report.excluded.iter().any(|(color, _)| color.hex == "#FFD6C2"),
report
.excluded
.iter()
.any(|(color, _)| color.hex == "#FFD6C2"),
"肤色前景应剔除暖浅桃色,excluded: {}",
report.excluded_summary()
);
assert!(
report.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"),
report
.excluded
.iter()
.any(|(color, _)| color.hex == "#FFF2A8"),
"肤色前景应剔除淡黄(Rule 2/3 关键新覆盖),excluded: {}",
report.excluded_summary()
);
@@ -520,7 +527,10 @@ mod tests {
let enabled = report_for(&image, true);
assert!(
enabled.excluded.iter().any(|(color, _)| color.hex == "#FFF2A8"),
enabled
.excluded
.iter()
.any(|(color, _)| color.hex == "#FFF2A8"),
"开启皮肤否决时淡黄应被剔除,excluded: {}",
enabled.excluded_summary()
);
@@ -542,7 +552,10 @@ mod tests {
let report = report_for(&transparent_image_with_center_block([127, 179, 255]), false);
assert!(
report.excluded.iter().any(|(color, _)| color.hex == "#7FB3FF"),
report
.excluded
.iter()
.any(|(color, _)| color.hex == "#7FB3FF"),
"蓝色前景应剔除中度天蓝,excluded: {}",
report.excluded_summary()
);
@@ -38,17 +38,16 @@ async fn main() {
});
let input_bytes = std::fs::read(&input_path)
.unwrap_or_else(|error| panic!("读取测试图片失败({input_path}):{error}"));
println!("[1/5] 已读取测试图片:{input_path}{} 字节)", input_bytes.len());
println!(
"[1/5] 已读取测试图片:{input_path}{} 字节)",
input_bytes.len()
);
// SegmentCommonImage 要求分辨率低于 2000x2000,超限先等比缩小。
const MAX_EDGE: u32 = 1999;
let decoded = image::load_from_memory(&input_bytes).expect("测试图片应可解码");
let input_bytes = if decoded.width() > MAX_EDGE || decoded.height() > MAX_EDGE {
let resized = decoded.resize(
MAX_EDGE,
MAX_EDGE,
image::imageops::FilterType::CatmullRom,
);
let resized = decoded.resize(MAX_EDGE, MAX_EDGE, image::imageops::FilterType::CatmullRom);
let mut buffer = std::io::Cursor::new(Vec::new());
resized
.write_to(&mut buffer, image::ImageFormat::Png)
@@ -70,8 +69,14 @@ async fn main() {
// --- 调用通用抠图 ---
// key 优先级:VIAPI 专用 → 官方 SDK 标准命名(#IMAGE_CALL)→ 短信 key 兜底。
let (matting_key_id, matting_key_secret) = [
("ALIYUN_IMAGESEG_ACCESS_KEY_ID", "ALIYUN_IMAGESEG_ACCESS_KEY_SECRET"),
("ALIBABA_CLOUD_ACCESS_KEY_ID", "ALIBABA_CLOUD_ACCESS_KEY_SECRET"),
(
"ALIYUN_IMAGESEG_ACCESS_KEY_ID",
"ALIYUN_IMAGESEG_ACCESS_KEY_SECRET",
),
(
"ALIBABA_CLOUD_ACCESS_KEY_ID",
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
),
("ALIYUN_SMS_ACCESS_KEY_ID", "ALIYUN_SMS_ACCESS_KEY_SECRET"),
]
.iter()
+69 -51
View File
@@ -162,9 +162,9 @@ pub struct UpstreamFailure {
impl MattingError {
pub fn message(&self) -> &str {
match self {
Self::InvalidConfig(message)
| Self::InvalidRequest(message)
| Self::Sign(message) => message,
Self::InvalidConfig(message) | Self::InvalidRequest(message) | Self::Sign(message) => {
message
}
Self::Upstream(failure) => &failure.message,
}
}
@@ -270,7 +270,10 @@ impl MattingClient {
}
let mut form = BTreeMap::new();
form.insert("Action".to_string(), SEGMENT_COMMON_IMAGE_ACTION.to_string());
form.insert(
"Action".to_string(),
SEGMENT_COMMON_IMAGE_ACTION.to_string(),
);
form.insert("Format".to_string(), "json".to_string());
form.insert("Version".to_string(), IMAGESEG_API_VERSION.to_string());
form.insert("ImageURL".to_string(), image_url);
@@ -451,17 +454,12 @@ impl MattingClient {
}
async fn download_result_image(&self, url: &str) -> Result<Vec<u8>, MattingError> {
let mut response = self
.client
.get(url)
.send()
.await
.map_err(|error| {
MattingError::upstream_transport_error(
describe_result_download_transport_error(&error),
error.is_timeout(),
)
})?;
let mut response = self.client.get(url).send().await.map_err(|error| {
MattingError::upstream_transport_error(
describe_result_download_transport_error(&error),
error.is_timeout(),
)
})?;
let status = response.status();
if !status.is_success() {
return Err(MattingError::upstream_http_error(
@@ -501,9 +499,7 @@ impl MattingClient {
content_type: &str,
) -> Result<String, MattingError> {
if bytes.is_empty() {
return Err(MattingError::InvalidRequest(
"上传内容不能为空".to_string(),
));
return Err(MattingError::InvalidRequest("上传内容不能为空".to_string()));
}
let sts = self.get_oss_sts_token().await?;
let file_name = file_name.trim().trim_matches('/');
@@ -536,7 +532,8 @@ impl MattingClient {
"PUT\n\n{content_type}\n{date}\nx-oss-security-token:{}\n/{VIAPI_TEMP_BUCKET}/{object_key}",
sts.security_token
);
let signature = hmac_sha1_base64(sts.access_key_secret.as_bytes(), string_to_sign.as_bytes())?;
let signature =
hmac_sha1_base64(sts.access_key_secret.as_bytes(), string_to_sign.as_bytes())?;
let authorization = format!("OSS {}:{}", sts.access_key_id, signature);
let target_url = format!("https://{VIAPI_TEMP_OSS_HOST}/{object_key}");
@@ -625,7 +622,9 @@ impl MattingClient {
format!(
"GetOssStsToken 返回失败(HTTP {}Code={}):{}",
http_status.as_u16(),
body.get("Code").and_then(|value| value.as_str()).unwrap_or("unknown"),
body.get("Code")
.and_then(|value| value.as_str())
.unwrap_or("unknown"),
body.get("Message")
.and_then(|value| value.as_str())
.unwrap_or("unknown")
@@ -762,10 +761,7 @@ fn encode_rgba_png(image: &image::RgbaImage) -> Result<Vec<u8>, MattingError> {
image::ExtendedColorType::Rgba8,
)
.map_err(|error| {
MattingError::upstream_response_error(
format!("编码抠图结果 PNG 失败:{error}"),
None,
)
MattingError::upstream_response_error(format!("编码抠图结果 PNG 失败:{error}"), None)
})?;
Ok(encoded)
}
@@ -934,13 +930,7 @@ fn current_aliyun_timestamp() -> String {
fn canonicalize_aliyun_form_params(params: &BTreeMap<String, String>) -> String {
params
.iter()
.map(|(key, value)| {
format!(
"{}={}",
urlencoding_encode(key),
urlencoding_encode(value)
)
})
.map(|(key, value)| format!("{}={}", urlencoding_encode(key), urlencoding_encode(value)))
.collect::<Vec<_>>()
.join("&")
}
@@ -1001,8 +991,10 @@ mod tests {
#[test]
fn upstream_transport_error_classifies_as_transport() {
let error =
MattingError::upstream_transport_error("通用抠图请求失败:dns error".to_string(), false);
let error = MattingError::upstream_transport_error(
"通用抠图请求失败:dns error".to_string(),
false,
);
assert!(error.external_call_attempted());
assert!(error.is_transport());
assert!(!error.is_timeout());
@@ -1032,10 +1024,7 @@ mod tests {
#[test]
fn upstream_response_error_is_external_but_not_transport() {
let error = MattingError::upstream_response_error(
"抠图结果尺寸不一致".to_string(),
None,
);
let error = MattingError::upstream_response_error("抠图结果尺寸不一致".to_string(), None);
assert!(error.external_call_attempted());
assert!(!error.is_transport());
assert!(!error.is_timeout());
@@ -1130,13 +1119,31 @@ mod tests {
let sanitized = sanitize_oss_upload_error_body(&body, token);
// 敏感串全部消失:StringToSign 明文、其中的 token、十六进制、签名串、以及 Message 里的 token 明文。
assert!(!sanitized.contains(token), "STS token 不能残留(含元素外的明文)");
assert!(!sanitized.contains("x-oss-security-token:CAIS"), "StringToSign 明文不能残留");
assert!(!sanitized.contains("sigSECRET"), "SignatureProvided 不能残留");
assert!(!sanitized.contains("50 55 54 0a"), "StringToSignBytes 不能残留");
assert!(
!sanitized.contains(token),
"STS token 不能残留(含元素外的明文)"
);
assert!(
!sanitized.contains("x-oss-security-token:CAIS"),
"StringToSign 明文不能残留"
);
assert!(
!sanitized.contains("sigSECRET"),
"SignatureProvided 不能残留"
);
assert!(
!sanitized.contains("50 55 54 0a"),
"StringToSignBytes 不能残留"
);
// 可诊断信息保留。
assert!(sanitized.contains("SignatureDoesNotMatch"), "OSS Code 应保留供诊断");
assert!(sanitized.contains("[redacted]"), "签名材料元素应被脱敏为 [redacted]");
assert!(
sanitized.contains("SignatureDoesNotMatch"),
"OSS Code 应保留供诊断"
);
assert!(
sanitized.contains("[redacted]"),
"签名材料元素应被脱敏为 [redacted]"
);
}
fn noise_image(width: u32, height: u32) -> image::DynamicImage {
@@ -1151,10 +1158,12 @@ mod tests {
#[test]
fn normalize_keeps_small_image_and_outputs_png() {
let source =
image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(200, 150, image::Rgba([10, 20, 30, 255])));
let (bytes, dims) =
normalize_matting_input_png(&source).expect("normalize should succeed");
let source = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
200,
150,
image::Rgba([10, 20, 30, 255]),
));
let (bytes, dims) = normalize_matting_input_png(&source).expect("normalize should succeed");
assert_eq!(dims, (200, 150), "小图不缩放,尺寸原样");
assert_eq!(
@@ -1168,12 +1177,18 @@ mod tests {
#[test]
fn normalize_caps_oversized_edge_to_1999() {
let source =
image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(2400, 1200, image::Rgba([0, 0, 0, 255])));
let source = image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
2400,
1200,
image::Rgba([0, 0, 0, 255]),
));
let (_bytes, (width, height)) =
normalize_matting_input_png(&source).expect("normalize should succeed");
assert!(width <= MAX_INPUT_EDGE && height <= MAX_INPUT_EDGE, "两边都 ≤1999,实得 {width}x{height}");
assert!(
width <= MAX_INPUT_EDGE && height <= MAX_INPUT_EDGE,
"两边都 ≤1999,实得 {width}x{height}"
);
assert_eq!(width.max(height), MAX_INPUT_EDGE, "最长边压到 1999");
}
@@ -1189,7 +1204,10 @@ mod tests {
let (bytes, (width, height)) = normalize_matting_input_png_within(&source, limit)
.expect("limited normalize should succeed");
assert!(width < 300 && height < 300, "应从 300x300 降尺寸,实得 {width}x{height}");
assert!(
width < 300 && height < 300,
"应从 300x300 降尺寸,实得 {width}x{height}"
);
assert!(
bytes.len() <= limit || width.min(height) <= MIN_INPUT_EDGE + 1,
"编码 {} 字节应落在 {limit} 内(或已触最小边下限)",
@@ -67,12 +67,10 @@ impl SpacetimeClient {
send_connect_once(&connect_sender, Ok(()));
})
.on_disconnect(move |_, error| {
let message = error
.map(|error| error.to_string())
.unwrap_or_else(|| {
"SpacetimeDB profile recharge expiration subscription disconnected"
.to_string()
});
let message = error.map(|error| error.to_string()).unwrap_or_else(|| {
"SpacetimeDB profile recharge expiration subscription disconnected"
.to_string()
});
send_connect_once(
&disconnect_sender,
Err(SpacetimeClientError::Procedure(message)),