feat: 支持coze,dify,派欧云等三方模型调用

This commit is contained in:
ageer
2025-05-11 17:25:02 +08:00
parent 9c2586ab43
commit 84b8d6f675
100 changed files with 350 additions and 169 deletions

View File

@@ -32,12 +32,11 @@ public class ChatConfig {
public OpenAiStreamClient openAiStreamClient() {
String apiHost = configService.getConfigValue("chat", "apiHost");
String apiKey = configService.getConfigValue("chat", "apiKey");
String url = configService.getConfigValue("chat", "apiUrl");
openAiStreamClient = createOpenAiStreamClient(apiHost,apiKey,url);
openAiStreamClient = createOpenAiStreamClient(apiHost,apiKey);
return openAiStreamClient;
}
public static OpenAiStreamClient createOpenAiStreamClient(String apiHost, String apiKey,String url) {
public static OpenAiStreamClient createOpenAiStreamClient(String apiHost, String apiKey) {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
@@ -48,7 +47,6 @@ public class ChatConfig {
.build();
return OpenAiStreamClient.builder()
.apiHost(apiHost)
.apiUrl(url)
.apiKey(Collections.singletonList(apiKey))
.keyStrategy(new KeyRandomStrategy())
.okHttpClient(okHttpClient)

View File

@@ -28,7 +28,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* 描述:聊天管理
* 聊天管理
*
* @author ageerle@163.com
* @date 2023-03-01

View File

@@ -3,7 +3,7 @@ package org.ruoyi.chat.domain.bo;
import lombok.Data;
/**
* 描述:文生视频请求对象
* 文生视频请求对象
*
* @author ageerle@163.com
* date 2024/6/27

View File

@@ -3,7 +3,7 @@ package org.ruoyi.chat.domain.bo;
import lombok.Data;
/**
* 描述:生成歌词
* 生成歌词
*
* @author ageerle@163.com
* date 2024/6/27

View File

@@ -6,6 +6,8 @@ import lombok.Getter;
public enum ChatModeType {
OLLAMA("ollama", "本地部署模型"),
CHAT("chat", "中转模型"),
DIFY("dify", "DIFY"),
COZE("coze", "扣子"),
VECTOR("vector", "知识库向量模型");
private final String code;

View File

@@ -3,7 +3,7 @@ package org.ruoyi.chat.enums;
import lombok.Getter;
/**
* 描述:是否显示
* 是否显示
*
* @author ageerle@163.com
* date 2025/4/10

View File

@@ -0,0 +1,43 @@
package org.ruoyi.chat.factory;
import org.ruoyi.chat.service.chat.IChatService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 聊天服务工厂类
*
* @author ageerle@163.com
* date 2025/5/10
*/
@Component
public class ChatServiceFactory implements ApplicationContextAware {
private final Map<String, IChatService> chatServiceMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 初始化时收集所有IChatService的实现
Map<String, IChatService> serviceMap = applicationContext.getBeansOfType(IChatService.class);
for (IChatService service : serviceMap.values()) {
if (service != null) {
chatServiceMap.put(service.getCategory(), service);
}
}
}
/**
* 根据模型类别获取对应的聊天服务实现
*/
public IChatService getChatService(String category) {
IChatService service = chatServiceMap.get(category);
if (service == null) {
throw new IllegalArgumentException("不支持的模型类别: " + category);
}
return service;
}
}

View File

@@ -24,7 +24,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Objects;
/**
* 描述:OpenAIEventSourceListener
* OpenAIEventSourceListener
*
* @author https:www.unfbx.com
* @date 2023-02-22

View File

@@ -16,6 +16,8 @@ public interface IChatService {
* @param chatRequest 请求对象
*/
SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter);
/**
* 获取此服务支持的模型类别
*/
String getCategory();
}

View File

@@ -1,26 +1,16 @@
package org.ruoyi.chat.service.chat;
import cn.dev33.satoken.stp.StpUtil;
import lombok.RequiredArgsConstructor;
import org.ruoyi.chat.enums.DisplayType;
import org.ruoyi.chat.enums.UserGradeType;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.domain.bo.ChatModelBo;
import org.ruoyi.domain.bo.ChatPackagePlanBo;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.domain.vo.ChatPackagePlanVo;
import org.ruoyi.service.IChatModelService;
import org.ruoyi.service.IChatPackagePlanService;
import org.ruoyi.system.domain.vo.SysUserVo;
import org.ruoyi.system.service.ISysUserService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 描述:用户模型信息
* 用户模型信息
*
* @author ageerle@163.com
* date 2025/4/10
@@ -32,30 +22,9 @@ public class UserModelService {
private final IChatModelService chatModelService;
private final ISysUserService userService;
private final IChatPackagePlanService packagePlanService;
public List<ChatModelVo> modelList(ChatModelBo bo) {
bo.setModelShow(DisplayType.VISIBLE.getCode());
List<ChatModelVo> chatModelList = chatModelService.queryList(bo);
ChatPackagePlanBo sysPackagePlanBo = new ChatPackagePlanBo();
if (StpUtil.isLogin()) {
Long userId = LoginHelper.getLoginUser().getUserId();
SysUserVo sysUserVo = userService.selectUserById(userId);
if (UserGradeType.UNPAID.getCode().equals(sysUserVo.getUserGrade())){
sysPackagePlanBo.setName("Free");
ChatPackagePlanVo chatPackagePlanVo = packagePlanService.queryList(sysPackagePlanBo).get(0);
List<String> array = new ArrayList<>(Arrays.asList(chatPackagePlanVo.getPlanDetail().split(",")));
chatModelList.removeIf(model -> !array.contains(model.getModelName()));
}
}else {
sysPackagePlanBo.setName("Visitor");
ChatPackagePlanVo sysPackagePlanVo = packagePlanService.queryList(sysPackagePlanBo).get(0);
List<String> array = new ArrayList<>(Arrays.asList(sysPackagePlanVo.getPlanDetail().split(",")));
chatModelList.removeIf(model -> !array.contains(model.getModelName()));
}
return new ArrayList<>(chatModelList);
return chatModelService.queryList(bo);
}
}

View File

@@ -0,0 +1,81 @@
package org.ruoyi.chat.service.chat.impl;
import com.coze.openapi.client.chat.CreateChatReq;
import com.coze.openapi.client.chat.model.ChatEvent;
import com.coze.openapi.client.chat.model.ChatEventType;
import com.coze.openapi.client.connversations.message.model.Message;
import com.coze.openapi.service.auth.TokenAuth;
import com.coze.openapi.service.config.Consts;
import com.coze.openapi.service.service.CozeAPI;
import io.reactivex.Flowable;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.service.IChatModelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 扣子聊天管理
*
* @author ageer
*/
@Service
@Slf4j
public class CozeServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
@Override
public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
TokenAuth authCli = new TokenAuth(chatModelVo.getApiKey());
CozeAPI coze =
new CozeAPI.Builder()
.baseURL(chatModelVo.getApiHost())
.auth(authCli)
.readTimeout(10000)
.build();
CreateChatReq req =
CreateChatReq.builder()
.botID(chatModelVo.getModelName())
.userID(chatRequest.getUserId().toString())
.messages(Collections.singletonList(Message.buildUserQuestionText("What can you do?")))
.build();
Flowable<ChatEvent> resp = coze.chat().stream(req);
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
resp.blockingForEach(
event -> {
if (ChatEventType.CONVERSATION_MESSAGE_DELTA.equals(event.getEvent())) {
emitter.send(event.getMessage().getContent());
log.info("coze: {}", event.getMessage().getContent());
}
if (ChatEventType.CONVERSATION_CHAT_COMPLETED.equals(event.getEvent())) {
emitter.complete();
log.info("Token usage: {}", event.getChat().getUsage().getTokenCount());
}
}
);
coze.shutdownExecutor();
});
return emitter;
}
@Override
public String getCategory() {
return ChatModeType.COZE.getCode();
}
}

