5 Commits

Author SHA1 Message Date
wangle
081da6d18d feat: 添加MiniMax作为LLM提供商,合并PR#280并补充监听
合并PR#280的MiniMax provider实现,解决与main分支的冲突,
并在MinimaxServiceImpl中补充MyChatModelListener监听,
与其他provider保持一致。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-17 18:31:53 +08:00
ageerle
74eb5b2530 Merge pull request #290 from yangzhen233/feature/zhipu-embedding
添加智谱向量模型实现
2026-04-17 18:24:24 +08:00
ageerle
b0328fe0ef Merge pull request #291 from xiaonieli7/main
fate:增加自定义模型,调整前端模型选择下拉框
2026-04-17 18:16:59 +08:00
Administrator
2ee0aae57e fate:增加自定义模型,调整前端模型选择下拉框 2026-04-17 08:31:40 +08:00
杨振
d9c3de660a 添加智谱向量模型实现 2026-04-16 21:18:11 +08:00
20 changed files with 537 additions and 214 deletions

View File

@@ -31,8 +31,8 @@
| 模块 | 现有能力
|:----------:|---
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱/MiniMax)、多模态理解、Coze/DIFY/FastGPT平台集成
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型

View File

@@ -34,7 +34,7 @@
| Module | Current Capabilities |
|:---:|---|
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu/MiniMax), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |

View File

@@ -0,0 +1,23 @@
-- ----------------------------
-- Add MiniMax provider
-- ----------------------------
INSERT INTO `chat_provider` (`id`, `provider_name`, `provider_code`, `provider_icon`, `provider_desc`, `api_host`, `status`, `sort_order`, `create_dept`, `create_time`, `create_by`, `update_by`, `update_time`, `remark`, `version`, `del_flag`, `update_ip`, `tenant_id`)
VALUES (2010000000000000001, 'MiniMax', 'minimax', NULL, 'MiniMax大模型服务支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, NULL, NOW(), '1', '1', NOW(), 'MiniMax厂商', NULL, '0', NULL, 0);
-- ----------------------------
-- Add MiniMax chat models
-- ----------------------------
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000002, 'chat', 'MiniMax-M2.7', 'minimax', 'MiniMax-M2.7', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax最新旗舰模型M2.7支持1M上下文窗口', 0);
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000003, 'chat', 'MiniMax-M2.5', 'minimax', 'MiniMax-M2.5', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax M2.5模型204K上下文窗口', 0);
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000004, 'chat', 'MiniMax-M2.5-highspeed', 'minimax', 'MiniMax-M2.5-highspeed', NULL, 'Y', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax M2.5高速版204K上下文窗口更低延迟', 0);
-- ----------------------------
-- Add MiniMax embedding model
-- ----------------------------
INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_dimension`, `model_show`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`)
VALUES (2010000000000000005, 'vector', 'embo-01', 'minimax', 'embo-01', 1536, 'N', 'https://api.minimax.io/v1', '', NULL, 1, NOW(), 1, NOW(), 'MiniMax embo-01嵌入模型1536维度', 0);

View File

@@ -173,6 +173,21 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -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<List<LinkedHashMap<String, String>>> providerOptions() {
List<LinkedHashMap<String, String>> options = new java.util.ArrayList<>();
for (ChatModeType type : ChatModeType.values()) {
LinkedHashMap<String, String> item = new LinkedHashMap<>();
item.put("label", type.getDescription());
item.put("value", type.getCode());
options.add(item);
}
return R.ok(options);
}
/**
* 导出模型管理列表
*/

View File

