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"), "日志行不能带换行"); } }