fix: 统一工作流失败原因与执行状态

- 关联 EASY-2,补齐模型错误分类、节点归属与执行日志

- 覆盖流式失败、重试状态及模型错误映射回归
This commit is contained in:
2026-09-08 10:48:17 +08:00
parent 45c708a212
commit 913a432ee7
18 changed files with 793 additions and 83 deletions

View File

@@ -20,6 +20,7 @@ import com.easyagents.core.model.chat.ChatContext;
import com.easyagents.core.model.chat.ChatModel;
import com.easyagents.core.model.chat.StreamResponseListener;
import com.easyagents.core.model.chat.response.AiMessageResponse;
import com.easyagents.core.model.exception.ModelException;
import com.easyagents.core.parser.AiMessageParser;
import com.easyagents.core.util.StringUtil;
import com.alibaba.fastjson2.JSON;
@@ -57,6 +58,7 @@ public class BaseStreamClientListener implements StreamClientListener {
@Override
public void onMessage(StreamClient client, String response) {
if (isFailure.get() || stoppedFlag.get()) return;
if (StringUtil.noText(response) || "[DONE]".equalsIgnoreCase(response.trim()) || finishedFlag.get()) {
notifyLastMessageAndStop(response);
return;
@@ -64,6 +66,12 @@ public class BaseStreamClientListener implements StreamClientListener {
try {
JSONObject jsonObject = JSON.parseObject(response);
if (jsonObject != null && jsonObject.get("error") != null) {
JSONObject error = jsonObject.get("error") instanceof JSONObject value ? value : new JSONObject();
String code = error.getString("code");
throw new ModelException(200, "Model stream error: " + response, null,
code, error.getString("type"), error.getString("message"));
}
AiMessage delta = messageParser.parse(jsonObject, chatContext);
//合并 增量 delta 到 fullMessage
@@ -77,8 +85,15 @@ public class BaseStreamClientListener implements StreamClientListener {
AiMessageResponse resp = new AiMessageResponse(chatContext, response, delta);
streamResponseListener.onMessage(context, resp);
} catch (Exception err) {
onFailure(this.context.getClient(), err);
onStop(this.context.getClient());
try {
onFailure(client, err);
} finally {
try {
client.stop();
} finally {
onStop(client);
}
}
}
}
@@ -133,6 +148,7 @@ public class BaseStreamClientListener implements StreamClientListener {
@Override
public void onFailure(StreamClient client, Throwable throwable) {
if (stoppedFlag.get() || finishedFlag.get()) return;
if (isFailure.compareAndSet(false, true)) {
context.setThrowable(throwable);
streamResponseListener.onFailure(context, throwable);

View File

@@ -15,6 +15,8 @@
*/
package com.easyagents.core.model.client.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.core.model.exception.ModelException;
import com.easyagents.core.util.StringUtil;
import okhttp3.Response;
@@ -25,11 +27,14 @@ import java.io.IOException;
class Util {
public static Throwable getFailureThrowable(Throwable t, Response response) {
if (t != null) {
if (t != null && response == null) {
return t;
}
if (response != null) {
String errorCode = null;
String errorType = null;
String errorMessage = null;
String errMessage = "Response code: " + response.code();
String message = response.message();
if (StringUtil.hasText(message)) {
@@ -40,12 +45,22 @@ class Util {
String string = body.string();
if (StringUtil.hasText(string)) {
errMessage += ", body: " + string;
try {
JSONObject payload = JSON.parseObject(string);
if (payload != null && payload.get("error") instanceof JSONObject error) {
errorCode = error.getString("code");
errorType = error.getString("type");
errorMessage = error.getString("message");
}
} catch (RuntimeException ignored) {
// 网关可能返回 HTML 或不完整 JSON仍保留 HTTP 状态和原始诊断。
}
}
}
} catch (IOException e) {
// ignore
}
t = new ModelException(errMessage);
t = new ModelException(response.code(), errMessage, t, errorCode, errorType, errorMessage);
}
return t;

View File

@@ -16,6 +16,38 @@
package com.easyagents.core.model.exception;
public class ModelException extends RuntimeException {
private Integer statusCode;
private String errorCode;
private String errorType;
private String errorMessage;
public ModelException(int statusCode, String message, Throwable cause) {
this(statusCode, message, cause, null, null, null);
}
public ModelException(int statusCode, String message, Throwable cause, String errorCode, String errorType, String errorMessage) {
super(message, cause);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.errorType = errorType;
this.errorMessage = errorMessage;
}
public Integer getStatusCode() {
return statusCode;
}
public String getErrorCode() {
return errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public String getErrorType() {
return errorType;
}
/**
* Constructs a new runtime exception with {@code null} as its

View File

@@ -0,0 +1,52 @@
package com.easyagents.core.model.client;
import com.easyagents.core.message.AiMessage;
import com.easyagents.core.model.chat.ChatConfig;
import com.easyagents.core.model.chat.ChatContext;
import com.easyagents.core.model.chat.StreamResponseListener;
import com.easyagents.core.model.chat.response.AiMessageResponse;
import org.junit.Assert;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class StreamFailureLifecycleTest {
@Test
public void completedStreamMustIgnoreLateFailureAndData() {
Fixture fixture = new Fixture();
fixture.listener.onMessage(fixture.client, "{}");
fixture.listener.onMessage(fixture.client, "[DONE]");
fixture.listener.onFailure(fixture.client, new IllegalStateException("late transport error"));
fixture.listener.onMessage(fixture.client, "{}");
Assert.assertEquals(List.of("message", "message", "stop"), fixture.events);
}
@Test
public void failedStreamMustStopWithoutLaterSuccess() {
Fixture fixture = new Fixture();
fixture.listener.onMessage(fixture.client, "{}");
fixture.listener.onMessage(fixture.client, "{\"error\":{\"code\":\"rate_limit_exceeded\"}}");
fixture.listener.onMessage(fixture.client, "[DONE]");
fixture.listener.onMessage(fixture.client, "{}");
fixture.listener.onFailure(fixture.client, new IllegalStateException("cancelled"));
Assert.assertEquals(List.of("message", "failure", "transport-stop", "stop"), fixture.events);
}
private static class Fixture {
final List<String> events = new ArrayList<>();
final StreamClient client = new StreamClient() {
public void start(String url, Map<String, String> headers, String payload, StreamClientListener listener, ChatConfig config) { }
public void stop() { events.add("transport-stop"); }
};
final BaseStreamClientListener listener = new BaseStreamClientListener(null, new ChatContext(), client,
new StreamResponseListener() {
public void onMessage(StreamContext context, AiMessageResponse response) { events.add("message"); }
public void onFailure(StreamContext context, Throwable error) { events.add("failure"); }
public void onStop(StreamContext context) { events.add("stop"); }
}, (json, context) -> {
AiMessage message = new AiMessage(); message.setContent("partial"); return message;
});
}
}

View File

@@ -0,0 +1,39 @@
package com.easyagents.core.model.client.impl;
import com.easyagents.core.model.exception.ModelException;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.Assert;
import org.junit.Test;
public class ModelHttpFailureTest {
@Test
public void shouldPreserveStructuredErrorFromActualProviderShape() {
ModelException error = failure("{\"error\":{\"message\":\"Model not found\",\"code\":404,\"type\":\"NotFound\"}}");
Assert.assertEquals(Integer.valueOf(404), error.getStatusCode());
Assert.assertEquals("404", error.getErrorCode());
Assert.assertEquals("NotFound", error.getErrorType());
Assert.assertEquals("Model not found", error.getErrorMessage());
Assert.assertTrue(error.getMessage().contains("Model not found"));
}
@Test
public void shouldPreserveHttpStatusForNonModelGatewayErrors() {
for (String body : new String[]{"<html>Not Found</html>", "{broken", "{\"error\":\"not found\"}", "null"}) {
ModelException error = failure(body);
Assert.assertEquals(Integer.valueOf(404), error.getStatusCode());
Assert.assertNull(error.getErrorCode());
Assert.assertNull(error.getErrorMessage());
}
}
private ModelException failure(String body) {
Response response = new Response.Builder().request(new Request.Builder().url("http://localhost/chat").build())
.protocol(Protocol.HTTP_1_1).code(404).message("Not Found")
.body(ResponseBody.create(MediaType.get("application/json"), body)).build();
return (ModelException) Util.getFailureThrowable(null, response);
}
}