@@ -1,105 +0,0 @@
package org.ruoyi.controller.knowledge;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphInstanceBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphInstanceVo;
import org.ruoyi.service.knowledge.IKnowledgeGraphInstanceService;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
/**
* 知识图谱实例
*
* @author ageerle
* @date 2025-12-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/graphInstance")
public class KnowledgeGraphInstanceController extends BaseController {
private final IKnowledgeGraphInstanceService knowledgeGraphInstanceService;
/**
* 查询知识图谱实例列表
*/
@SaCheckPermission("system:graphInstance:list")
@GetMapping("/list")
public TableDataInfo<KnowledgeGraphInstanceVo> list(KnowledgeGraphInstanceBo bo, PageQuery pageQuery) {
return knowledgeGraphInstanceService.queryPageList(bo, pageQuery);
}
/**
* 导出知识图谱实例列表
*/
@SaCheckPermission("system:graphInstance:export")
@Log(title = "知识图谱实例", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(KnowledgeGraphInstanceBo bo, HttpServletResponse response) {
List<KnowledgeGraphInstanceVo> list = knowledgeGraphInstanceService.queryList(bo);
ExcelUtil.exportExcel(list, "知识图谱实例", KnowledgeGraphInstanceVo.class, response);
}
/**
* 获取知识图谱实例详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:graphInstance:query")
@GetMapping("/{id}")
public R<KnowledgeGraphInstanceVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(knowledgeGraphInstanceService.queryById(id));
}
/**
* 新增知识图谱实例
*/
@SaCheckPermission("system:graphInstance:add")
@Log(title = "知识图谱实例", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
return toAjax(knowledgeGraphInstanceService.insertByBo(bo));
}
/**
* 修改知识图谱实例
*/
@SaCheckPermission("system:graphInstance:edit")
@Log(title = "知识图谱实例", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
return toAjax(knowledgeGraphInstanceService.updateByBo(bo));
}
/**
* 删除知识图谱实例
*
* @param ids 主键串
*/
@SaCheckPermission("system:graphInstance:remove")
@Log(title = "知识图谱实例", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(knowledgeGraphInstanceService.deleteWithValidByIds(List.of(ids), true));
}
}

View File

@@ -1,105 +0,0 @@
package org.ruoyi.controller.knowledge;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphSegmentBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphSegmentVo;
import org.ruoyi.service.knowledge.IKnowledgeGraphSegmentService;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.common.mybatis.core.page.PageQuery;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
/**
* 知识图谱片段
*
* @author ageerle
* @date 2025-12-17
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/graphSegment")
public class KnowledgeGraphSegmentController extends BaseController {
private final IKnowledgeGraphSegmentService knowledgeGraphSegmentService;
/**
* 查询知识图谱片段列表
*/
@SaCheckPermission("system:graphSegment:list")
@GetMapping("/list")
public TableDataInfo<KnowledgeGraphSegmentVo> list(KnowledgeGraphSegmentBo bo, PageQuery pageQuery) {
return knowledgeGraphSegmentService.queryPageList(bo, pageQuery);
}
/**
* 导出知识图谱片段列表
*/
@SaCheckPermission("system:graphSegment:export")
@Log(title = "知识图谱片段", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(KnowledgeGraphSegmentBo bo, HttpServletResponse response) {
List<KnowledgeGraphSegmentVo> list = knowledgeGraphSegmentService.queryList(bo);
ExcelUtil.exportExcel(list, "知识图谱片段", KnowledgeGraphSegmentVo.class, response);
}
/**
* 获取知识图谱片段详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:graphSegment:query")
@GetMapping("/{id}")
public R<KnowledgeGraphSegmentVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(knowledgeGraphSegmentService.queryById(id));
}
/**
* 新增知识图谱片段
*/
@SaCheckPermission("system:graphSegment:add")
@Log(title = "知识图谱片段", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
return toAjax(knowledgeGraphSegmentService.insertByBo(bo));
}
/**
* 修改知识图谱片段
*/
@SaCheckPermission("system:graphSegment:edit")
@Log(title = "知识图谱片段", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
return toAjax(knowledgeGraphSegmentService.updateByBo(bo));
}
/**
* 删除知识图谱片段
*
* @param ids 主键串
*/
@SaCheckPermission("system:graphSegment:remove")
@Log(title = "知识图谱片段", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(knowledgeGraphSegmentService.deleteWithValidByIds(List.of(ids), true));
}
}

