31eb75cdad
线上日志(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;不恢复自动重试。
121 lines
5.7 KiB
Java
121 lines
5.7 KiB
Java
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"), "日志行不能带换行");
|
||
}
|
||
}
|