feat(ai): 创意助手出图链路收口——去重试、图片模式包装提示词、输出形状兼容(issue 19)
- 删除 CREATIVE_ASSISTANT_MAX_RETRY / executeCreativeAssistantWithRetry /
isRetryableCreativeFailure,失败即失败;日志仍带 failedNode 与上游 error。
- 图片模式(mode=image)把 user_content 包装成「【任务类型】生成图片…」意图声明,
避免上游「问题分类器」把画面描述判成文案/脚本分支;不带 mode 的入口保持原话直送。
- outputs 形状兼容:image7 这类数组里的 markdown 图片按内容归一到 photo_files
({url,name,type}),findTextValue 支持 List 中的图片字符串;raw 保持上游原样。
- 测试:新增 AiCreativeNoRetryIntegrationTest(2)、AiCreativeImagePromptTest(4)、
AiCreativeImageOutputTest(5),删除两个重试用例;全量 148 跑 0 failures。
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package com.gxwebsoft.ai.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 出图 outputs 的形状兼容(见 .scratch/ai-assistant/issues/19)。
|
||||
*
|
||||
* <p>2026-10-21 在 dev 实例实测到的真实终态是
|
||||
* {@code {"image7": ["", ""]}}:
|
||||
* 变量名和类型都不在端上白名单里,四个页面一律显示「接口已返回,但未解析到可展示的内容」。
|
||||
* 修复思路是**按内容识别图片**并归一到 {@code photo_files},而不是把 {@code image7}
|
||||
* 写进白名单——上游把节点换一次名,白名单就会再失效一次。
|
||||
*/
|
||||
class AiCreativeImageOutputTest {
|
||||
|
||||
private static final String IMAGE_A =
|
||||
"https://ark-acg-cn-beijing.tos-cn-beijing.volces.com/doubao-seedream-5-0-pro/2026/a.jpeg"
|
||||
+ "?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Signature=0123456789abcdef0123456789abcdef";
|
||||
private static final String IMAGE_B =
|
||||
"https://ark-acg-cn-beijing.tos-cn-beijing.volces.com/doubao-seedream-5-0-pro/2026/b.jpeg"
|
||||
+ "?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Signature=fedcba9876543210fedcba9876543210";
|
||||
|
||||
private static Map<String, Object> buildResult(JSONObject outputs) throws Exception {
|
||||
JSONObject data = new JSONObject(true);
|
||||
data.put("status", "succeeded");
|
||||
data.put("outputs", outputs);
|
||||
|
||||
JSONObject responseData = new JSONObject(true);
|
||||
responseData.put("event", "workflow_finished");
|
||||
responseData.put("workflow_run_id", "b2e18332-68bd-49e6-90e2-bd9d16d428ff");
|
||||
responseData.put("task_id", "0b1fdd88-cc6f-45b8-989d-be4f85bcff74");
|
||||
responseData.put("data", data);
|
||||
|
||||
Method method = AiController.class.getDeclaredMethod(
|
||||
"buildCreativeAssistantResult", Map.class, String.class);
|
||||
method.setAccessible(true);
|
||||
//noinspection unchecked
|
||||
return (Map<String, Object>) method.invoke(new AiController(), responseData, "");
|
||||
}
|
||||
|
||||
private static JSONObject imageArrayOutputs() {
|
||||
JSONArray images = new JSONArray();
|
||||
images.add("");
|
||||
images.add("");
|
||||
|
||||
JSONObject outputs = new JSONObject(true);
|
||||
outputs.put("image7", images);
|
||||
return outputs;
|
||||
}
|
||||
|
||||
@Test
|
||||
void image7数组_归一到photo_files并出可渲染正文() throws Exception {
|
||||
Map<String, Object> result = buildResult(imageArrayOutputs());
|
||||
|
||||
assertEquals("succeeded", result.get("status"));
|
||||
Map<?, ?> outputs = (Map<?, ?>) result.get("outputs");
|
||||
assertNotNull(outputs, "成功态必须下发 outputs");
|
||||
List<?> photoFiles = (List<?>) outputs.get("photo_files");
|
||||
assertNotNull(photoFiles, "图片必须归一到 photo_files,端上四个页面才认得:" + outputs);
|
||||
assertEquals(2, photoFiles.size());
|
||||
|
||||
Map<?, ?> first = (Map<?, ?>) photoFiles.get(0);
|
||||
assertEquals(IMAGE_A, first.get("url"));
|
||||
assertEquals("a.jpeg", first.get("name"));
|
||||
assertEquals("image", first.get("type"));
|
||||
|
||||
String answer = String.valueOf(result.get("answer"));
|
||||
assertTrue(answer.contains(""), "answer 也要能直接渲染:" + answer);
|
||||
assertTrue(answer.contains(""), "两张图都要在:" + answer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 归一只影响下发载荷_不动上游raw() throws Exception {
|
||||
Map<String, Object> result = buildResult(imageArrayOutputs());
|
||||
|
||||
Map<?, ?> raw = (Map<?, ?>) result.get("raw");
|
||||
Map<?, ?> rawOutputs = (Map<?, ?>) ((Map<?, ?>) raw.get("data")).get("outputs");
|
||||
assertTrue(rawOutputs.containsKey("image7"), "raw 必须保持上游原样");
|
||||
assertFalse(rawOutputs.containsKey("photo_files"),
|
||||
"补出来的规范字段不能写回 raw,否则日志里的原始报文会被改写:" + rawOutputs);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 老字段photo_file字符串_同样归一() throws Exception {
|
||||
JSONObject outputs = new JSONObject(true);
|
||||
outputs.put("photo_file", "");
|
||||
|
||||
List<?> photoFiles = (List<?>) ((Map<?, ?>) buildResult(outputs).get("outputs")).get("photo_files");
|
||||
assertNotNull(photoFiles, "10-13 的老字段也要能收进规范字段:" + outputs);
|
||||
assertEquals(1, photoFiles.size());
|
||||
assertEquals(IMAGE_A, ((Map<?, ?>) photoFiles.get(0)).get("url"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void photo_files里的裸URL字符串_转成文件对象() throws Exception {
|
||||
JSONArray files = new JSONArray();
|
||||
files.add(IMAGE_A);
|
||||
files.add(IMAGE_B);
|
||||
|
||||
JSONObject outputs = new JSONObject(true);
|
||||
outputs.put("photo_files", files);
|
||||
|
||||
List<?> photoFiles = (List<?>) ((Map<?, ?>) buildResult(outputs).get("outputs")).get("photo_files");
|
||||
assertEquals(2, photoFiles.size(), "字符串元素要转成 {url,name,type}:" + photoFiles);
|
||||
assertEquals(IMAGE_A, ((Map<?, ?>) photoFiles.get(0)).get("url"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 图片与脚本并存_不把文件地址当脚本() throws Exception {
|
||||
JSONObject outputs = new JSONObject(true);
|
||||
outputs.put("Script_result", "### 脚本\n第一幕:开场舞");
|
||||
outputs.put("image7", new JSONArray());
|
||||
|
||||
Map<String, Object> result = buildResult(outputs);
|
||||
assertEquals("### 脚本\n第一幕:开场舞", result.get("answer"));
|
||||
assertFalse(((Map<?, ?>) result.get("outputs")).containsKey("photo_files"),
|
||||
"没有图片时不应补出空的 photo_files");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.gxwebsoft.ai.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 图片模式的提示词包装(见 .scratch/ai-assistant/issues/19)。
|
||||
*
|
||||
* <p>背景:上游创意助手工作流先经「问题分类器」分流。用户只写画面描述
|
||||
* (「毕业季,图书馆门口,暖色」)时,分类器可能判成文案或脚本分支,
|
||||
* 于是同一个需求有时出图、有时出一段文字。端上选了图片模式就已经知道意图,
|
||||
* 后端把这份意图明确写进 {@code user_content},让分类器不必猜。
|
||||
*/
|
||||
class AiCreativeImagePromptTest {
|
||||
|
||||
private static Map<String, Object> buildInputs(Map<String, Object> requestBody) throws Exception {
|
||||
Method method = AiController.class.getDeclaredMethod(
|
||||
"buildCreativeAssistantInputs", Map.class);
|
||||
method.setAccessible(true);
|
||||
//noinspection unchecked
|
||||
return (Map<String, Object>) method.invoke(new AiController(), requestBody);
|
||||
}
|
||||
|
||||
private static Map<String, Object> imageRequest(String userContent) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("user_content", userContent);
|
||||
body.put("mode", "image");
|
||||
return body;
|
||||
}
|
||||
|
||||
@Test
|
||||
void 图片模式_用户原话前带出图意图() throws Exception {
|
||||
String query = "毕业季,图书馆门口,暖色";
|
||||
String content = String.valueOf(buildInputs(imageRequest(query)).get("user_content"));
|
||||
|
||||
assertTrue(content.startsWith("【任务类型】生成图片"),
|
||||
"出图意图必须放在最前面,分类器才会先看到它:" + content);
|
||||
assertTrue(content.contains(query), "用户原话必须原样保留:" + content);
|
||||
assertTrue(content.contains("不要输出文案、脚本或视频"),
|
||||
"要明确排除另外几个分支,否则分类器仍可能走文案分支:" + content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 图片模式_只发user_content不带多余变量() throws Exception {
|
||||
Map<String, Object> inputs = buildInputs(imageRequest("校园夜景海报"));
|
||||
|
||||
assertEquals(1, inputs.size(), "参考素材已下线,不该再往上游送 user_files:" + inputs);
|
||||
assertFalse(inputs.containsKey("user_files"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 文字模式_原样透传不包装() throws Exception {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("user_content", "写一篇迎新晚会推文");
|
||||
body.put("mode", "writing");
|
||||
|
||||
assertEquals("写一篇迎新晚会推文", buildInputs(body).get("user_content"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 未传模式_原样透传不包装() throws Exception {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("user_content", "海报排版");
|
||||
|
||||
assertEquals("海报排版", buildInputs(body).get("user_content"));
|
||||
}
|
||||
}
|
||||
+21
-39
@@ -16,18 +16,19 @@ import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
/**
|
||||
* 创意助手自动重试的端到端验证(见 .scratch/ai-assistant/issues/11)。
|
||||
* 创意助手**不自动重试**的端到端验证(见 .scratch/ai-assistant/issues/19)。
|
||||
*
|
||||
* <p>真起一个本地 HTTP server 扮演 Dify:第一次返回上游容量不足的失败流,
|
||||
* 第二次返回成功流,然后确认 controller **真的重跑了工作流**并把第二次的产出回给端上。
|
||||
* <p>真起一个本地 HTTP server 扮演 Dify:返回的流里带上游容量不足的失败,
|
||||
* 然后确认 controller 只请求了一次、且把失败文案直接回给端上。
|
||||
*
|
||||
* <p>单元测试只能证明"判定函数返回 true";这条用例证明的是"重试确实发生了、且结果被采用",
|
||||
* 也就是用户看到的"有时候能出图、有时候失败"是否真的被修掉。
|
||||
* <p>历史背景:这里曾经是「重试一次」的用例(issue 11)。重试确实能把失败率
|
||||
* 从约三成压到约一成,但出图一次要等 45~105 秒,用户看不到"正在重试",
|
||||
* 体验上就是把一次失败拖成两次等待。现在改为失败即失败。
|
||||
*/
|
||||
class AiCreativeRetryIntegrationTest {
|
||||
class AiCreativeNoRetryIntegrationTest {
|
||||
|
||||
/**
|
||||
* 上游容量不足的原文(2026-10-13 内网实测,request_id 为真实值)。
|
||||
@@ -40,37 +41,37 @@ class AiCreativeRetryIntegrationTest {
|
||||
+ "\"request_id\":\"202610131604021580A5E0F3C983DD3D63\",\"status\":50500}'";
|
||||
|
||||
/**
|
||||
* 凭证失效:重试无用,必须只跑一次。
|
||||
* 凭证失效:同样只跑一次,文案仍是统一的失败提示。
|
||||
*/
|
||||
private static final String ACCESS_DENIED_ERROR =
|
||||
"req_id: dc688258de PluginInvokeError: API request failed with status code 50400,"
|
||||
+ " message: Access Denied";
|
||||
|
||||
@Test
|
||||
void 上游容量不足_自动重跑一次并把第二次产出回给端上() throws Exception {
|
||||
void 上游容量不足_不重跑且直接回可读文案() throws Exception {
|
||||
AtomicInteger hits = new AtomicInteger();
|
||||
try (MockDify dify = MockDify.start(exchange -> {
|
||||
int attempt = hits.incrementAndGet();
|
||||
String stream = attempt == 1 ? capacityFailureStream() : succeededStream();
|
||||
MockDify.reply(exchange, stream);
|
||||
hits.incrementAndGet();
|
||||
MockDify.reply(exchange, capacityFailureStream());
|
||||
})) {
|
||||
ApiResult<?> result = executeCreative(dify.baseUrl());
|
||||
|
||||
assertEquals(2, hits.get(), "第一次撞上上游容量不足后应自动重跑一次");
|
||||
assertEquals(0, result.getCode(), "重试成功后对端仍是正常返回");
|
||||
assertEquals(1, hits.get(), "不再自动重试,工作流只应被请求一次");
|
||||
assertEquals(0, result.getCode(), "失败态仍以 HTTP 200 + status 表达");
|
||||
|
||||
Map<?, ?> payload = (Map<?, ?>) result.getData();
|
||||
assertEquals("succeeded", payload.get("status"), "第二次的状态应透出");
|
||||
assertEquals("迎新晚会海报已生成", payload.get("answer"), "第二次的产出应回给端上");
|
||||
assertEquals("failed", payload.get("status"));
|
||||
assertEquals("生成服务繁忙,请稍后重试", payload.get("answer"));
|
||||
assertFalse(payload.containsKey("raw"), "失败态不得下发整包");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void 凭证失效_不重试且只跑一次() throws Exception {
|
||||
void 凭证失效_只跑一次且文案不变() throws Exception {
|
||||
AtomicInteger hits = new AtomicInteger();
|
||||
try (MockDify dify = MockDify.start(exchange -> {
|
||||
hits.incrementAndGet();
|
||||
MockDify.reply(exchange, failureStream(ACCESS_DENIED_ERROR));
|
||||
MockDify.reply(exchange, failureBody(ACCESS_DENIED_ERROR));
|
||||
})) {
|
||||
ApiResult<?> result = executeCreative(dify.baseUrl());
|
||||
|
||||
@@ -78,7 +79,7 @@ class AiCreativeRetryIntegrationTest {
|
||||
Map<?, ?> payload = (Map<?, ?>) result.getData();
|
||||
assertEquals("failed", payload.get("status"));
|
||||
assertEquals("生成失败,请稍后重试", payload.get("answer"));
|
||||
assertTrue(!payload.containsKey("raw"), "失败态仍不得下发整包");
|
||||
assertFalse(payload.containsKey("raw"), "失败态仍不得下发整包");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +97,7 @@ class AiCreativeRetryIntegrationTest {
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("user_content", "生成一张迎新晚会海报");
|
||||
requestBody.put("mode", "image");
|
||||
|
||||
Method method = AiController.class.getDeclaredMethod(
|
||||
"executeCreativeAssistant", Map.class, Integer.class);
|
||||
@@ -111,10 +113,6 @@ class AiCreativeRetryIntegrationTest {
|
||||
+ failureBody(CAPACITY_ERROR);
|
||||
}
|
||||
|
||||
private static String failureStream(String error) {
|
||||
return failureBody(error);
|
||||
}
|
||||
|
||||
private static String failureBody(String error) {
|
||||
JSONObject data = new JSONObject(true);
|
||||
data.put("status", "failed");
|
||||
@@ -129,22 +127,6 @@ class AiCreativeRetryIntegrationTest {
|
||||
return sse(finished);
|
||||
}
|
||||
|
||||
private static String succeededStream() {
|
||||
JSONObject outputs = new JSONObject(true);
|
||||
outputs.put("photo_message", "迎新晚会海报已生成");
|
||||
|
||||
JSONObject data = new JSONObject(true);
|
||||
data.put("status", "succeeded");
|
||||
data.put("outputs", outputs);
|
||||
|
||||
JSONObject finished = new JSONObject(true);
|
||||
finished.put("event", "workflow_finished");
|
||||
finished.put("workflow_run_id", "run-2");
|
||||
finished.put("task_id", "task-2");
|
||||
finished.put("data", data);
|
||||
return node("即梦AI-图像生成", "succeeded", null) + sse(finished);
|
||||
}
|
||||
|
||||
private static String node(String title, String status, String error) {
|
||||
JSONObject data = new JSONObject(true);
|
||||
data.put("title", title);
|
||||
@@ -1,124 +0,0 @@
|
||||
package com.gxwebsoft.ai.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.gxwebsoft.common.core.utils.JSONUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 上游瞬时故障的重试判定与失败文案(见 .scratch/ai-assistant/issues/11)。
|
||||
*
|
||||
* <p>背景:内网实例上「即梦AI-图像生成」偶发
|
||||
* {@code {"code":50500,...,"status_code":110001,"status_message":"no more instances to retry"}},
|
||||
* 2026-10-13 同一 prompt 连跑 6 次有 2 次失败,整节点 3 秒即返回——是上游算力不足,
|
||||
* 与内容、参数、凭证都无关,重试一次通常就成。
|
||||
*
|
||||
* <p>而凭证失效、模型停用、审核拒绝重试无用,必须排除,
|
||||
* 否则会把用户的一次失败拖成两次等待。
|
||||
*/
|
||||
class AiCreativeRetryTest {
|
||||
|
||||
/**
|
||||
* 2026-10-13 内网实测的上游原文(保留关键结构,request_id 为真实值)。
|
||||
*/
|
||||
private static final String CAPACITY_ERROR =
|
||||
"An error occurred in the shaoruidong/volcengine_ai/volcengine_ai, please contact the"
|
||||
+ " author of shaoruidong/volcengine_ai/volcengine_ai for help, error type:"
|
||||
+ " ToolProviderCredentialValidationError, error details: b'{\"code\":50500,"
|
||||
+ "\"data\":{\"algorithm_base_resp\":{\"status_code\":110001,\"status_message\":"
|
||||
+ "\"no more instances to retry: last error: <nil>\"},\"binary_data_base64\":[]},"
|
||||
+ "\"message\":\"Internal Error\","
|
||||
+ "\"request_id\":\"202610131604021580A5E0F3C983DD3D63\",\"status\":50500}'";
|
||||
|
||||
private static final String ACCESS_DENIED_ERROR =
|
||||
"req_id: dc688258de PluginInvokeError: [models] Error: API request failed with status"
|
||||
+ " code 50400, message: Access Denied";
|
||||
|
||||
private static final String MODEL_DISABLED_ERROR =
|
||||
"req_id: dc688258de PluginInvokeError: Model disabled.";
|
||||
|
||||
private static JSONObject failedEvent(String error) {
|
||||
JSONObject data = new JSONObject(true);
|
||||
data.put("status", "failed");
|
||||
data.put("outputs", new JSONObject(true));
|
||||
data.put("error", error);
|
||||
|
||||
JSONObject event = new JSONObject(true);
|
||||
event.put("event", "workflow_finished");
|
||||
event.put("workflow_run_id", "b2e18332-68bd-49e6-90e2-bd9d16d428ff");
|
||||
event.put("task_id", "0b1fdd88-cc6f-45b8-989d-be4f85bcff74");
|
||||
event.put("data", data);
|
||||
return event;
|
||||
}
|
||||
|
||||
private static Map<String, Object> buildFailedResult(String error) throws Exception {
|
||||
// 直接构造 JSONObject 作为报文体,别绕 JSONUtil.toJSONString——它按值序列化,
|
||||
// 对字符串不加引号,拼出来的报文非法,parseObject 会静默返回 null。
|
||||
JSONObject responseData = failedEvent(error);
|
||||
Method method = AiController.class.getDeclaredMethod(
|
||||
"buildCreativeAssistantResult", Map.class, String.class);
|
||||
method.setAccessible(true);
|
||||
//noinspection unchecked
|
||||
return (Map<String, Object>) method.invoke(new AiController(), responseData, "");
|
||||
}
|
||||
|
||||
private static boolean retryable(String reason) throws Exception {
|
||||
Method method = AiController.class.getDeclaredMethod("isRetryableCreativeFailure", String.class);
|
||||
method.setAccessible(true);
|
||||
return (Boolean) method.invoke(null, reason);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 上游容量不足_值得重试() throws Exception {
|
||||
assertTrue(retryable(CAPACITY_ERROR), "no more instances to retry 属于瞬时故障,应重试");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 上游容量不足_文案换成服务繁忙() throws Exception {
|
||||
Map<String, Object> result = buildFailedResult(CAPACITY_ERROR);
|
||||
assertEquals("failed", result.get("status"), "状态仍须可被判为失败");
|
||||
assertEquals("生成服务繁忙,请稍后重试", result.get("answer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 上游容量不足_失败态仍不得下发上游原文() throws Exception {
|
||||
String json = JSONUtil.toJSONString(buildFailedResult(CAPACITY_ERROR));
|
||||
assertFalse(json.contains("50500"), "不得下发上游错误码:" + json);
|
||||
assertFalse(json.contains("no more instances"), "不得下发上游错误原文:" + json);
|
||||
assertFalse(json.contains("request_id"), "不得下发 req_id:" + json);
|
||||
assertFalse(json.contains("volcengine"), "不得下发上游插件名:" + json);
|
||||
assertFalse(json.contains("raw"), "失败态不得带 raw:" + json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 凭证失效_不重试且文案不变() throws Exception {
|
||||
assertFalse(retryable(ACCESS_DENIED_ERROR), "凭证问题重试无用");
|
||||
assertEquals("生成失败,请稍后重试", buildFailedResult(ACCESS_DENIED_ERROR).get("answer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 模型停用_不重试且文案不变() throws Exception {
|
||||
assertFalse(retryable(MODEL_DISABLED_ERROR), "模型被停用重试无用");
|
||||
assertEquals("生成失败,请稍后重试", buildFailedResult(MODEL_DISABLED_ERROR).get("answer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 没有错误信息_不重试() throws Exception {
|
||||
String blank = "";
|
||||
assertFalse(retryable(null), "错误信息缺失时无从判断,保守不重试");
|
||||
assertFalse(retryable(blank), "错误信息为空时不重试");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 网关瞬时故障_值得重试() throws Exception {
|
||||
assertTrue(retryable("502 Bad Gateway"));
|
||||
assertTrue(retryable("connection reset by peer"));
|
||||
assertTrue(retryable("Read timed out"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user