fix: 统一工作流失败原因与执行状态
- 关联 EASY-2,补齐模型错误分类、节点归属与执行日志 - 覆盖流式失败、重试状态及模型错误映射回归
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user