View File

@@ -0,0 +1,96 @@
package org.ruoyi.chat.service.chat.impl;
import io.github.imfangs.dify.client.DifyClient;
import io.github.imfangs.dify.client.DifyClientFactory;
import io.github.imfangs.dify.client.callback.ChatStreamCallback;
import io.github.imfangs.dify.client.enums.ResponseMode;
import io.github.imfangs.dify.client.event.ErrorEvent;
import io.github.imfangs.dify.client.event.MessageEndEvent;
import io.github.imfangs.dify.client.event.MessageEvent;
import io.github.imfangs.dify.client.model.DifyConfig;
import io.github.imfangs.dify.client.model.chat.ChatMessage;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.service.IChatModelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* dify 聊天管理
*
* @author ageer
*/
@Service
@Slf4j
public class DifyServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
@Override
public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
// 使用自定义配置创建客户端
DifyConfig config = DifyConfig.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.connectTimeout(5000)
.readTimeout(60000)
.writeTimeout(30000)
.build();
DifyClient chatClient = DifyClientFactory.createClient(config);
// 创建聊天消息
ChatMessage message = ChatMessage.builder()
.query(chatRequest.getPrompt())
.user(chatRequest.getUserId().toString())
.responseMode(ResponseMode.STREAMING)
.build();
// 发送流式消息
try {
chatClient.sendChatMessageStream(message, new ChatStreamCallback() {
@SneakyThrows
@Override
public void onMessage(MessageEvent event) {
emitter.send(event.getAnswer());
log.info("收到消息片段: {}", event.getAnswer());
}
@Override
public void onMessageEnd(MessageEndEvent event) {
emitter.complete();
log.info("消息结束完整消息ID: {}", event.getMessageId());
}
@Override
public void onError(ErrorEvent event) {
System.err.println("错误: " + event.getMessage());
}
@Override
public void onException(Throwable throwable) {
System.err.println("异常: " + throwable.getMessage());
}
});
} catch (Exception e) {
log.error("dify请求失败{}", e.getMessage());
}
return emitter;
}
@Override
public String getCategory() {
return ChatModeType.DIFY.getCode();
}
}

