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;
/**
* 创意生成失败文案的归类。
*
*
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},
* 于是这两例都落到「生成失败,请稍后重试」——把上游繁忙说成用户内容的问题。
*
*
这里直接复刻 {@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 data = new LinkedHashMap<>();
data.put("status", "failed");
data.put("error", upstreamError);
Map 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 responseData = new LinkedHashMap<>();
responseData.put("data", new LinkedHashMap());
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"), "日志行不能带换行");
}
}