Files
tuanwei-java/src/test/java/com/gxwebsoft/ai/controller/AiCreativeBusyClassificationTest.java
T
weicw-dshsa 31eb75cdad fix(ai): 创意助手繁忙归类补 503/50508 与流式中断,失败日志保留尾部
线上日志(2026-10-21 18:10/18:50)显示失败节点已从出图节点移到最前面的
「问题分类器」(SiliconFlow):503 code 50508 System is too busy,以及
requests/urllib3 read_chunked 流式读取中断。旧归类只认 50500,用户看到的是
「生成失败」而不是「生成服务繁忙」;且 briefUpstreamError 只留头部,
Python traceback 的异常类型在尾部被截掉。

- isUpstreamCapacityFailure 增补 50508 / 503 / 流式中断等模式
- briefUpstreamError 改为头 2/3 + 标记 + 尾 1/3(仍单行、仍 <900 字符)
- buildCreativeAssistantResult 在成功但无可展示内容时记 WARN(含 outputKeys
  与 photo_files 条数),让解析兜底路径下一次可诊断
- 新增 AiCreativeBusyClassificationTest(6 条)

issue 19;不恢复自动重试。
2026-10-21 19:09:03 +08:00

121 lines
5.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.gxwebsoft.ai.controller;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* 创意生成失败文案的归类。
*
* <p>2026-10-21 线上日志(dev 实例 18:10 / 18:50 两例)显示:失败节点从
* 「即梦AI-图像生成」换成了最前面的「问题分类器」(LLM 节点,provider 为 SiliconFlow),
* 上游返回的是 503 / {@code code:50508 "System is too busy"} 与流式读取中断。
* 旧实现只认 {@code 50500} 与 {@code no more instances to retry},
* 于是这两例都落到「生成失败,请稍后重试」——把上游繁忙说成用户内容的问题。
*
* <p>这里直接复刻 {@code creativeFailureAnswer} 的入口,锁住「哪些原文算繁忙」。
*/
class AiCreativeBusyClassificationTest {
/** 期望的繁忙文案(与 {@code CREATIVE_BUSY_MESSAGE} 同值)。 */
private static final String BUSY = "生成服务繁忙,请稍后重试";
/** 期望的通用失败文案(与 {@code CREATIVE_FAILED_MESSAGE} 同值)。 */
private static final String FAILED = "生成失败,请稍后重试";
/**
* 复刻调用入口:把上游 error 放进 {@code data.error},交给私有的
* {@code creativeFailureAnswer(responseData)}。
*/
private String answerFor(String upstreamError) throws Exception {
Map<String, Object> data = new LinkedHashMap<>();
data.put("status", "failed");
data.put("error", upstreamError);
Map<String, Object> responseData = new LinkedHashMap<>();
responseData.put("event", "workflow_finished");
responseData.put("data", data);
AiController controller = new AiController();
Method method = AiController.class.getDeclaredMethod("creativeFailureAnswer", Map.class);
method.setAccessible(true);
return String.valueOf(method.invoke(controller, responseData));
}
/** 2026-10-21 18:50 的真实原文:SiliconFlow 503 / 50508。 */
@Test
void 硅基流动503繁忙_归为繁忙文案() throws Exception {
String reason = "req_id: 1cdf48dd63 PluginInvokeError: {\"args\":{\"description\":\"[models] "
+ "Error: API request failed with status code 503: {\"code\":50508,"
+ "\"message\":\"System is too busy now. Please try again later.\",\"data\":null}\"}}";
assertEquals(BUSY, answerFor(reason));
}
/** 2026-10-21 18:10 的真实原文:流式响应读到一半被掐断(尾部才是异常类型)。 */
@Test
void 流式读取中断_归为繁忙文案() throws Exception {
String reason = "req_id: b0f0928ba8 PluginInvokeError: {\"args\":{\"traceback\":\"Traceback (most recent call last):\\n"
+ " File \\\"/app/.../requests/models.py\\\", line 937, in generate\\n"
+ " yield from self.raw.stream(chunk_size, decode_content=True)\\n"
+ " File \\\"/app/.../urllib3/response.py\\\", line 1260, in stream\\n"
+ " yield from self.read_chunked(amt, decode_content=decode_content)\\n"
+ "requests.exceptions.ChunkedEncodingError: ('Connection broken: IncompleteRead(0 bytes read)')\"}";
assertEquals(BUSY, answerFor(reason));
}
/** 老规则仍然生效:图片工具没有可用实例。 */
@Test
void 即梦无可用实例_归为繁忙文案() throws Exception {
String reason = "{\"code\":50500,\"status_code\":110001,\"status_message\":\"no more instances to retry\"}";
assertEquals(BUSY, answerFor(reason));
}
/** 凭证/权限类失败不是繁忙,别把「去检查配置」说成「稍后再试」。 */
@Test
void 权限失败_仍为通用失败文案() throws Exception {
String reason = "req_id: dc688258de PluginInvokeError: [models] Error: API request failed with status code 403";
assertEquals(FAILED, answerFor(reason));
}
/** 无 error 字段时不能抛异常,按通用失败处理。 */
@Test
void 无error字段_按通用失败处理() throws Exception {
Map<String, Object> responseData = new LinkedHashMap<>();
responseData.put("data", new LinkedHashMap<String, Object>());
AiController controller = new AiController();
Method method = AiController.class.getDeclaredMethod("creativeFailureAnswer", Map.class);
method.setAccessible(true);
assertEquals(FAILED, String.valueOf(method.invoke(controller, responseData)));
}
/** 截断必须保留尾部:Python traceback 的异常类型只在最后一行。 */
@Test
void 超长上游原文_头尾都保留() throws Exception {
StringBuilder reason = new StringBuilder("HEAD_MARKER");
while (reason.length() < 900) {
reason.append(" middle-padding");
}
reason.append("ChunkedEncodingError: TAIL_MARKER");
AiController controller = new AiController();
Method method = AiController.class.getDeclaredMethod("briefUpstreamError", String.class);
method.setAccessible(true);
String brief = String.valueOf(method.invoke(null, reason.toString()));
org.junit.jupiter.api.Assertions.assertTrue(brief.startsWith("HEAD_MARKER"), brief);
org.junit.jupiter.api.Assertions.assertTrue(brief.endsWith("TAIL_MARKER"), brief);
org.junit.jupiter.api.Assertions.assertTrue(brief.contains("...(truncated)..."), brief);
org.junit.jupiter.api.Assertions.assertTrue(brief.length() < 900, "截断后仍应短:" + brief.length());
org.junit.jupiter.api.Assertions.assertFalse(brief.contains("\n"), "日志行不能带换行");
}
}