diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java index 15884158..adf4322b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java @@ -9,6 +9,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission; import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ModelType; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; @@ -23,6 +24,8 @@ import org.ruoyi.common.log.enums.BusinessType; import org.ruoyi.common.excel.utils.ExcelUtil; import org.ruoyi.common.mybatis.core.page.TableDataInfo; +import java.util.LinkedHashMap; + /** * 模型管理 * @@ -55,6 +58,21 @@ public class ChatModelController extends BaseController { return R.ok(chatModelService.queryList(bo)); } + /** + * 获取模型供应商枚举 + */ + @GetMapping("/providerOptions") + public R>> providerOptions() { + List> options = new java.util.ArrayList<>(); + for (ChatModeType type : ChatModeType.values()) { + LinkedHashMap item = new LinkedHashMap<>(); + item.put("label", type.getDescription()); + item.put("value", type.getCode()); + options.add(item); + } + return R.ok(options); + } + /** * 导出模型管理列表 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java index c07d9444..1b91b8a2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java @@ -15,7 +15,8 @@ public enum ChatModeType { DEEP_SEEK("deepseek", "深度求索"), QIAN_WEN("qianwen", "通义千问"), OPEN_AI("openai", "openai"), - PPIO("ppio", "ppio"); + PPIO("ppio", "ppio"), + CUSTOM_API("custom_api", "自定义API"); private final String code; private final String description; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java index 8017fea2..91331c92 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/AbstractChatService.java @@ -1,9 +1,13 @@ package org.ruoyi.service.chat; +import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import java.time.Duration; + /** * 聊天消息Service接口 * @@ -21,6 +25,23 @@ public interface AbstractChatService { */ StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest); + /** + * 创建同步聊天模型(供 Agent/SupervisorAgent 使用) + * 默认实现使用 OpenAI 兼容协议,适用于 OpenAI、DeepSeek、PPIO 等兼容接口的 provider。 + * ZhiPu、QianWen、Ollama 等需覆盖此方法使用各自 SDK。 + * + * @param chatModelVo 模型配置 + * @return 同步聊天模型实例 + */ + default ChatModel buildChatModel(ChatModelVo chatModelVo) { + return OpenAiChatModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .timeout(Duration.ofSeconds(120)) + .build(); + } + /** * 获取服务提供商名称 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/CustomApiServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/CustomApiServiceImpl.java new file mode 100644 index 00000000..a4db18f7 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/CustomApiServiceImpl.java @@ -0,0 +1,72 @@ +package org.ruoyi.service.chat.impl.provider; + +import cn.hutool.core.util.StrUtil; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.enums.ChatModeType; +import org.ruoyi.observability.MyChatModelListener; +import org.ruoyi.service.chat.AbstractChatService; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.List; + +/** + * 自定义 API 服务调用 + * + * 适用于 OpenAI 兼容接口或仅通过通用 HTTP 协议接入的第三方大模型服务。 + * 通过模型配置中的 apiHost / apiKey / modelName 即可复用,不需要再写死具体供应商。 + * + * @author better + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class CustomApiServiceImpl implements AbstractChatService { + + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(180); + + @Override + public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { + return OpenAiStreamingChatModel.builder() + .baseUrl(normalizeBaseUrl(chatModelVo.getApiHost())) + .apiKey(defaultIfBlank(chatModelVo.getApiKey(), "EMPTY")) + .modelName(chatModelVo.getModelName()) + .timeout(DEFAULT_TIMEOUT) + .listeners(List.of(new MyChatModelListener())) + .returnThinking(chatRequest.getEnableThinking()) + .build(); + } + + @Override + public ChatModel buildChatModel(ChatModelVo chatModelVo) { + return OpenAiChatModel.builder() + .baseUrl(normalizeBaseUrl(chatModelVo.getApiHost())) + .apiKey(defaultIfBlank(chatModelVo.getApiKey(), "EMPTY")) + .modelName(chatModelVo.getModelName()) + .timeout(DEFAULT_TIMEOUT) + .build(); + } + + @Override + public String getProviderName() { + return ChatModeType.CUSTOM_API.getCode(); + } + + private String normalizeBaseUrl(String baseUrl) { + if (StrUtil.isBlank(baseUrl)) { + throw new IllegalArgumentException("自定义API的请求地址(apiHost)不能为空"); + } + return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + } + + private String defaultIfBlank(String value, String defaultValue) { + return StrUtil.isBlank(value) ? defaultValue : value; + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java index a9346538..240ed215 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/OllamaServiceImpl.java @@ -1,7 +1,9 @@ package org.ruoyi.service.chat.impl.provider; +import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,6 +39,14 @@ public class OllamaServiceImpl implements AbstractChatService { .build(); } + @Override + public ChatModel buildChatModel(ChatModelVo chatModelVo) { + return OllamaChatModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .modelName(chatModelVo.getModelName()) + .build(); + } + @Override public String getProviderName() { return ChatModeType.OLLAMA.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java index 73289939..2490dd2e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java @@ -1,7 +1,9 @@ package org.ruoyi.service.chat.impl.provider; +import dev.langchain4j.community.model.dashscope.QwenChatModel; import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; +import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -38,6 +40,14 @@ public class QianWenChatServiceImpl implements AbstractChatService { .build(); } + @Override + public ChatModel buildChatModel(ChatModelVo chatModelVo) { + return QwenChatModel.builder() + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .build(); + } + @Override public String getProviderName() { return ChatModeType.QIAN_WEN.getCode(); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java index 9e1b504d..920dbaf1 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java @@ -1,7 +1,9 @@ package org.ruoyi.service.chat.impl.provider; +import dev.langchain4j.community.model.zhipu.ZhipuAiChatModel; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; +import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,6 +37,14 @@ public class ZhiPuChatServiceImpl implements AbstractChatService { .build(); } + @Override + public ChatModel buildChatModel(ChatModelVo chatModelVo) { + return ZhipuAiChatModel.builder() + .apiKey(chatModelVo.getApiKey()) + .model(chatModelVo.getModelName()) + .build(); + } + @Override public String getProviderName() { return ChatModeType.ZHI_PU.getCode();