View File

@@ -7,6 +7,8 @@ import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
import io.github.ollama4j.models.chat.OllamaChatRequestModel;
import io.github.ollama4j.models.generate.OllamaStreamHandler;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.chat.util.SSEUtil;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.request.ChatRequest;
@@ -22,14 +24,18 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* @author ageer
*/
@Service
@Slf4j
public class OllamaServiceImpl {
public class OllamaServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
@Autowired
private IChatModelService chatModelService;
public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
@Override
public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
String host = chatModelVo.getApiHost();
List<Message> msgList = chatRequest.getMessages();
@@ -73,4 +79,8 @@ public class OllamaServiceImpl {
return emitter;
}
@Override
public String getCategory() {
return ChatModeType.OLLAMA.getCode();
}
}

View File

@@ -3,6 +3,7 @@ package org.ruoyi.chat.service.chat.impl;
import io.modelcontextprotocol.client.McpSyncClient;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.listener.SSEEventSourceListener;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
@@ -21,6 +22,9 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
/**
* @author ageer
*/
@Service
@Slf4j
public class OpenAIServiceImpl implements IChatService {
@@ -28,9 +32,6 @@ public class OpenAIServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
private OpenAiStreamClient openAiStreamClient;
@Value("${spring.ai.mcp.client.enabled}")
private Boolean enabled;
@@ -47,7 +48,7 @@ public class OpenAIServiceImpl implements IChatService {
@Override
public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey(),chatModelVo.getApiUrl());
OpenAiStreamClient openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
List<Message> messages = chatRequest.getMessages();
if (enabled) {
String toolString = mcpChat(chatRequest.getPrompt());
@@ -69,4 +70,9 @@ public class OpenAIServiceImpl implements IChatService {
return this.chatClient.prompt(prompt).call().content();
}
@Override
public String getCategory() {
return ChatModeType.CHAT.getCode();
}
}

View File

@@ -8,7 +8,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ResponseBody;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.factory.ChatServiceFactory;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.chat.service.chat.ISseService;
import org.ruoyi.chat.util.IpUtil;
import org.ruoyi.chat.util.SSEUtil;
@@ -61,9 +63,7 @@ public class SseServiceImpl implements ISseService {
private final IChatModelService chatModelService;
private final OpenAIServiceImpl openAIService;
private final OllamaServiceImpl ollamaService;
private final ChatServiceFactory chatServiceFactory;
private final IChatSessionService chatSessionService;
@@ -95,7 +95,8 @@ public class SseServiceImpl implements ISseService {
chatCostService.deductToken(chatRequest);
}
// 根据模型分类调用不同的处理逻辑
switchModelAndHandle(chatRequest,sseEmitter);
IChatService chatService = chatServiceFactory.getChatService(chatModelVo.getCategory());
chatService.chat(chatRequest, sseEmitter);
} catch (Exception e) {
log.error(e.getMessage(),e);
SSEUtil.sendErrorEvent(sseEmitter,e.getMessage());
@@ -147,17 +148,6 @@ public class SseServiceImpl implements ISseService {
}
}
/**
* 根据模型名称前缀调用不同的处理逻辑
*/
private void switchModelAndHandle(ChatRequest chatRequest,SseEmitter emitter) {
// 调用ollama中部署的本地模型
if (ChatModeType.OLLAMA.getCode().equals(chatModelVo.getCategory())) {
ollamaService.chat(chatRequest,emitter);
} else {
openAIService.chat(chatRequest,emitter);
}
}
/**
* 构建消息列表