mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-13 20:53:42 +08:00
feat: 兼容不选自动模型时的原先逻辑;封装通用方法,简化创建有监控的SSE,简化流式错误输出并通知重试;
This commit is contained in:
@@ -46,12 +46,15 @@ public class SSEEventSourceListener extends EventSourceListener {
|
|||||||
|
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
|
private boolean retryEnabled;
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
public SSEEventSourceListener(SseEmitter emitter,Long userId,Long sessionId, String token) {
|
public SSEEventSourceListener(SseEmitter emitter,Long userId,Long sessionId, String token, boolean retryEnabled) {
|
||||||
this.emitter = emitter;
|
this.emitter = emitter;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
this.retryEnabled = retryEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -129,8 +132,12 @@ public class SSEEventSourceListener extends EventSourceListener {
|
|||||||
if (Objects.isNull(response)) {
|
if (Objects.isNull(response)) {
|
||||||
// 透传错误到前端
|
// 透传错误到前端
|
||||||
SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败");
|
SSEUtil.sendErrorEvent(emitter, t != null ? t.getMessage() : "SSE连接失败");
|
||||||
// 通知重试(以 emitter 为键)
|
if (retryEnabled) {
|
||||||
RetryNotifier.notifyFailure(emitter);
|
// 通知重试(以 emitter 为键)
|
||||||
|
RetryNotifier.notifyFailure(emitter);
|
||||||
|
} else {
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ResponseBody body = response.body();
|
ResponseBody body = response.body();
|
||||||
@@ -142,8 +149,12 @@ public class SSEEventSourceListener extends EventSourceListener {
|
|||||||
log.error("OpenAI sse连接异常data:{},异常:{}", response, t);
|
log.error("OpenAI sse连接异常data:{},异常:{}", response, t);
|
||||||
SSEUtil.sendErrorEvent(emitter, String.valueOf(response));
|
SSEUtil.sendErrorEvent(emitter, String.valueOf(response));
|
||||||
}
|
}
|
||||||
// 通知重试
|
if (retryEnabled) {
|
||||||
RetryNotifier.notifyFailure(emitter);
|
// 通知重试
|
||||||
|
RetryNotifier.notifyFailure(emitter);
|
||||||
|
} else {
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
eventSource.cancel();
|
eventSource.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import java.util.Collections;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import org.ruoyi.chat.support.RetryNotifier;
|
import org.ruoyi.chat.support.RetryNotifier;
|
||||||
|
import org.ruoyi.chat.support.ChatServiceHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扣子聊天管理
|
* 扣子聊天管理
|
||||||
@@ -69,7 +70,7 @@ public class CozeServiceImpl implements IChatService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
RetryNotifier.notifyFailure(emitter);
|
ChatServiceHelper.onStreamError(emitter, ex.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
coze.shutdownExecutor();
|
coze.shutdownExecutor();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import lombok.SneakyThrows;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.chat.enums.ChatModeType;
|
import org.ruoyi.chat.enums.ChatModeType;
|
||||||
import org.ruoyi.chat.service.chat.IChatService;
|
import org.ruoyi.chat.service.chat.IChatService;
|
||||||
|
import org.ruoyi.chat.support.ChatServiceHelper;
|
||||||
import org.ruoyi.common.chat.request.ChatRequest;
|
import org.ruoyi.common.chat.request.ChatRequest;
|
||||||
import org.ruoyi.domain.vo.ChatModelVo;
|
import org.ruoyi.domain.vo.ChatModelVo;
|
||||||
import org.ruoyi.service.IChatModelService;
|
import org.ruoyi.service.IChatModelService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import org.ruoyi.chat.support.RetryNotifier;
|
|
||||||
/**
|
/**
|
||||||
* deepseek
|
* deepseek
|
||||||
*/
|
*/
|
||||||
@@ -58,15 +57,14 @@ public class DeepSeekChatImpl implements IChatService {
|
|||||||
@Override
|
@Override
|
||||||
public void onError(Throwable error) {
|
public void onError(Throwable error) {
|
||||||
System.err.println("错误: " + error.getMessage());
|
System.err.println("错误: " + error.getMessage());
|
||||||
// 通知上层失败,进入重试/降级(以 emitter 为键)
|
ChatServiceHelper.onStreamError(emitter, error.getMessage());
|
||||||
RetryNotifier.notifyFailure(emitter);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("deepseek请求失败:{}", e.getMessage());
|
log.error("deepseek请求失败:{}", e.getMessage());
|
||||||
// 同步异常直接通知失败
|
// 同步异常直接通知失败
|
||||||
RetryNotifier.notifyFailure(emitter);
|
ChatServiceHelper.onStreamError(emitter, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return emitter;
|
return emitter;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import org.ruoyi.chat.support.ChatServiceHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片识别模型
|
* 图片识别模型
|
||||||
@@ -131,7 +132,7 @@ public class ImageServiceImpl implements IChatService {
|
|||||||
// 获取会话token(从入口透传,避免非Web线程取值报错)
|
// 获取会话token(从入口透传,避免非Web线程取值报错)
|
||||||
String token = chatRequest.getToken();
|
String token = chatRequest.getToken();
|
||||||
// 创建 SSE 事件源监听器
|
// 创建 SSE 事件源监听器
|
||||||
SSEEventSourceListener listener = new SSEEventSourceListener(emitter, chatRequest.getUserId(), chatRequest.getSessionId(), token);
|
SSEEventSourceListener listener = ChatServiceHelper.createOpenAiListener(emitter, chatRequest);
|
||||||
|
|
||||||
// 构建聊天完成请求
|
// 构建聊天完成请求
|
||||||
ChatCompletion completion = ChatCompletion
|
ChatCompletion completion = ChatCompletion
|
||||||
@@ -142,7 +143,12 @@ public class ImageServiceImpl implements IChatService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 发起流式聊天完成请求
|
// 发起流式聊天完成请求
|
||||||
openAiStreamClient.streamChatCompletion(completion, listener);
|
try {
|
||||||
|
openAiStreamClient.streamChatCompletion(completion, listener);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
ChatServiceHelper.onStreamError(emitter, ex.getMessage());
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import org.ruoyi.chat.support.RetryNotifier;
|
import org.ruoyi.chat.support.RetryNotifier;
|
||||||
|
import org.ruoyi.chat.support.ChatServiceHelper;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,16 +67,14 @@ public class OllamaServiceImpl implements IChatService {
|
|||||||
try {
|
try {
|
||||||
emitter.send(substr);
|
emitter.send(substr);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
SSEUtil.sendErrorEvent(emitter, e.getMessage());
|
ChatServiceHelper.onStreamError(emitter, e.getMessage());
|
||||||
RetryNotifier.notifyFailure(emitter);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
api.chat(requestModel, streamHandler);
|
api.chat(requestModel, streamHandler);
|
||||||
emitter.complete();
|
emitter.complete();
|
||||||
RetryNotifier.clear(emitter);
|
RetryNotifier.clear(emitter);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
SSEUtil.sendErrorEvent(emitter, e.getMessage());
|
ChatServiceHelper.onStreamError(emitter, e.getMessage());
|
||||||
RetryNotifier.notifyFailure(emitter);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package org.ruoyi.chat.service.chat.impl;
|
package org.ruoyi.chat.service.chat.impl;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
|
||||||
import io.modelcontextprotocol.client.McpSyncClient;
|
import io.modelcontextprotocol.client.McpSyncClient;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.chat.config.ChatConfig;
|
import org.ruoyi.chat.config.ChatConfig;
|
||||||
import org.ruoyi.chat.enums.ChatModeType;
|
import org.ruoyi.chat.enums.ChatModeType;
|
||||||
import org.ruoyi.chat.listener.SSEEventSourceListener;
|
import org.ruoyi.chat.listener.SSEEventSourceListener;
|
||||||
import org.ruoyi.chat.service.chat.IChatService;
|
import org.ruoyi.chat.service.chat.IChatService;
|
||||||
|
import org.ruoyi.chat.support.ChatServiceHelper;
|
||||||
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
|
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
|
||||||
import org.ruoyi.common.chat.entity.chat.Message;
|
import org.ruoyi.common.chat.entity.chat.Message;
|
||||||
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
|
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
|
||||||
@@ -22,7 +22,6 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.ruoyi.chat.support.RetryNotifier;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,8 +57,7 @@ public class OpenAIServiceImpl implements IChatService {
|
|||||||
Message userMessage = Message.builder().content("工具返回信息:"+toolString).role(Message.Role.USER).build();
|
Message userMessage = Message.builder().content("工具返回信息:"+toolString).role(Message.Role.USER).build();
|
||||||
messages.add(userMessage);
|
messages.add(userMessage);
|
||||||
}
|
}
|
||||||
String token = chatRequest.getToken();
|
SSEEventSourceListener listener = ChatServiceHelper.createOpenAiListener(emitter, chatRequest);
|
||||||
SSEEventSourceListener listener = new SSEEventSourceListener(emitter,chatRequest.getUserId(),chatRequest.getSessionId(), token);
|
|
||||||
ChatCompletion completion = ChatCompletion
|
ChatCompletion completion = ChatCompletion
|
||||||
.builder()
|
.builder()
|
||||||
.messages(messages)
|
.messages(messages)
|
||||||
@@ -69,8 +67,7 @@ public class OpenAIServiceImpl implements IChatService {
|
|||||||
try {
|
try {
|
||||||
openAiStreamClient.streamChatCompletion(completion, listener);
|
openAiStreamClient.streamChatCompletion(completion, listener);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// 同步异常也触发失败回调(以 emitter 为键)
|
ChatServiceHelper.onStreamError(emitter, ex.getMessage());
|
||||||
RetryNotifier.notifyFailure(emitter);
|
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
return emitter;
|
return emitter;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.ruoyi.service.IChatModelService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import org.ruoyi.chat.support.ChatServiceHelper;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,12 +58,12 @@ public class QianWenAiChatServiceImpl implements IChatService {
|
|||||||
@Override
|
@Override
|
||||||
public void onError(Throwable error) {
|
public void onError(Throwable error) {
|
||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter);
|
ChatServiceHelper.onStreamError(emitter, error.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("千问请求失败:{}", e.getMessage());
|
log.error("千问请求失败:{}", e.getMessage());
|
||||||
org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter);
|
ChatServiceHelper.onStreamError(emitter, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return emitter;
|
return emitter;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.ruoyi.service.IChatModelService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import org.ruoyi.chat.support.ChatServiceHelper;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -51,9 +52,7 @@ public class ZhipuAiChatServiceImpl implements IChatService {
|
|||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@Override
|
@Override
|
||||||
public void onError(Throwable error) {
|
public void onError(Throwable error) {
|
||||||
// 透传错误并触发重试(以 emitter 为键)
|
ChatServiceHelper.onStreamError(emitter, error.getMessage());
|
||||||
emitter.send(error.getMessage());
|
|
||||||
org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -73,7 +72,7 @@ public class ZhipuAiChatServiceImpl implements IChatService {
|
|||||||
model.chat(chatRequest.getPrompt(), handler);
|
model.chat(chatRequest.getPrompt(), handler);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("智谱清言请求失败:{}", e.getMessage());
|
log.error("智谱清言请求失败:{}", e.getMessage());
|
||||||
org.ruoyi.chat.support.RetryNotifier.notifyFailure(emitter);
|
ChatServiceHelper.onStreamError(emitter, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return emitter;
|
return emitter;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.ruoyi.chat.support;
|
||||||
|
|
||||||
|
import org.ruoyi.chat.listener.SSEEventSourceListener;
|
||||||
|
import org.ruoyi.common.chat.request.ChatRequest;
|
||||||
|
import org.ruoyi.chat.util.SSEUtil;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抽取各聊天实现类的通用逻辑:
|
||||||
|
* - 创建带开关的 SSE 监听器
|
||||||
|
* - 统一的流错误处理(根据是否在重试场景决定通知或直接结束)
|
||||||
|
* - 统一的完成处理(清理回调并 complete)
|
||||||
|
*/
|
||||||
|
public class ChatServiceHelper {
|
||||||
|
|
||||||
|
public static SSEEventSourceListener createOpenAiListener(SseEmitter emitter, ChatRequest chatRequest) {
|
||||||
|
boolean retryEnabled = Boolean.TRUE.equals(chatRequest.getAutoSelectModel());
|
||||||
|
return new SSEEventSourceListener(
|
||||||
|
emitter,
|
||||||
|
chatRequest.getUserId(),
|
||||||
|
chatRequest.getSessionId(),
|
||||||
|
chatRequest.getToken(),
|
||||||
|
retryEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onStreamError(SseEmitter emitter, String errorMessage) {
|
||||||
|
SSEUtil.sendErrorEvent(emitter, errorMessage);
|
||||||
|
if (RetryNotifier.hasCallback(emitter)) {
|
||||||
|
RetryNotifier.notifyFailure(emitter);
|
||||||
|
} else {
|
||||||
|
emitter.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onStreamComplete(SseEmitter emitter) {
|
||||||
|
try {
|
||||||
|
emitter.complete();
|
||||||
|
} finally {
|
||||||
|
RetryNotifier.clear(emitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +39,13 @@ public class RetryNotifier {
|
|||||||
cb.run();
|
cb.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasCallback(Object emitterLike) {
|
||||||
|
if (emitterLike == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return FAILURE_CALLBACKS.containsKey(keyOf(emitterLike));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user