From ae78e3bd5b409c19a667a1b822034a2fe0fce90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 8 Aug 2026 18:13:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=92=E7=BB=9D=E6=9C=AA=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E7=9A=84=E5=9B=BE=E6=A0=87=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 图标规范元数据解析统一检查 OpenAI Chat finish_reason length 与 content_filter 结果进入既有重试循环 新增完整 JSON 仍被不完整原因拒绝的回归测试 --- .../api-server/src/editor_project_icon.rs | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/server-rs/crates/api-server/src/editor_project_icon.rs b/server-rs/crates/api-server/src/editor_project_icon.rs index 67989ef1f..80a19e7e5 100644 --- a/server-rs/crates/api-server/src/editor_project_icon.rs +++ b/server-rs/crates/api-server/src/editor_project_icon.rs @@ -664,33 +664,22 @@ async fn run_icon_spec_extra_param_llm( for attempt in 1..=ICON_SPEC_LLM_MAX_ATTEMPTS { match llm_client.run(request.clone()).await { - Ok(response) => { - let text = response.text.trim(); - if text.is_empty() { - if attempt == ICON_SPEC_LLM_MAX_ATTEMPTS { - return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details( - json!({ - "provider": "editor-icon-spec-llm", - "operation": "complete-extra-param", - "message": "LLM 未返回可用文本", - }), - )); - } - } else { - match parse_extra_param(text) { - Ok(extra_param) => return Ok(extra_param), - Err(_) if attempt < ICON_SPEC_LLM_MAX_ATTEMPTS => {} - Err(_) => { - return Err(AppError::from_status(StatusCode::BAD_GATEWAY) - .with_details(json!({ - "provider": "editor-icon-spec-llm", - "operation": "complete-extra-param", - "message": "LLM 返回内容不符合图标规范参数契约", - }))); - } - } + Ok(response) => match parse_extra_param_response( + response.text.as_str(), + response.finish_reason.as_deref(), + ) { + Ok(extra_param) => return Ok(extra_param), + Err(_) if attempt < ICON_SPEC_LLM_MAX_ATTEMPTS => {} + Err(error) => { + return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details( + json!({ + "provider": "editor-icon-spec-llm", + "operation": "complete-extra-param", + "message": error.message(), + }), + )); } - } + }, Err(error) if is_retryable_llm_error(&error) => { if attempt == ICON_SPEC_LLM_MAX_ATTEMPTS { return Err(map_llm_error(error)); @@ -703,6 +692,39 @@ async fn run_icon_spec_extra_param_llm( unreachable!("icon spec extra param retry loop always returns") } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ExtraParamResponseError { + Empty, + Incomplete, + Invalid, +} + +impl ExtraParamResponseError { + fn message(self) -> &'static str { + match self { + Self::Empty => "LLM 未返回可用文本", + Self::Incomplete => "LLM 返回内容未完整结束", + Self::Invalid => "LLM 返回内容不符合图标规范参数契约", + } + } +} + +fn parse_extra_param_response( + text: &str, + finish_reason: Option<&str>, +) -> Result { + if finish_reason.is_some_and(|reason| { + platform_llm::is_incomplete_finish_reason(platform_llm::LlmApiKind::OpenAiChat, reason) + }) { + return Err(ExtraParamResponseError::Incomplete); + } + let text = text.trim(); + if text.is_empty() { + return Err(ExtraParamResponseError::Empty); + } + parse_extra_param(text).map_err(|_| ExtraParamResponseError::Invalid) +} + fn parse_extra_param(text: &str) -> Result { let mut extra_param = serde_json::from_str::(text).map_err(|_| ())?; for value in [ @@ -2752,6 +2774,20 @@ mod tests { assert!(parse_extra_param(oversized.to_string().as_str()).is_err()); } + #[test] + fn extra_param_rejects_explicitly_incomplete_llm_responses() { + let complete_json = + r#"{"genre":"策略","theme":"现代军事","useCase":"PC","targetUser":"策略游戏玩家"}"#; + + for finish_reason in ["length", "content_filter"] { + assert_eq!( + parse_extra_param_response(complete_json, Some(finish_reason)), + Err(ExtraParamResponseError::Incomplete) + ); + } + assert!(parse_extra_param_response(complete_json, Some("stop")).is_ok()); + } + #[test] fn icon_spec_generation_text_enforces_prompt_max_length() { assert!(