View File

@@ -15,7 +15,9 @@ public enum ChatModeType {
DEEP_SEEK("deepseek", "深度求索"),
QIAN_WEN("qianwen", "通义千问"),
OPEN_AI("openai", "openai"),
PPIO("ppio", "ppio");
PPIO("ppio", "ppio"),
CUSTOM_API("custom_api", "自定义API"),
MINIMAX("minimax", "MiniMax");
private final String code;
private final String description;

View File

@@ -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();
}
/**
* 获取服务提供商名称
*/

View File

@@ -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;
}
}

View File

@@ -0,0 +1,44 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
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.util.List;
/**
* MiniMax服务调用
* <p>
* MiniMax提供OpenAI兼容的API接口支持MiniMax-M2.7、MiniMax-M2.5等模型。
* API地址https://api.minimax.io/v1
*
* @author octopus
* @date 2026/3/21
*/
@Service
@Slf4j
public class MinimaxServiceImpl implements AbstractChatService {
@Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return OpenAiStreamingChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking())
.build();
}
@Override
public String getProviderName() {
return ChatModeType.MINIMAX.getCode();
}
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -0,0 +1,17 @@
package org.ruoyi.service.embed.impl;
import org.springframework.stereotype.Component;
/**
* MiniMax嵌入模型兼容OpenAI接口
* <p>
* 支持embo-01模型1536维度向量。
* API地址https://api.minimax.io/v1
*
* @author octopus
* @date 2026/3/21
*/
@Component("minimax")
public class MinimaxEmbeddingProvider extends OpenAiEmbeddingProvider {
}

View File

@@ -0,0 +1,48 @@
package org.ruoyi.service.embed.impl;
import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ModalityType;
import org.ruoyi.service.embed.BaseEmbedModelService;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
/**
* @Author:yang
* @Date:
* @Description: 智谱AI嵌入模型
*/
@Component("zhipu")
public class ZhipuAiEmbeddingProvider implements BaseEmbedModelService {
protected ChatModelVo chatModelVo;
@Override
public void configure(ChatModelVo config) {
this.chatModelVo = config;
}
@Override
public Set<ModalityType> getSupportedModalities() {
return Set.of(ModalityType.TEXT);
}
@Override
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
EmbeddingModel model = ZhipuAiEmbeddingModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
.model(chatModelVo.getModelName())
.dimensions(chatModelVo.getModelDimension())
.build();
return model.embedAll(textSegments);
}
}

View File

@@ -0,0 +1,42 @@
package org.ruoyi.enums;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for ChatModeType enum
*/
class ChatModeTypeTest {
@Test
void minimaxEnumExists() {
ChatModeType minimax = ChatModeType.MINIMAX;
assertNotNull(minimax);
}
@Test
void minimaxCode_isMinimax() {
assertEquals("minimax", ChatModeType.MINIMAX.getCode());
}
@Test
void minimaxDescription_isMiniMax() {
assertEquals("MiniMax", ChatModeType.MINIMAX.getDescription());
}
@Test
void allProviders_haveUniqueCode() {
ChatModeType[] values = ChatModeType.values();
long uniqueCodes = java.util.Arrays.stream(values)
.map(ChatModeType::getCode)
.distinct()
.count();
assertEquals(values.length, uniqueCodes, "All providers must have unique codes");
}
@Test
void valueOf_minimax() {
assertEquals(ChatModeType.MINIMAX, ChatModeType.valueOf("MINIMAX"));
}
}

View File

@@ -0,0 +1,70 @@
package org.ruoyi.integration;
import dev.langchain4j.model.chat.StreamingChatModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.service.chat.impl.provider.MinimaxServiceImpl;
import static org.junit.jupiter.api.Assertions.*;
/**
* Integration tests for MiniMax provider.
* These tests require a valid MINIMAX_API_KEY environment variable.
*/
@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".+")
class MinimaxIntegrationTest {
private MinimaxServiceImpl minimaxService;
private String apiKey;
@BeforeEach
void setUp() {
minimaxService = new MinimaxServiceImpl();
apiKey = System.getenv("MINIMAX_API_KEY");
}
@Test
void buildStreamingChatModel_withRealApiKey_M27() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey(apiKey);
modelVo.setModelName("MiniMax-M2.7");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model, "Should create streaming model with real API key");
}
@Test
void buildStreamingChatModel_withRealApiKey_M25() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey(apiKey);
modelVo.setModelName("MiniMax-M2.5");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model, "Should create streaming model with M2.5");
}
@Test
void buildStreamingChatModel_withRealApiKey_M25Highspeed() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey(apiKey);
modelVo.setModelName("MiniMax-M2.5-highspeed");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model, "Should create streaming model with M2.5-highspeed");
}
}

View File

@@ -0,0 +1,76 @@
package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for MinimaxServiceImpl
*/
class MinimaxServiceImplTest {
private MinimaxServiceImpl minimaxService;
@BeforeEach
void setUp() {
minimaxService = new MinimaxServiceImpl();
}
@Test
void getProviderName_returnsMinimaxCode() {
assertEquals("minimax", minimaxService.getProviderName());
assertEquals(ChatModeType.MINIMAX.getCode(), minimaxService.getProviderName());
}
@Test
void buildStreamingChatModel_returnsNonNull() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey("test-api-key");
modelVo.setModelName("MiniMax-M2.7");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model);
}
@Test
void buildStreamingChatModel_withThinkingEnabled() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey("test-api-key");
modelVo.setModelName("MiniMax-M2.5");
ChatRequest request = new ChatRequest();
request.setEnableThinking(true);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model);
}
@Test
void buildStreamingChatModel_withHighspeedModel() {
ChatModelVo modelVo = new ChatModelVo();
modelVo.setApiHost("https://api.minimax.io/v1");
modelVo.setApiKey("test-api-key");
modelVo.setModelName("MiniMax-M2.5-highspeed");
ChatRequest request = new ChatRequest();
request.setEnableThinking(false);
StreamingChatModel model = minimaxService.buildStreamingChatModel(modelVo, request);
assertNotNull(model);
}
@Test
void implementsAbstractChatService() {
assertInstanceOf(org.ruoyi.service.chat.AbstractChatService.class, minimaxService);
}
}

View File

@@ -0,0 +1,55 @@
package org.ruoyi.service.embed.impl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ModalityType;
import org.ruoyi.service.embed.BaseEmbedModelService;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for MinimaxEmbeddingProvider
*/
class MinimaxEmbeddingProviderTest {
private MinimaxEmbeddingProvider provider;
@BeforeEach
void setUp() {
provider = new MinimaxEmbeddingProvider();
}
@Test
void implementsBaseEmbedModelService() {
assertInstanceOf(BaseEmbedModelService.class, provider);
}
@Test
void extendsOpenAiEmbeddingProvider() {
assertInstanceOf(OpenAiEmbeddingProvider.class, provider);
}
@Test
void getSupportedModalities_returnsText() {
Set<ModalityType> modalities = provider.getSupportedModalities();
assertNotNull(modalities);
assertTrue(modalities.contains(ModalityType.TEXT));
assertEquals(1, modalities.size());
}
@Test
void configure_setsModelConfig() {
ChatModelVo config = new ChatModelVo();
config.setApiHost("https://api.minimax.io/v1");
config.setApiKey("test-api-key");
config.setModelName("embo-01");
config.setModelDimension(1536);
provider.configure(config);
// configure sets internal state; verify no exception thrown
assertNotNull(provider);
}
}