20 Commits

Author SHA1 Message Date
ageerle
dbdacdad5c Merge pull request #165 from Code-Mr-Jiu/main
MCP相关功能----进程管理及mcp_info  CRUD
2025-10-12 19:06:27 +08:00
ageerle
ce52402e4c Merge pull request #196 from zhangyue-mars/add-deepseek-java-files
feat: update ChatRequest and DeepSeekChatImpl for DeepSeek integration
2025-10-12 19:06:09 +08:00
ageerle
559661f498 Merge pull request #221 from seven-ocean/feature/aihuman
Feature/aihuman
2025-10-12 19:04:50 +08:00
ageerle
4bacb4bf27 Merge pull request #222 from Cyclones-Y/main
通过策略模式扩展milvus向量库
2025-10-12 19:04:35 +08:00
Yzm
72337563ea feat(chat): 添加根据会话ID查询聊天消息列表接口,优化会话ID设置逻辑 2025-10-12 18:15:11 +08:00
Yzm
77f7ac0af1 refactor(knowledge): 标记向量存储服务为首选实现
- 添加 @Primary 注解以指定为主要 Bean 实现
- 确保在多个实现存在时优先使用该服务
2025-10-11 20:09:15 +08:00
Yzm
c995c94fca Merge remote-tracking branch 'upstream/main' into feat_vectorStore
# Conflicts:
#	ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java
2025-10-10 18:29:58 +08:00
Maxchen
e12e4c4669 Merge remote-tracking branch 'origin/feature/aihuman' into feature/aihuman 2025-10-10 14:11:43 +08:00
Maxchen
3cbbfdf771 Merge remote-tracking branch 'origin/feature/aihuman' into feature/aihuman 2025-10-10 14:11:30 +08:00
Yzm
17c52e9048 refactor(VectorStoreServiceImpl): 添加@Primary注解以指定主要实现
在多个实现存在时,明确指定VectorStoreServiceImpl作为主要实现类
2025-09-29 21:49:27 +08:00
Yzm
f71cf85dc8 feat(knowledge): 实现Milvus向量库策略并重构配置管理
- 新增Milvus向量库策略实现类MilvusVectorStoreStrategy
- 重构向量库配置管理,使用VectorStoreProperties统一配置
- 修改AbstractVectorStoreStrategy抽象类依赖注入方式
- 更新Weaviate策略实现类适配新的配置方式
- 移除旧的ConfigService配置读取方式
- 添加向量库类型配置项,默认使用weaviate
- 实现Milvus集合创建、数据存储、向量搜索和删除功能
- 优化向量库策略工厂类VectorStoreStrategyFactory初始化逻辑
- 删除已废弃的Milvus实现指南文档
- 升级Milvus SDK版本并调整相关API调用方式
2025-09-29 21:45:01 +08:00
Yzm
ef49429543 feat(milvus): 实现Milvus向量数据库集成
- 添加Milvus Java SDK依赖
- 实现MilvusVectorStoreStrategy核心功能
- 支持集合管理、数据存储、向量搜索和数据删除
- 添加Milvus实现指南文档
- 更新数据库连接配置
- 修改VectorStoreService接口添加异常声明
2025-09-29 18:36:48 +08:00
Yzm
39fe2cc48f Merge remote-tracking branch 'upstream/main' into feat_vectorStore 2025-09-28 16:51:16 +08:00
Yzm
aa1c771e72 feat(knowledge): 实现向量库策略模式支持多向量库
- 新增向量库策略接口及抽象基类
- 实现Weaviate向量库策略- 实现Milvus向量库策略(占位实现)
- 添加向量库策略工厂类动态选择实现
- 修改向量存储服务使用策略模式
- 更新知识信息service调用参数顺序
- 添加文档分段和知识片段ID生成注释
- 修改dev环境数据库配置为github版本
2025-09-25 18:44:19 +08:00
ZhangYue
2ac34e313a fix:修复切换知识库的时候只有ID为1L才能查看所有知识库,改为可以查看自己创建的知识库 2025-09-16 10:26:39 +08:00
ZhangYue
4baa970118 feat: update ChatRequest and DeepSeekChatImpl for DeepSeek integration 2025-09-10 09:55:47 +08:00
酒亦
5631fe92c6 Merge branch 'ageerle:main' into main 2025-08-12 15:25:41 +08:00
酒亦
43dc0f419f feat:基于sse模式 启动mcp服务器 (未测试) 2025-08-12 14:00:18 +08:00
酒亦
bc2eb8fdb9 feat:基于stdio模式 启动mcp服务器 2025-08-11 21:22:12 +08:00
酒亦
9891259452 mcp 信息 增删改查 完成 2025-08-10 20:50:04 +08:00
36 changed files with 3342 additions and 271 deletions

View File

@@ -328,3 +328,17 @@ spring:
servers-configuration: classpath:mcp-server.json
request-timeout: 300s
--- # 向量库配置
vector-store:
# 向量存储类型 (weaviate/milvus)
type: weaviate
# Weaviate配置
weaviate:
protocol: http
host: 127.0.0.1:6038
classname: LocalKnowledge
# Milvus配置
milvus:
url: http://localhost:19530
collectionname: LocalKnowledge

View File

@@ -72,6 +72,11 @@ public class ChatRequest {
*/
private Boolean hasAttachment;
/**
* 是否启用深度思考
*/
private Boolean enableThinking;
/**
* 是否自动切换模型
*/
@@ -82,9 +87,4 @@ public class ChatRequest {
*/
private String token;
/**
* 消息ID保存消息成功后设置用于后续扣费更新
*/
private Long messageId;
}

View File

@@ -0,0 +1,62 @@
package org.ruoyi.common.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 向量库配置属性
*
* @author ageer
*/
@Data
@Component
@ConfigurationProperties(prefix = "vector-store")
public class VectorStoreProperties {
/**
* 向量库类型
*/
private String type = "weaviate";
/**
* Weaviate配置
*/
private Weaviate weaviate = new Weaviate();
/**
* Milvus配置
*/
private Milvus milvus = new Milvus();
@Data
public static class Weaviate {
/**
* 协议
*/
private String protocol = "http";
/**
* 主机地址
*/
private String host = "localhost:8080";
/**
* 类名
*/
private String classname = "Document";
}
@Data
public static class Milvus {
/**
* 连接URL
*/
private String url = "http://localhost:19530";
/**
* 集合名称
*/
private String collectionname = "knowledge_base";
}
}

View File

@@ -0,0 +1,67 @@
package org.ruoyi.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.annotation.DataColumn;
import org.ruoyi.core.domain.BaseEntity;
import java.util.Date;
/**
* MCP对象 mcp_info
*
* @author ageerle
* @date Sat Aug 09 16:50:58 CST 2025
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mcp_info")
public class McpInfo extends BaseEntity {
/**
* id
*/
@TableId(value = "mcp_id", type = IdType.AUTO)
private Integer mcpId;
/**
* 服务器名称
*/
private String serverName;
/**
* 链接方式
*/
private String transportType;
/**
* Command
*/
private String command;
/**
* Args
*/
private String arguments;
private String description;
/**
* Env
*/
private String env;
/**
* 是否启用
*/
private Boolean status;
}

View File

@@ -0,0 +1,59 @@
package org.ruoyi.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.ruoyi.domain.McpInfo;
import java.io.Serializable;
/**
* MCP业务对象 mcp_info
*
* @author ageerle
* @date Sat Aug 09 16:50:58 CST 2025
*/
@Data
@AutoMapper(target = McpInfo.class, reverseConvertGenerate = false)
public class McpInfoBo implements Serializable {
/**
* id
*/
@NotNull(message = "id不能为空" )
private Integer mcpId;
/**
* 服务器名称
*/
private String serverName;
/**
* 链接方式
*/
private String transportType;
/**
* Command
*/
private String command;
/**
* Args
*/
private String arguments;
private String description;
/**
* Env
*/
private String env;
/**
* 是否启用
*/
private Boolean status;
}

View File

@@ -0,0 +1,65 @@
package org.ruoyi.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.ruoyi.common.excel.annotation.ExcelDictFormat;
import org.ruoyi.common.excel.convert.ExcelDictConvert;
import org.ruoyi.domain.McpInfo;
import java.io.Serializable;
/**
* MCP视图对象 mcp_info
*
* @author jiyi
* @date Sat Aug 09 16:50:58 CST 2025
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = McpInfo.class)
public class McpInfoVo implements Serializable {
private Integer mcpId;
/**
* 服务器名称
*/
@ExcelProperty(value = "服务器名称")
private String serverName;
/**
* 链接方式
*/
@ExcelProperty(value = "链接方式", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "mcp_transport_type")
private String transportType;
/**
* Command
*/
@ExcelProperty(value = "Command")
private String command;
/**
* Args
*/
@ExcelProperty(value = "Args")
private String arguments;
@ExcelProperty(value = "Description")
private String description;
/**
* Env
*/
@ExcelProperty(value = "Env")
private String env;
/**
* 是否启用
*/
@ExcelProperty(value = "是否启用")
private Boolean status;
}

View File

@@ -0,0 +1,33 @@
package org.ruoyi.mapper;
import org.apache.ibatis.annotations.*;
import org.ruoyi.core.mapper.BaseMapperPlus;
import org.ruoyi.domain.McpInfo;
import org.ruoyi.domain.vo.McpInfoVo;
import java.util.List;
/**
* MCPMapper接口
*
* @author jiuyi
* @date Sat Aug 09 16:50:58 CST 2025
*/
@Mapper
public interface McpInfoMapper extends BaseMapperPlus<McpInfo, McpInfoVo> {
@Select("SELECT * FROM mcp_info WHERE server_name = #{serverName}")
McpInfo selectByServerName(@Param("serverName") String serverName);
@Select("SELECT * FROM mcp_info WHERE status = 1")
List<McpInfo> selectActiveServers();
@Select("SELECT server_name FROM mcp_info WHERE status = 1")
List<String> selectActiveServerNames();
@Update("UPDATE mcp_info SET status = #{status} WHERE server_name = #{serverName}")
int updateActiveStatus(@Param("serverName") String serverName, @Param("status") Boolean status);
@Delete("DELETE FROM mcp_info WHERE server_name = #{serverName}")
int deleteByServerName(@Param("serverName") String serverName);
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.ruoyi.mapper.McpInfoMapper">
</mapper>

View File

@@ -74,6 +74,12 @@
<version>1.19.6</version>
</dependency>
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.6.4</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>

View File

@@ -1,5 +1,6 @@
package org.ruoyi.service;
import org.ruoyi.common.core.exception.ServiceException;
import org.ruoyi.domain.bo.QueryVectorBo;
import org.ruoyi.domain.bo.StoreEmbeddingBo;
@@ -11,15 +12,15 @@ import java.util.List;
*/
public interface VectorStoreService {
void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo);
void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) throws ServiceException;
List<String> getQueryVector(QueryVectorBo queryVectorBo);
void createSchema(String kid,String modelName);
void createSchema(String vectorModelName, String kid,String modelName);
void removeById(String id,String modelName);
void removeById(String id,String modelName) throws ServiceException;
void removeByDocId(String docId, String kid);
void removeByDocId(String docId, String kid) throws ServiceException;
void removeByFid(String fid, String kid);
void removeByFid(String fid, String kid) throws ServiceException;
}

View File

@@ -1,36 +1,14 @@
package org.ruoyi.service.impl;
import cn.hutool.json.JSONObject;
import com.google.protobuf.ServiceException;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.weaviate.WeaviateEmbeddingStore;
import io.weaviate.client.Config;
import io.weaviate.client.WeaviateClient;
import io.weaviate.client.base.Result;
import io.weaviate.client.v1.batch.api.ObjectsBatchDeleter;
import io.weaviate.client.v1.batch.model.BatchDeleteResponse;
import io.weaviate.client.v1.filters.Operator;
import io.weaviate.client.v1.filters.WhereFilter;
import io.weaviate.client.v1.graphql.model.GraphQLResponse;
import io.weaviate.client.v1.schema.model.Property;
import io.weaviate.client.v1.schema.model.Schema;
import io.weaviate.client.v1.schema.model.WeaviateClass;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.service.ConfigService;
import org.ruoyi.domain.bo.QueryVectorBo;
import org.ruoyi.domain.bo.StoreEmbeddingBo;
import org.ruoyi.embedding.BaseEmbedModelService;
import org.ruoyi.embedding.EmbeddingModelFactory;
import org.ruoyi.service.VectorStoreService;
import org.ruoyi.service.strategy.VectorStoreStrategy;
import org.ruoyi.service.strategy.VectorStoreStrategyFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@@ -41,210 +19,62 @@ import java.util.stream.Collectors;
* @author ageer
*/
@Service
@Primary
@Slf4j
@RequiredArgsConstructor
public class VectorStoreServiceImpl implements VectorStoreService {
private final ConfigService configService;
private final VectorStoreStrategyFactory strategyFactory;
// private EmbeddingStore<TextSegment> embeddingStore;
private WeaviateClient client;
private final EmbeddingModelFactory embeddingModelFactory;
/**
* 获取当前配置的向量库策略
*/
private VectorStoreStrategy getCurrentStrategy() {
return strategyFactory.getStrategy();
}
@Override
public void createSchema(String kid, String modelName) {
String protocol = configService.getConfigValue("weaviate", "protocol");
String host = configService.getConfigValue("weaviate", "host");
String className = configService.getConfigValue("weaviate", "classname")+kid;
// 创建 Weaviate 客户端
client= new WeaviateClient(new Config(protocol, host));
// 检查类是否存在,如果不存在就创建 schema
Result<Schema> schemaResult = client.schema().getter().run();
Schema schema = schemaResult.getResult();
boolean classExists = false;
for (WeaviateClass weaviateClass : schema.getClasses()) {
if (weaviateClass.getClassName().equals(className)) {
classExists = true;
break;
}
}
if (!classExists) {
// 类不存在,创建 schema
WeaviateClass build = WeaviateClass.builder()
.className(className)
.vectorizer("none")
.properties(
List.of(Property.builder().name("text").dataType(Collections.singletonList("text")).build(),
Property.builder().name("fid").dataType(Collections.singletonList("text")).build(),
Property.builder().name("kid").dataType(Collections.singletonList("text")).build(),
Property.builder().name("docId").dataType(Collections.singletonList("text")).build())
)
.build();
Result<Boolean> createResult = client.schema().classCreator().withClass(build).run();
if (createResult.hasErrors()) {
log.error("Schema 创建失败: {}", createResult.getError());
} else {
log.info("Schema 创建成功: {}", className);
}
}
// embeddingStore = WeaviateEmbeddingStore.builder()
// .scheme(protocol)
// .host(host)
// .objectClass(className)
// .scheme(protocol)
// .avoidDups(true)
// .consistencyLevel("ALL")
// .build();
public void createSchema(String vectorModelName, String kid, String modelName) {
log.info("创建向量库schema: vectorModelName={}, kid={}, modelName={}", vectorModelName, kid, modelName);
VectorStoreStrategy strategy = getCurrentStrategy();
strategy.createSchema(vectorModelName, kid, modelName);
}
@Override
public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) {
createSchema(storeEmbeddingBo.getKid(), storeEmbeddingBo.getVectorModelName());
BaseEmbedModelService model = embeddingModelFactory.createModel(storeEmbeddingBo.getEmbeddingModelId());
List<String> chunkList = storeEmbeddingBo.getChunkList();
List<String> fidList = storeEmbeddingBo.getFids();
String kid = storeEmbeddingBo.getKid();
String docId = storeEmbeddingBo.getDocId();
long startTime = System.currentTimeMillis();
for (int i = 0; i < chunkList.size(); i++) {
String text = chunkList.get(i);
String fid = fidList.get(i);
Embedding embedding = model.embed(text).content();
Map<String, Object> properties = Map.of(
"text", text,
"fid",fid,
"kid", kid,
"docId", docId
);
Float[] vector = toObjectArray(embedding.vector());
client.data().creator()
.withClassName("LocalKnowledge" + kid) // 注意替换成实际类名
.withProperties(properties)
.withVector(vector)
.run();
}
long endTime = System.currentTimeMillis();
log.info("向量存储完成消耗时间:"+ (endTime-startTime)/1000+"");
log.info("存储向量数据: kid={}, docId={}, 数据条数={}",
storeEmbeddingBo.getKid(), storeEmbeddingBo.getDocId(), storeEmbeddingBo.getChunkList().size());
VectorStoreStrategy strategy = getCurrentStrategy();
strategy.storeEmbeddings(storeEmbeddingBo);
}
private static Float[] toObjectArray(float[] primitive) {
Float[] result = new Float[primitive.length];
for (int i = 0; i < primitive.length; i++) {
result[i] = primitive[i]; // 自动装箱
}
return result;
}
@Override
public List<String> getQueryVector(QueryVectorBo queryVectorBo) {
createSchema(queryVectorBo.getKid(), queryVectorBo.getVectorModelName());
BaseEmbedModelService model = embeddingModelFactory.createModel(queryVectorBo.getEmbeddingModelId());
Embedding queryEmbedding = model.embed(queryVectorBo.getQuery()).content();
float[] vector = queryEmbedding.vector();
List<String> vectorStrings = new ArrayList<>();
for (float v : vector) {
vectorStrings.add(String.valueOf(v));
}
String vectorStr = String.join(",", vectorStrings);
String className = configService.getConfigValue("weaviate", "classname") ;
// 构建 GraphQL 查询
String graphQLQuery = String.format(
"{\n" +
" Get {\n" +
" %s(nearVector: {vector: [%s]} limit: %d) {\n" +
" text\n" +
" fid\n" +
" kid\n" +
" docId\n" +
" _additional {\n" +
" distance\n" +
" id\n" +
" }\n" +
" }\n" +
" }\n" +
"}",
className+ queryVectorBo.getKid(),
vectorStr,
queryVectorBo.getMaxResults()
);
Result<GraphQLResponse> result = client.graphQL().raw().withQuery(graphQLQuery).run();
List<String> resultList = new ArrayList<>();
if (result != null && !result.hasErrors()) {
Object data = result.getResult().getData();
JSONObject entries = new JSONObject(data);
Map<String, cn.hutool.json.JSONArray> entriesMap = entries.get("Get", Map.class);
cn.hutool.json.JSONArray objects = entriesMap.get(className + queryVectorBo.getKid());
if(objects.isEmpty()){
return resultList;
}
for (Object object : objects) {
Map<String, String> map = (Map<String, String>) object;
String content = map.get("text");
resultList.add( content);
}
return resultList;
} else {
log.error("GraphQL 查询失败: {}", result.getError());
return resultList;
}
log.info("查询向量数据: kid={}, query={}, maxResults={}",
queryVectorBo.getKid(), queryVectorBo.getQuery(), queryVectorBo.getMaxResults());
VectorStoreStrategy strategy = getCurrentStrategy();
return strategy.getQueryVector(queryVectorBo);
}
@Override
@SneakyThrows
public void removeById(String id, String modelName) {
String protocol = configService.getConfigValue("weaviate", "protocol");
String host = configService.getConfigValue("weaviate", "host");
String className = configService.getConfigValue("weaviate", "classname");
String finalClassName = className + id;
WeaviateClient client = new WeaviateClient(new Config(protocol, host));
Result<Boolean> result = client.schema().classDeleter().withClassName(finalClassName).run();
if (result.hasErrors()) {
log.error("失败删除向量: " + result.getError());
throw new ServiceException("失败删除向量数据!");
} else {
log.info("成功删除向量数据: " + result.getResult());
}
log.info("根据ID删除向量数据: id={}, modelName={}", id, modelName);
VectorStoreStrategy strategy = getCurrentStrategy();
strategy.removeById(id, modelName);
}
@Override
public void removeByDocId(String docId, String kid) {
String className = configService.getConfigValue("weaviate", "classname") + kid;
// 构建 Where 条件
WhereFilter whereFilter = WhereFilter.builder()
.path("docId")
.operator(Operator.Equal)
.valueText(docId)
.build();
ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter();
Result<BatchDeleteResponse> result = deleter.withClassName(className)
.withWhere(whereFilter)
.run();
if (result != null && !result.hasErrors()) {
log.info("成功删除 docId={} 的所有向量数据", docId);
} else {
log.error("删除失败: {}", result.getError());
}
log.info("根据docId删除向量数据: docId={}, kid={}", docId, kid);
VectorStoreStrategy strategy = getCurrentStrategy();
strategy.removeByDocId(docId, kid);
}
@Override
public void removeByFid(String fid, String kid) {
String className = configService.getConfigValue("weaviate", "classname") + kid;
// 构建 Where 条件
WhereFilter whereFilter = WhereFilter.builder()
.path("fid")
.operator(Operator.Equal)
.valueText(fid)
.build();
ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter();
Result<BatchDeleteResponse> result = deleter.withClassName(className)
.withWhere(whereFilter)
.run();
if (result != null && !result.hasErrors()) {
log.info("成功删除 fid={} 的所有向量数据", fid);
} else {
log.error("删除失败: {}", result.getError());
}
log.info("根据fid删除向量数据: fid={}, kid={}", fid, kid);
VectorStoreStrategy strategy = getCurrentStrategy();
strategy.removeByFid(fid, kid);
}
}

View File

@@ -0,0 +1,62 @@
package org.ruoyi.service.strategy;
import org.ruoyi.common.core.exception.ServiceException;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.config.VectorStoreProperties;
/**
* 向量库策略抽象基类
* 提供公共的方法实现如embedding模型获取等
*
* @author ageer
*/
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractVectorStoreStrategy implements VectorStoreStrategy {
protected final VectorStoreProperties vectorStoreProperties;
/**
* 获取向量模型
*/
@SneakyThrows
protected EmbeddingModel getEmbeddingModel(String modelName, String apiKey, String baseUrl) {
EmbeddingModel embeddingModel;
if ("quentinz/bge-large-zh-v1.5".equals(modelName)) {
embeddingModel = OllamaEmbeddingModel.builder()
.baseUrl(baseUrl)
.modelName(modelName)
.build();
} else if ("baai/bge-m3".equals(modelName)) {
embeddingModel = OpenAiEmbeddingModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(modelName)
.build();
} else {
throw new ServiceException("未找到对应向量化模型!");
}
return embeddingModel;
}
/**
* 将float数组转换为Float对象数组
*/
protected static Float[] toObjectArray(float[] primitive) {
Float[] result = new Float[primitive.length];
for (int i = 0; i < primitive.length; i++) {
result[i] = primitive[i]; // 自动装箱
}
return result;
}
/**
* 获取向量库类型标识
*/
public abstract String getVectorStoreType();
}

View File

@@ -0,0 +1,18 @@
package org.ruoyi.service.strategy;
import org.ruoyi.service.VectorStoreService;
/**
* 向量库策略接口
* 继承VectorStoreService以避免重复定义相同的方法
*
* @author ageer
*/
public interface VectorStoreStrategy extends VectorStoreService {
/**
* 获取向量库类型标识
* @return 向量库类型weaviate, milvus
*/
String getVectorStoreType();
}

View File

@@ -0,0 +1,74 @@
package org.ruoyi.service.strategy;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.config.VectorStoreProperties;
import org.ruoyi.service.strategy.impl.MilvusVectorStoreStrategy;
import org.ruoyi.service.strategy.impl.WeaviateVectorStoreStrategy;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 向量库策略工厂
* 根据配置动态选择向量库实现
*
* @author ageer
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class VectorStoreStrategyFactory {
private final VectorStoreProperties vectorStoreProperties;
private final WeaviateVectorStoreStrategy weaviateStrategy;
private final MilvusVectorStoreStrategy milvusStrategy;
private Map<String, VectorStoreStrategy> strategies;
@PostConstruct
public void init() {
strategies = new HashMap<>();
strategies.put("weaviate", weaviateStrategy);
strategies.put("milvus", milvusStrategy);
log.info("向量库策略工厂初始化完成,支持的策略: {}", strategies.keySet());
}
/**
* 获取当前配置的向量库策略
*/
public VectorStoreStrategy getStrategy() {
String vectorStoreType = vectorStoreProperties.getType();
if (vectorStoreType == null || vectorStoreType.trim().isEmpty()) {
vectorStoreType = "weaviate"; // 默认使用weaviate
}
VectorStoreStrategy strategy = strategies.get(vectorStoreType.toLowerCase());
if (strategy == null) {
log.warn("未找到向量库策略: {}, 使用默认策略: weaviate", vectorStoreType);
strategy = strategies.get("weaviate");
}
log.debug("使用向量库策略: {}", vectorStoreType);
return strategy;
}
/**
* 根据类型获取向量库策略
*/
public VectorStoreStrategy getStrategy(String type) {
if (type == null || type.trim().isEmpty()) {
return getStrategy();
}
VectorStoreStrategy strategy = strategies.get(type.toLowerCase());
if (strategy == null) {
log.warn("未找到向量库策略: {}, 使用默认策略", type);
return getStrategy();
}
return strategy;
}
}

View File

@@ -0,0 +1,337 @@
package org.ruoyi.service.strategy.impl;
import org.ruoyi.common.core.exception.ServiceException;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.model.embedding.EmbeddingModel;
import io.milvus.client.MilvusServiceClient;
import io.milvus.common.clientenum.ConsistencyLevelEnum;
import io.milvus.grpc.*;
import io.milvus.param.*;
import io.milvus.param.collection.*;
import io.milvus.param.dml.DeleteParam;
import io.milvus.param.dml.InsertParam;
import io.milvus.param.dml.SearchParam;
import io.milvus.param.index.CreateIndexParam;
import io.milvus.param.index.DescribeIndexParam;
import io.milvus.response.DescCollResponseWrapper;
import io.milvus.response.SearchResultsWrapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.config.VectorStoreProperties;
import org.ruoyi.domain.bo.QueryVectorBo;
import org.ruoyi.domain.bo.StoreEmbeddingBo;
import org.ruoyi.service.strategy.AbstractVectorStoreStrategy;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* Milvus向量库策略实现
*
* @author ageer
*/
@Slf4j
@Component
public class MilvusVectorStoreStrategy extends AbstractVectorStoreStrategy {
private MilvusServiceClient milvusClient;
public MilvusVectorStoreStrategy(VectorStoreProperties vectorStoreProperties) {
super(vectorStoreProperties);
}
@Override
public String getVectorStoreType() {
return "milvus";
}
@Override
public void createSchema(String vectorModelName, String kid, String modelName) {
String url = vectorStoreProperties.getMilvus().getUrl();
String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid;
// 创建Milvus客户端连接
ConnectParam connectParam = ConnectParam.newBuilder()
.withUri(url)
.build();
milvusClient = new MilvusServiceClient(connectParam);
// 检查集合是否存在
HasCollectionParam hasCollectionParam = HasCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build();
R<Boolean> hasCollectionResponse = milvusClient.hasCollection(hasCollectionParam);
if (hasCollectionResponse.getStatus() != R.Status.Success.getCode()) {
log.error("检查集合是否存在失败: {}", hasCollectionResponse.getMessage());
return;
}
if (!hasCollectionResponse.getData()) {
// 创建字段
List<FieldType> fields = new ArrayList<>();
// ID字段 (主键)
fields.add(FieldType.newBuilder()
.withName("id")
.withDataType(DataType.Int64)
.withPrimaryKey(true)
.withAutoID(true)
.build());
// 文本字段
fields.add(FieldType.newBuilder()
.withName("text")
.withDataType(DataType.VarChar)
.withMaxLength(65535)
.build());
// fid字段
fields.add(FieldType.newBuilder()
.withName("fid")
.withDataType(DataType.VarChar)
.withMaxLength(255)
.build());
// kid字段
fields.add(FieldType.newBuilder()
.withName("kid")
.withDataType(DataType.VarChar)
.withMaxLength(255)
.build());
// docId字段
fields.add(FieldType.newBuilder()
.withName("docId")
.withDataType(DataType.VarChar)
.withMaxLength(255)
.build());
// 向量字段
fields.add(FieldType.newBuilder()
.withName("vector")
.withDataType(DataType.FloatVector)
.withDimension(1024) // 根据实际embedding维度调整
.build());
// 创建集合
CreateCollectionParam createCollectionParam = CreateCollectionParam.newBuilder()
.withCollectionName(collectionName)
.withDescription("Knowledge base collection for " + kid)
.withShardsNum(2)
.withFieldTypes(fields)
.build();
R<RpcStatus> createCollectionResponse = milvusClient.createCollection(createCollectionParam);
if (createCollectionResponse.getStatus() != R.Status.Success.getCode()) {
log.error("创建集合失败: {}", createCollectionResponse.getMessage());
return;
}
// 创建索引
CreateIndexParam createIndexParam = CreateIndexParam.newBuilder()
.withCollectionName(collectionName)
.withFieldName("vector")
.withIndexType(IndexType.IVF_FLAT)
.withMetricType(MetricType.L2)
.withExtraParam("{\"nlist\":1024}")
.build();
R<RpcStatus> createIndexResponse = milvusClient.createIndex(createIndexParam);
if (createIndexResponse.getStatus() != R.Status.Success.getCode()) {
log.error("创建索引失败: {}", createIndexResponse.getMessage());
} else {
log.info("Milvus集合和索引创建成功: {}", collectionName);
}
} else {
log.info("Milvus集合已存在: {}", collectionName);
}
}
@Override
public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) {
createSchema(storeEmbeddingBo.getVectorModelName(), storeEmbeddingBo.getKid(), storeEmbeddingBo.getVectorModelName());
EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName(),
storeEmbeddingBo.getApiKey(), storeEmbeddingBo.getBaseUrl());
List<String> chunkList = storeEmbeddingBo.getChunkList();
List<String> fidList = storeEmbeddingBo.getFids();
String kid = storeEmbeddingBo.getKid();
String docId = storeEmbeddingBo.getDocId();
String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid;
log.info("Milvus向量存储条数记录: " + chunkList.size());
long startTime = System.currentTimeMillis();
// 准备批量插入数据
List<InsertParam.Field> fields = new ArrayList<>();
List<String> textList = new ArrayList<>();
List<String> fidListData = new ArrayList<>();
List<String> kidList = new ArrayList<>();
List<String> docIdList = new ArrayList<>();
List<List<Float>> vectorList = new ArrayList<>();
for (int i = 0; i < chunkList.size(); i++) {
String text = chunkList.get(i);
String fid = fidList.get(i);
Embedding embedding = embeddingModel.embed(text).content();
textList.add(text);
fidListData.add(fid);
kidList.add(kid);
docIdList.add(docId);
List<Float> vector = new ArrayList<>();
for (float f : embedding.vector()) {
vector.add(f);
}
vectorList.add(vector);
}
// 构建字段数据
fields.add(new InsertParam.Field("text", textList));
fields.add(new InsertParam.Field("fid", fidListData));
fields.add(new InsertParam.Field("kid", kidList));
fields.add(new InsertParam.Field("docId", docIdList));
fields.add(new InsertParam.Field("vector", vectorList));
// 执行插入
InsertParam insertParam = InsertParam.newBuilder()
.withCollectionName(collectionName)
.withFields(fields)
.build();
R<MutationResult> insertResponse = milvusClient.insert(insertParam);
if (insertResponse.getStatus() != R.Status.Success.getCode()) {
log.error("Milvus向量存储失败: {}", insertResponse.getMessage());
throw new ServiceException("Milvus向量存储失败");
} else {
log.info("Milvus向量存储成功插入条数: {}", insertResponse.getData().getInsertCnt());
}
long endTime = System.currentTimeMillis();
log.info("Milvus向量存储完成消耗时间" + (endTime - startTime) / 1000 + "");
}
@Override
public List<String> getQueryVector(QueryVectorBo queryVectorBo) {
createSchema(queryVectorBo.getVectorModelName(), queryVectorBo.getKid(), queryVectorBo.getVectorModelName());
EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName(),
queryVectorBo.getApiKey(), queryVectorBo.getBaseUrl());
Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content();
String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + queryVectorBo.getKid();
List<String> resultList = new ArrayList<>();
// 加载集合到内存
LoadCollectionParam loadCollectionParam = LoadCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build();
milvusClient.loadCollection(loadCollectionParam);
// 准备查询向量
List<List<Float>> searchVectors = new ArrayList<>();
List<Float> queryVector = new ArrayList<>();
for (float f : queryEmbedding.vector()) {
queryVector.add(f);
}
searchVectors.add(queryVector);
// 构建搜索参数
SearchParam searchParam = SearchParam.newBuilder()
.withCollectionName(collectionName)
.withMetricType(MetricType.L2)
.withOutFields(Arrays.asList("text", "fid", "kid", "docId"))
.withTopK(queryVectorBo.getMaxResults())
.withVectors(searchVectors)
.withVectorFieldName("vector")
.withParams("{\"nprobe\":10}")
.build();
R<SearchResults> searchResponse = milvusClient.search(searchParam);
if (searchResponse.getStatus() != R.Status.Success.getCode()) {
log.error("Milvus查询失败: {}", searchResponse.getMessage());
return resultList;
}
SearchResultsWrapper wrapper = new SearchResultsWrapper(searchResponse.getData().getResults());
// 遍历搜索结果
for (int i = 0; i < wrapper.getIDScore(0).size(); i++) {
SearchResultsWrapper.IDScore idScore = wrapper.getIDScore(0).get(i);
// 获取text字段数据
List<?> textFieldData = wrapper.getFieldData("text", 0);
if (textFieldData != null && i < textFieldData.size()) {
Object textObj = textFieldData.get(i);
if (textObj != null) {
resultList.add(textObj.toString());
log.debug("找到相似文本ID: {}, 距离: {}, 内容: {}",
idScore.getLongID(), idScore.getScore(), textObj.toString());
}
}
}
return resultList;
}
@Override
@SneakyThrows
public void removeById(String id, String modelName) {
String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + id;
// 删除整个集合
DropCollectionParam dropCollectionParam = DropCollectionParam.newBuilder()
.withCollectionName(collectionName)
.build();
R<RpcStatus> dropResponse = milvusClient.dropCollection(dropCollectionParam);
if (dropResponse.getStatus() != R.Status.Success.getCode()) {
log.error("Milvus集合删除失败: {}", dropResponse.getMessage());
throw new ServiceException("Milvus集合删除失败");
} else {
log.info("Milvus集合删除成功: {}", collectionName);
}
}
@Override
public void removeByDocId(String docId, String kid) {
String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid;
String expr = "docId == \"" + docId + "\"";
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(expr)
.build();
R<MutationResult> deleteResponse = milvusClient.delete(deleteParam);
if (deleteResponse.getStatus() != R.Status.Success.getCode()) {
log.error("Milvus删除失败: {}", deleteResponse.getMessage());
throw new ServiceException("Milvus删除失败");
} else {
log.info("Milvus成功删除 docId={} 的所有向量数据,删除条数: {}", docId, deleteResponse.getData().getDeleteCnt());
}
}
@Override
public void removeByFid(String fid, String kid) {
String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid;
String expr = "fid == \"" + fid + "\"";
DeleteParam deleteParam = DeleteParam.newBuilder()
.withCollectionName(collectionName)
.withExpr(expr)
.build();
R<MutationResult> deleteResponse = milvusClient.delete(deleteParam);
if (deleteResponse.getStatus() != R.Status.Success.getCode()) {
log.error("Milvus删除失败: {}", deleteResponse.getMessage());
throw new ServiceException("Milvus删除失败");
} else {
log.info("Milvus成功删除 fid={} 的所有向量数据,删除条数: {}", fid, deleteResponse.getData().getDeleteCnt());
}
}
}

View File

@@ -0,0 +1,233 @@
package org.ruoyi.service.strategy.impl;
import cn.hutool.json.JSONObject;
import org.ruoyi.common.core.exception.ServiceException;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.model.embedding.EmbeddingModel;
import io.weaviate.client.Config;
import io.weaviate.client.WeaviateClient;
import io.weaviate.client.base.Result;
import io.weaviate.client.v1.batch.api.ObjectsBatchDeleter;
import io.weaviate.client.v1.batch.model.BatchDeleteResponse;
import io.weaviate.client.v1.filters.Operator;
import io.weaviate.client.v1.filters.WhereFilter;
import io.weaviate.client.v1.graphql.model.GraphQLResponse;
import io.weaviate.client.v1.schema.model.Property;
import io.weaviate.client.v1.schema.model.Schema;
import io.weaviate.client.v1.schema.model.WeaviateClass;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.config.VectorStoreProperties;
import org.ruoyi.domain.bo.QueryVectorBo;
import org.ruoyi.domain.bo.StoreEmbeddingBo;
import org.ruoyi.service.strategy.AbstractVectorStoreStrategy;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* Weaviate向量库策略实现
*
* @author ageer
*/
@Slf4j
@Component
public class WeaviateVectorStoreStrategy extends AbstractVectorStoreStrategy {
private WeaviateClient client;
public WeaviateVectorStoreStrategy(VectorStoreProperties vectorStoreProperties) {
super(vectorStoreProperties);
}
@Override
public String getVectorStoreType() {
return "weaviate";
}
@Override
public void createSchema(String vectorModelName, String kid, String modelName) {
String protocol = vectorStoreProperties.getWeaviate().getProtocol();
String host = vectorStoreProperties.getWeaviate().getHost();
String className = vectorStoreProperties.getWeaviate().getClassname() + kid;
// 创建 Weaviate 客户端
client = new WeaviateClient(new Config(protocol, host));
// 检查类是否存在,如果不存在就创建 schema
Result<Schema> schemaResult = client.schema().getter().run();
Schema schema = schemaResult.getResult();
boolean classExists = false;
for (WeaviateClass weaviateClass : schema.getClasses()) {
if (weaviateClass.getClassName().equals(className)) {
classExists = true;
break;
}
}
if (!classExists) {
// 类不存在,创建 schema
WeaviateClass build = WeaviateClass.builder()
.className(className)
.vectorizer("none")
.properties(
List.of(Property.builder().name("text").dataType(Collections.singletonList("text")).build(),
Property.builder().name("fid").dataType(Collections.singletonList("text")).build(),
Property.builder().name("kid").dataType(Collections.singletonList("text")).build(),
Property.builder().name("docId").dataType(Collections.singletonList("text")).build())
)
.build();
Result<Boolean> createResult = client.schema().classCreator().withClass(build).run();
if (createResult.hasErrors()) {
log.error("Schema 创建失败: {}", createResult.getError());
} else {
log.info("Schema 创建成功: {}", className);
}
}
}
@Override
public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) {
createSchema(storeEmbeddingBo.getVectorModelName(), storeEmbeddingBo.getKid(), storeEmbeddingBo.getVectorModelName());
EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName(),
storeEmbeddingBo.getApiKey(), storeEmbeddingBo.getBaseUrl());
List<String> chunkList = storeEmbeddingBo.getChunkList();
List<String> fidList = storeEmbeddingBo.getFids();
String kid = storeEmbeddingBo.getKid();
String docId = storeEmbeddingBo.getDocId();
log.info("向量存储条数记录: " + chunkList.size());
long startTime = System.currentTimeMillis();
for (int i = 0; i < chunkList.size(); i++) {
String text = chunkList.get(i);
String fid = fidList.get(i);
Embedding embedding = embeddingModel.embed(text).content();
Map<String, Object> properties = Map.of(
"text", text,
"fid", fid,
"kid", kid,
"docId", docId
);
Float[] vector = toObjectArray(embedding.vector());
client.data().creator()
.withClassName("LocalKnowledge" + kid)
.withProperties(properties)
.withVector(vector)
.run();
}
long endTime = System.currentTimeMillis();
log.info("向量存储完成消耗时间:" + (endTime - startTime) / 1000 + "");
}
@Override
public List<String> getQueryVector(QueryVectorBo queryVectorBo) {
createSchema(queryVectorBo.getVectorModelName(), queryVectorBo.getKid(), queryVectorBo.getVectorModelName());
EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName(),
queryVectorBo.getApiKey(), queryVectorBo.getBaseUrl());
Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content();
float[] vector = queryEmbedding.vector();
List<String> vectorStrings = new ArrayList<>();
for (float v : vector) {
vectorStrings.add(String.valueOf(v));
}
String vectorStr = String.join(",", vectorStrings);
String className = vectorStoreProperties.getWeaviate().getClassname();
// 构建 GraphQL 查询
String graphQLQuery = String.format(
"{\n" +
" Get {\n" +
" %s(nearVector: {vector: [%s]} limit: %d) {\n" +
" text\n" +
" fid\n" +
" kid\n" +
" docId\n" +
" _additional {\n" +
" distance\n" +
" id\n" +
" }\n" +
" }\n" +
" }\n" +
"}",
className + queryVectorBo.getKid(),
vectorStr,
queryVectorBo.getMaxResults()
);
Result<GraphQLResponse> result = client.graphQL().raw().withQuery(graphQLQuery).run();
List<String> resultList = new ArrayList<>();
if (result != null && !result.hasErrors()) {
Object data = result.getResult().getData();
JSONObject entries = new JSONObject(data);
Map<String, cn.hutool.json.JSONArray> entriesMap = entries.get("Get", Map.class);
cn.hutool.json.JSONArray objects = entriesMap.get(className + queryVectorBo.getKid());
if (objects.isEmpty()) {
return resultList;
}
for (Object object : objects) {
Map<String, String> map = (Map<String, String>) object;
String content = map.get("text");
resultList.add(content);
}
return resultList;
} else {
log.error("GraphQL 查询失败: {}", result.getError());
return resultList;
}
}
@Override
@SneakyThrows
public void removeById(String id, String modelName) {
String protocol = vectorStoreProperties.getWeaviate().getProtocol();
String host = vectorStoreProperties.getWeaviate().getHost();
String className = vectorStoreProperties.getWeaviate().getClassname();
String finalClassName = className + id;
WeaviateClient client = new WeaviateClient(new Config(protocol, host));
Result<Boolean> result = client.schema().classDeleter().withClassName(finalClassName).run();
if (result.hasErrors()) {
log.error("失败删除向量: " + result.getError());
throw new ServiceException("失败删除向量数据!");
} else {
log.info("成功删除向量数据: " + result.getResult());
}
}
@Override
public void removeByDocId(String docId, String kid) {
String className = vectorStoreProperties.getWeaviate().getClassname() + kid;
// 构建 Where 条件
WhereFilter whereFilter = WhereFilter.builder()
.path("docId")
.operator(Operator.Equal)
.valueText(docId)
.build();
ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter();
Result<BatchDeleteResponse> result = deleter.withClassName(className)
.withWhere(whereFilter)
.run();
if (result != null && !result.hasErrors()) {
log.info("成功删除 docId={} 的所有向量数据", docId);
} else {
log.error("删除失败: {}", result.getError());
}
}
@Override
public void removeByFid(String fid, String kid) {
String className = vectorStoreProperties.getWeaviate().getClassname() + kid;
// 构建 Where 条件
WhereFilter whereFilter = WhereFilter.builder()
.path("fid")
.operator(Operator.Equal)
.valueText(fid)
.build();
ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter();
Result<BatchDeleteResponse> result = deleter.withClassName(className)
.withWhere(whereFilter)
.run();
if (result != null && !result.hasErrors()) {
log.info("成功删除 fid={} 的所有向量数据", fid);
} else {
log.error("删除失败: {}", result.getError());
}
}
}

View File

@@ -45,6 +45,18 @@ public class ChatMessageController extends BaseController {
return chatMessageService.queryPageList(bo, pageQuery);
}
/**
* 根据会话ID查询聊天消息列表
*/
@GetMapping("/listBySession/{sessionId}")
public TableDataInfo<ChatMessageVo> listBySession(@NotNull(message = "会话ID不能为空")
@PathVariable Long sessionId,
PageQuery pageQuery) {
ChatMessageBo bo = new ChatMessageBo();
bo.setSessionId(sessionId);
return chatMessageService.queryPageList(bo, pageQuery);
}
/**
* 导出聊天消息列表
*/

View File

@@ -1,21 +1,39 @@
package org.ruoyi.chat.service.chat.impl;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okhttp3.Response;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.chat.support.ChatServiceHelper;
import org.ruoyi.common.chat.entity.chat.Message;
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.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* deepseek
*/
@@ -26,9 +44,24 @@ public class DeepSeekChatImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
// 创建一个用于直接API调用的OkHttpClient
private final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
@Override
public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
// 检查是否启用深度思考且是deepseek模型
if (Boolean.TRUE.equals(chatRequest.getEnableThinking())) {
return handleDeepSeekWithThinking(chatRequest, emitter, chatModelVo);
}
StreamingChatModel chatModel = OpenAiStreamingChatModel.builder()
.baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey())
@@ -39,7 +72,31 @@ public class DeepSeekChatImpl implements IChatService {
.build();
// 发送流式消息
try {
chatModel.chat(chatRequest.getPrompt(), new StreamingChatResponseHandler() {
// 构建消息列表,包含历史对话消息和当前用户消息
List<ChatMessage> messages = new ArrayList<>();
// 添加历史对话消息
if (chatRequest.getMessages() != null) {
for (Message message : chatRequest.getMessages()) {
// 检查消息内容是否有效
if (message.getContent() == null || String.valueOf(message.getContent()).trim().isEmpty()) {
continue; // 跳过空消息
}
if (Message.Role.SYSTEM.getName().equals(message.getRole())) {
messages.add(new SystemMessage(String.valueOf(message.getContent())));
} else if (Message.Role.USER.getName().equals(message.getRole())) {
messages.add(new UserMessage(String.valueOf(message.getContent())));
} else if (Message.Role.ASSISTANT.getName().equals(message.getRole())) {
messages.add(new dev.langchain4j.data.message.AiMessage(String.valueOf(message.getContent())));
}
}
}
// 添加当前用户消息
messages.add(new UserMessage(chatRequest.getPrompt()));
chatModel.chat(messages, new StreamingChatResponseHandler() {
@SneakyThrows
@Override
public void onPartialResponse(String partialResponse) {
@@ -70,8 +127,208 @@ public class DeepSeekChatImpl implements IChatService {
return emitter;
}
/**
* 处理启用深度思考的deepseek模型请求
*/
private SseEmitter handleDeepSeekWithThinking(ChatRequest chatRequest, SseEmitter emitter, ChatModelVo chatModelVo) {
try {
// 构建请求到外部API
String url = chatModelVo.getApiHost() + "/v1/chat/completions";
String apiKey = chatModelVo.getApiKey();
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", chatModelVo.getModelName());
requestBody.put("response_format", Map.of("type", "text"));
requestBody.put("max_tokens", 4000); // 修复将max_tokens从81920改为4000符合API要求
requestBody.put("temperature", 1);
requestBody.put("top_p", 1);
requestBody.put("top_k", 50);
requestBody.put("enable_thinking", chatRequest.getEnableThinking());
requestBody.put("stream", chatRequest.getStream());
// 构建消息 - DeepSeek模型不需要系统提示词
List<Map<String, Object>> messages = new ArrayList<>();
// 添加历史对话消息 (只添加用户和助手消息)
if (chatRequest.getMessages() != null) {
for (Message message : chatRequest.getMessages()) {
// 检查消息内容是否有效
if (message.getContent() == null || String.valueOf(message.getContent()).trim().isEmpty()) {
continue; // 跳过空消息
}
// DeepSeek模型在深度思考模式下只接受user和assistant角色的消息
if (Message.Role.SYSTEM.getName().equals(message.getRole())) {
// 跳过系统消息
continue;
}
Map<String, Object> historyMessage = new HashMap<>();
historyMessage.put("role", message.getRole());
historyMessage.put("content", String.valueOf(message.getContent()));
messages.add(historyMessage);
}
}
// 添加当前用户消息
Map<String, Object> userMessage = new HashMap<>();
userMessage.put("role", "user");
userMessage.put("content", String.valueOf(chatRequest.getPrompt()));
messages.add(userMessage);
requestBody.put("messages", messages);
// 创建ObjectMapper实例
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
String requestBodyStr = objectMapper.writeValueAsString(requestBody);
// 打印请求体用于调试
log.info("打印请求体: {}", requestBodyStr);
// 创建请求
Request request = new Request.Builder()
.url(url)
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.post(RequestBody.create(requestBodyStr, JSON))
.build();
// 执行异步请求
this.client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
try {
log.error("深度思考请求失败: {}", e.getMessage(), e);
emitter.send("深度思考请求失败: " + e.getMessage());
emitter.complete();
} catch (IOException ioException) {
log.error("发送错误消息失败: {}", ioException.getMessage(), ioException);
}
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) {
// 打印完整的错误响应体
String errorBody = "";
if (response.body() != null) {
errorBody = response.body().string();
}
log.error("深度思考请求失败,状态码: {},响应体: {}", response.code(), errorBody);
try {
emitter.send("深度思考请求失败,状态码: " + response.code() + ",响应体: " + errorBody);
emitter.complete();
return;
} catch (IOException e) {
log.error("发送错误消息失败: {}", e.getMessage(), e);
return;
}
}
try (ResponseBody responseBody = response.body()) {
if (responseBody == null) {
log.error("响应体为空");
emitter.send("响应体为空");
emitter.complete();
return;
}
// 流式读取响应
processThinkingResponse(responseBody, emitter);
} catch (Exception e) {
log.error("处理响应时出错: {}", e.getMessage(), e);
try {
emitter.send("处理响应时出错: " + e.getMessage());
emitter.complete();
} catch (IOException ioException) {
log.error("发送错误消息失败: {}", ioException.getMessage(), ioException);
}
}
}
});
} catch (Exception e) {
log.error("处理深度思考请求时出错: {}", e.getMessage(), e);
ChatServiceHelper.onStreamError(emitter, e.getMessage());
}
return emitter;
}
/**
* 处理深度思考的流式响应(边解析边推送)
*/
private void processThinkingResponse(ResponseBody responseBody, SseEmitter emitter) throws IOException {
// 标记是否进入正式回答阶段
boolean thinkingComplete = false;
try (BufferedReader reader = new BufferedReader(responseBody.charStream())) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.startsWith("data: ")) {
continue;
}
String jsonData = line.substring(6).trim();
if ("[DONE]".equals(jsonData)) {
break;
}
try {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> chunk = mapper.readValue(jsonData, Map.class);
if (chunk.containsKey("choices") && chunk.get("choices") instanceof List) {
List<Map<String, Object>> choices = (List<Map<String, Object>>) chunk.get("choices");
if (!choices.isEmpty()) {
Map<String, Object> choice = choices.get(0);
if (choice.containsKey("delta") && choice.get("delta") instanceof Map) {
Map<String, Object> delta = (Map<String, Object>) choice.get("delta");
// 推送思考过程
if (delta.containsKey("reasoning_content") && delta.get("reasoning_content") != null) {
String reasoningChunk = delta.get("reasoning_content").toString();
emitter.send(SseEmitter.event().data(reasoningChunk).name("thinking"));
log.debug("Reasoning Chunk: {}", reasoningChunk);
}
// 推送正式回答
if (delta.containsKey("content") && delta.get("content") != null) {
String content = delta.get("content").toString();
// 第一次进入回答阶段时,加个提示头
if (!thinkingComplete) {
emitter.send(SseEmitter.event().data("\n\n回答内容\n").name("answer-header"));
thinkingComplete = true;
}
emitter.send(SseEmitter.event().data(content).name("answer"));
log.debug("Answer Chunk:{}", content);
}
}
}
}
} catch (Exception e) {
log.warn("解析JSON数据失败忽略本行: {}", jsonData, e);
}
}
emitter.complete();
log.info("深度思考流式响应完成");
} catch (IOException e) {
log.error("读取响应流时出错: {}", e.getMessage(), e);
try {
emitter.send(SseEmitter.event().data("读取响应流时出错: " + e.getMessage()).name("error"));
emitter.complete();
} catch (IOException ioException) {
log.error("发送错误消息失败: {}", ioException.getMessage(), ioException);
}
}
}
@Override
public String getCategory() {
return ChatModeType.DEEPSEEK.getCode();
}
}
}

View File

@@ -100,24 +100,7 @@ public class SseServiceImpl implements ISseService {
// 设置用户id
chatRequest.setUserId(LoginHelper.getUserId());
//待优化的地方 这里请前端提交send的时候传递uuid进来或者sessionId
//待优化的地方 这里请前端提交send的时候传递uuid进来或者sessionId
//待优化的地方 这里请前端提交send的时候传递uuid进来或者sessionId
{
// 设置会话id
if (chatRequest.getUuid() == null) {
//暂时随机生成会话id
chatRequest.setSessionId(System.currentTimeMillis());
} else {
//这里或许需要修改一下这里应该用uuid 或者 前端传递 sessionId
chatRequest.setSessionId(chatRequest.getUuid());
}
}
chatRequest.setUserId(chatCostService.getUserId());
// 设置会话id
if (chatRequest.getSessionId() == null) {
ChatSessionBo chatSessionBo = new ChatSessionBo();
chatSessionBo.setUserId(chatCostService.getUserId());

View File

@@ -91,45 +91,66 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
public TableDataInfo<KnowledgeInfoVo> queryPageListByRole(KnowledgeInfoBo bo, PageQuery pageQuery) {
// 查询用户关联角色
LoginUser loginUser = LoginHelper.getLoginUser();
if (StringUtils.isEmpty(loginUser.getKroleGroupIds()) || StringUtils.isEmpty(loginUser.getKroleGroupType())) {
return new TableDataInfo<>();
}
// 角色/角色组id列表
List<String> groupIdList = Arrays.stream(loginUser.getKroleGroupIds().split(","))
.filter(StringUtils::isNotEmpty)
.toList();
List<KnowledgeRole> knowledgeRoles;
LambdaQueryWrapper<KnowledgeRole> roleLqw = Wrappers.lambdaQuery();
if ("role".equals(loginUser.getKroleGroupType())) {
roleLqw.in(KnowledgeRole::getId, groupIdList);
} else {
roleLqw.in(KnowledgeRole::getGroupId, groupIdList);
}
knowledgeRoles = knowledgeRoleMapper.selectList(roleLqw);
if (CollectionUtils.isEmpty(knowledgeRoles)) {
return new TableDataInfo<>();
}
// 查询知识库id列表
LambdaQueryWrapper<KnowledgeRoleRelation> relationLqw = Wrappers.lambdaQuery();
relationLqw.in(KnowledgeRoleRelation::getKnowledgeRoleId, knowledgeRoles.stream().map(KnowledgeRole::getId).filter(Objects::nonNull).collect(Collectors.toList()));
List<KnowledgeRoleRelation> knowledgeRoleRelations = knowledgeRoleRelationMapper.selectList(relationLqw);
if (CollectionUtils.isEmpty(knowledgeRoleRelations)) {
return new TableDataInfo<>();
}
// 构建查询条件
LambdaQueryWrapper<KnowledgeInfo> lqw = buildQueryWrapper(bo);
// 在查询用户创建的知识库条件下,拼接角色分配知识库
lqw.or(q -> q.in(
KnowledgeInfo::getId,
knowledgeRoleRelations.stream()
.map(KnowledgeRoleRelation::getKnowledgeId)
.filter(Objects::nonNull)
.collect(Collectors.toList())
));
// 管理员用户直接查询所有数据
if (Objects.equals(loginUser.getUserId(), 1L)) {
Page<KnowledgeInfoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
// 检查用户是否配置了角色信息
if (StringUtils.isNotEmpty(loginUser.getKroleGroupIds()) && StringUtils.isNotEmpty(loginUser.getKroleGroupType())) {
// 角色/角色组id列表
List<String> groupIdList = Arrays.stream(loginUser.getKroleGroupIds().split(","))
.filter(StringUtils::isNotEmpty)
.toList();
// 查询用户关联的角色
List<KnowledgeRole> knowledgeRoles = new ArrayList<>();
LambdaQueryWrapper<KnowledgeRole> roleLqw = Wrappers.lambdaQuery();
if ("role".equals(loginUser.getKroleGroupType())) {
roleLqw.in(KnowledgeRole::getId, groupIdList);
} else {
roleLqw.in(KnowledgeRole::getGroupId, groupIdList);
}
knowledgeRoles = knowledgeRoleMapper.selectList(roleLqw);
// 如果用户有关联角色
if (!CollectionUtils.isEmpty(knowledgeRoles)) {
// 查询这些角色关联的知识库
LambdaQueryWrapper<KnowledgeRoleRelation> relationLqw = Wrappers.lambdaQuery();
relationLqw.in(KnowledgeRoleRelation::getKnowledgeRoleId,
knowledgeRoles.stream().map(KnowledgeRole::getId).filter(Objects::nonNull).collect(Collectors.toList()));
List<KnowledgeRoleRelation> knowledgeRoleRelations = knowledgeRoleRelationMapper.selectList(relationLqw);
// 如果角色关联了知识库
if (!CollectionUtils.isEmpty(knowledgeRoleRelations)) {
// 查询用户自己的知识库和角色分配的知识库
lqw.and(q -> q.eq(KnowledgeInfo::getUid, loginUser.getUserId())
.or()
.in(KnowledgeInfo::getId,
knowledgeRoleRelations.stream()
.map(KnowledgeRoleRelation::getKnowledgeId)
.filter(Objects::nonNull)
.collect(Collectors.toList())
)
);
} else {
// 用户没有关联任何知识库,只显示自己的
lqw.eq(KnowledgeInfo::getUid, loginUser.getUserId());
}
} else {
// 用户没有关联角色,只显示自己的
lqw.eq(KnowledgeInfo::getUid, loginUser.getUserId());
}
} else {
// 用户没有配置角色信息,只显示自己的
lqw.eq(KnowledgeInfo::getUid, loginUser.getUserId());
}
Page<KnowledgeInfoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
@@ -216,7 +237,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
}
baseMapper.insert(knowledgeInfo);
if (knowledgeInfo != null) {
vectorStoreService.createSchema(String.valueOf(knowledgeInfo.getId()),
vectorStoreService.createSchema(knowledgeInfo.getVectorModelName(),String.valueOf(knowledgeInfo.getId()),
bo.getVectorModelName());
}
} else {
@@ -258,6 +279,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
knowledgeAttach.setDocType(fileName.substring(fileName.lastIndexOf(".") + 1));
String content = "";
ResourceLoader resourceLoader = resourceLoaderFactory.getLoaderByFileType(knowledgeAttach.getDocType());
// 文档分段入库
List<String> fids = new ArrayList<>();
try {
content = resourceLoader.getContent(file.getInputStream());
@@ -265,6 +287,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
List<KnowledgeFragment> knowledgeFragmentList = new ArrayList<>();
if (CollUtil.isNotEmpty(chunkList)) {
for (int i = 0; i < chunkList.size(); i++) {
// 生成知识片段ID
String fid = RandomUtil.randomString(10);
fids.add(fid);
KnowledgeFragment knowledgeFragment = new KnowledgeFragment();

View File

@@ -0,0 +1,20 @@
package org.ruoyi.mcp.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Autowired
private DynamicMcpToolCallbackProvider dynamicMcpToolCallbackProvider;
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultTools(java.util.List.of(dynamicMcpToolCallbackProvider.createToolCallbackProvider().getToolCallbacks()))
.build();
}
}

View File

@@ -0,0 +1,97 @@
package org.ruoyi.mcp.config;
import org.ruoyi.mcp.service.McpInfoService;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* 动态MCP工具回调提供者
*
* 这个类有大问题 ,没有测试!!!!!!!
*/
@Component
public class DynamicMcpToolCallbackProvider {
@Autowired
private McpInfoService mcpInfoService;
@Autowired
private McpProcessManager mcpProcessManager;
@Autowired
private McpToolInvoker mcpToolInvoker;
/**
* 创建工具回调提供者
*/
public ToolCallbackProvider createToolCallbackProvider() {
List<FunctionCallback> callbacks = new ArrayList<>();
List<String> activeServerNames = mcpInfoService.getActiveServerNames();
for (String serverName : activeServerNames) {
FunctionCallback callback = createMcpToolCallback(serverName);
callbacks.add(callback);
}
return ToolCallbackProvider.from(callbacks);
}
private FunctionCallback createMcpToolCallback(String serverName) {
return new ToolCallback() {
@Override
public ToolDefinition getToolDefinition() {
// 获取工具配置
McpServerConfig config = mcpInfoService.getToolConfigByName(serverName);
if (config == null) {
// 返回一个默认的ToolDefinition
return ToolDefinition.builder()
.name(serverName)
.description("MCP tool for " + serverName)
.build();
}
// 根据config创建ToolDefinition
return ToolDefinition.builder()
.name(serverName)
.description(config.getDescription()) // 假设McpServerConfig有getDescription方法
.build();
}
@Override
public String call(String toolInput) {
try {
// 获取工具配置
McpServerConfig config = mcpInfoService.getToolConfigByName(serverName);
if (config == null) {
return "{\"error\": \"MCP tool not found: " + serverName + "\", \"serverName\": \"" + serverName + "\"}";
}
// 确保 MCP 服务器正在运行
ensureMcpServerRunning(serverName, config);
// 调用 MCP 工具
Object result = mcpToolInvoker.invokeTool(serverName, toolInput);
return "{\"result\": \"" + result.toString() + "\", \"serverName\": \"" + serverName + "\"}";
} catch (Exception e) {
return "{\"error\": \"MCP tool execution failed: " + e.getMessage() + "\", \"serverName\": \"" + serverName + "\"}";
}
}
};
}
private void ensureMcpServerRunning(String serverName, McpServerConfig config) {
if (!mcpProcessManager.isMcpServerRunning(serverName)) {
boolean started = mcpProcessManager.startMcpServer(
serverName,
config
);
if (!started) {
throw new RuntimeException("Failed to start MCP server: " + serverName);
}
}
}
}

View File

@@ -0,0 +1,20 @@
package org.ruoyi.mcp.config;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import java.util.List;
public class McpConfig {
@JsonProperty("mcpServers")
private Map<String, McpServerConfig> mcpServers;
// getters and setters
public Map<String, McpServerConfig> getMcpServers() {
return mcpServers;
}
public void setMcpServers(Map<String, McpServerConfig> mcpServers) {
this.mcpServers = mcpServers;
}
}

View File

@@ -0,0 +1,341 @@
package org.ruoyi.mcp.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.mcp.service.McpInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
@Slf4j
@Component
public class McpProcessManager {
private final Map<String, Process> runningProcesses = new ConcurrentHashMap<>();
private final Map<String, McpServerProcess> mcpServerProcesses = new ConcurrentHashMap<>();
private final ExecutorService executorService = Executors.newCachedThreadPool();
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, BufferedWriter> processWriters = new ConcurrentHashMap<>();
private final Map<String, BufferedReader> processReaders = new ConcurrentHashMap<>();
@Autowired
private McpInfoService mcpInfoService;
/**
* 启动 MCP 服务器进程(支持环境变量)
*/
public boolean startMcpServer(String serverName, McpServerConfig serverConfig) {
try {
log.info("启动MCP服务器进程: {}", serverName);
ProcessBuilder processBuilder = new ProcessBuilder();
// 构建命令
List<String> commandList = buildCommandListWithFullPaths(serverConfig.getCommand(), serverConfig.getArgs());
processBuilder.command(commandList);
// 设置工作目录
if (serverConfig.getWorkingDirectory() != null) {
processBuilder.directory(new File(serverConfig.getWorkingDirectory()));
} else {
processBuilder.directory(new File(System.getProperty("user.dir")));
}
// 设置环境变量
if (serverConfig.getEnv() != null) {
processBuilder.environment().putAll(serverConfig.getEnv());
}
// ===== 关键:在 start 之前打印完整的调试信息 =====
System.out.println("=== ProcessBuilder 调试信息 ===");
System.out.println("完整命令列表: " + commandList);
System.out.println("命令字符串: " + String.join(" ", commandList));
System.out.println("工作目录: " + processBuilder.directory());
System.out.println("================================");
//https://www.modelscope.cn/mcp/servers/@worryzyy/howtocook-mcp
// 启动进程
Process process = processBuilder.start();
// 获取输入输出流
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
processWriters.put(serverName, writer);
processReaders.put(serverName, reader);
// 存储进程引用
McpServerProcess serverProcess = new McpServerProcess(serverName, process, serverConfig);
mcpServerProcesses.put(serverName, serverProcess);
// 启动日志读取线程
executorService.submit(() -> readProcessOutput(serverName, process));
// 启动 MCP 通信监听线程
executorService.submit(() -> listenMcpMessages(serverName, reader));
// 更新服务器状态
mcpInfoService.enableTool(serverName);
boolean isAlive = process.isAlive();
if (isAlive) {
log.info("成功启动MCP服务器: {} 命令: {}", serverName, commandList);
} else {
System.err.println("✗ MCP server [" + serverName + "] failed to start");
// 读取错误输出
readErrorOutput(process);
}
return true;
} catch (IOException e) {
log.error("启动MCP服务器进程失败: " + serverName, e);
// 更新服务器状态为禁用
//mcpInfoService.disableTool(serverName);
throw new RuntimeException("Failed to start MCP server process: " + e.getMessage(), e);
}
}
/**
* 发送 MCP 消息
*/
public boolean sendMcpMessage(String serverName, Map<String, Object> message) {
try {
BufferedWriter writer = processWriters.get(serverName);
if (writer == null) {
System.err.println("未找到服务器 [" + serverName + "] 的输出流");
return false;
}
String jsonMessage = objectMapper.writeValueAsString(message);
System.out.println("发送消息到 [" + serverName + "]: " + jsonMessage);
writer.write(jsonMessage);
writer.newLine();
writer.flush();
return true;
} catch (Exception e) {
System.err.println("发送消息到 [" + serverName + "] 失败: " + e.getMessage());
return false;
}
}
/**
* 监听 MCP 消息
*/
private void listenMcpMessages(String serverName, BufferedReader reader) {
try {
String line;
while ((line = reader.readLine()) != null) {
try {
// 解析收到的 JSON 消息
Map<String, Object> message = objectMapper.readValue(line, Map.class);
System.out.println("收到来自 [" + serverName + "] 的消息: " + message);
// 处理不同类型的 MCP 消息
handleMessage(serverName, message);
} catch (Exception e) {
System.err.println("解析消息失败: " + line + ", 错误: " + e.getMessage());
// 如果不是 JSON当作普通日志输出
System.out.println("[" + serverName + "] 日志: " + line);
}
}
} catch (IOException e) {
if (isMcpServerRunning(serverName)) {
System.err.println("监听 [" + serverName + "] 消息时出错: " + e.getMessage());
}
}
}
/**
* 处理 MCP 消息(更新版本)
*/
private void handleMessage(String serverName, Map<String, Object> message) {
String type = (String) message.get("type");
if (type == null) return;
switch (type) {
case "ready":
System.out.println("MCP 服务器 [" + serverName + "] 准备就绪");
break;
case "response":
System.out.println("MCP 服务器 [" + serverName + "] 响应: " + message.get("data"));
break;
case "error":
System.err.println("MCP 服务器 [" + serverName + "] 错误: " + message.get("message"));
break;
default:
System.out.println("MCP 服务器 [" + serverName + "] 未知消息类型: " + type);
break;
}
}
/**
* 构建命令列表
*/
private List<String> buildCommandListWithFullPaths(String command, List<String> args) {
List<String> commandList = new ArrayList<>();
if (isWindows() && "npx".equalsIgnoreCase(command)) {
// 在 Windows 上使用 cmd.exe 包装以确保兼容性
commandList.add("cmd.exe");
commandList.add("/c");
commandList.add("npx");
commandList.addAll(args);
} else {
commandList.add(command);
commandList.addAll(args);
}
return commandList;
}
/**
* 检查是否为 Windows 系统
*/
private boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
/**
* 读取错误输出
*/
private void readErrorOutput(Process process) {
try {
InputStream errorStream = process.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream));
String line;
while ((line = reader.readLine()) != null) {
System.err.println("ERROR: " + line);
}
} catch (Exception e) {
System.err.println("Failed to read error output: " + e.getMessage());
}
}
/**
* 停止 MCP 服务器进程
*/
public boolean stopMcpServer(String serverName) {
Process process = runningProcesses.remove(serverName);
BufferedWriter writer = processWriters.remove(serverName);
BufferedReader reader = processReaders.remove(serverName);
try {
if (writer != null) {
writer.close();
}
if (reader != null) {
reader.close();
}
} catch (IOException e) {
System.err.println("关闭流时出错: " + e.getMessage());
}
// 更新服务器状态为禁用
mcpInfoService.disableTool(serverName);
if (process != null && process.isAlive()) {
process.destroy();
try {
if (!process.waitFor(5, TimeUnit.SECONDS)) {
process.destroyForcibly();
process.waitFor(1, TimeUnit.SECONDS);
}
System.out.println("MCP server [" + serverName + "] stopped");
return true;
} catch (InterruptedException e) {
process.destroyForcibly();
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
/**
* 重启 MCP 服务器进程
*/
public boolean restartMcpServer(String serverName, String command, List<String> args, Map<String, String> env) {
stopMcpServer(serverName);
McpServerConfig mcpServerConfig = new McpServerConfig();
mcpServerConfig.setCommand(command);
mcpServerConfig.setArgs(args);
mcpServerConfig.setEnv(env);
return startMcpServer(serverName, mcpServerConfig);
}
/**
* 检查 MCP 服务器是否运行
*/
public boolean isMcpServerRunning(String serverName) {
Process process = runningProcesses.get(serverName);
return process != null && process.isAlive();
}
/**
* 获取所有运行中的 MCP 服务器
*/
public Set<String> getRunningMcpServers() {
Set<String> running = new HashSet<>();
for (Map.Entry<String, Process> entry : runningProcesses.entrySet()) {
if (entry.getValue().isAlive()) {
running.add(entry.getKey());
}
}
return running;
}
/**
* 获取进程信息
*/
public McpServerProcess getProcessInfo(String serverName) {
return mcpServerProcesses.get(serverName);
}
private void readProcessOutput(String serverName, Process process) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null && process.isAlive()) {
System.out.println("[" + serverName + "] " + line);
}
} catch (IOException e) {
System.err.println("Error reading output from " + serverName + ": " + e.getMessage());
}
}
private String getProcessId(Process process) {
try {
// Java 9+ 可以直接获取 PID
return String.valueOf(process.pid());
} catch (Exception e) {
// Java 8 兼容处理
return "unknown";
}
}
/**
* MCP服务器进程信息
*/
public static class McpServerProcess {
private final String name;
private final Process process;
private final McpServerConfig config;
private final LocalDateTime startTime;
public McpServerProcess(String name, Process process, McpServerConfig config) {
this.name = name;
this.process = process;
this.config = config;
this.startTime = LocalDateTime.now();
}
// Getters
public String getName() { return name; }
public Process getProcess() { return process; }
public McpServerConfig getConfig() { return config; }
public LocalDateTime getStartTime() { return startTime; }
}
}

View File

@@ -0,0 +1,287 @@
package org.ruoyi.mcp.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.ProcessInfo;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.Disposable;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Component
public class McpProcessSSEManager {
private final Map<String, Process> runningProcesses = new ConcurrentHashMap<>();
private final Map<String, ProcessInfo> processInfos = new ConcurrentHashMap<>();
private final Map<String, WebClient> sseClients = new ConcurrentHashMap<>();
private final Map<String, Disposable> sseSubscriptions = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private McpSSEToolInvoker mcpToolInvoker;
/**
* 启动 MCP 服务器进程SSE 模式)
*/
public boolean startMcpServer(String serverName, String command, List<String> args, Map<String, String> env) {
try {
System.out.println("准备启动 MCP 服务器 (SSE 模式): " + serverName);
// 如果已经运行,先停止
if (isMcpServerRunning(serverName)) {
stopMcpServer(serverName);
}
// 构建命令
List<String> commandList = buildCommandList(command, args);
// 创建 ProcessBuilder
ProcessBuilder processBuilder = new ProcessBuilder(commandList);
processBuilder.redirectErrorStream(true);
// 设置工作目录
String workingDir = System.getProperty("user.dir");
processBuilder.directory(new File(workingDir));
// 打印调试信息
System.out.println("=== ProcessBuilder 调试信息 ===");
System.out.println("完整命令列表: " + commandList);
System.out.println("================================");
// 执行命令
Process process = processBuilder.start();
runningProcesses.put(serverName, process);
ProcessInfo processInfo = new ProcessInfo();
processInfo.setStartTime(System.currentTimeMillis());
processInfo.setPid(getProcessId(process));
processInfos.put(serverName, processInfo);
// 启动日志读取线程
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(() -> readProcessOutput(serverName, process));
// 等待进程启动
Thread.sleep(3000);
boolean isAlive = process.isAlive();
if (isAlive) {
System.out.println("✓ MCP 服务器 [" + serverName + "] 启动成功");
// 初始化 SSE 连接
initializeSseConnection(serverName);
} else {
System.err.println("✗ MCP 服务器 [" + serverName + "] 启动失败");
readErrorOutput(process);
}
return isAlive;
} catch (Exception e) {
System.err.println("✗ 启动 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
private String getProcessId(Process process) {
try {
return String.valueOf(process.pid());
} catch (Exception e) {
return "unknown";
}
}
private void readProcessOutput(String serverName, Process process) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null && process.isAlive()) {
System.out.println("[" + serverName + "] " + line);
}
} catch (IOException e) {
System.err.println("Error reading output from " + serverName + ": " + e.getMessage());
}
}
/**
* 读取错误输出
*/
private void readErrorOutput(Process process) {
try {
InputStream errorStream = process.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream));
String line;
while ((line = reader.readLine()) != null) {
System.err.println("ERROR: " + line);
}
} catch (Exception e) {
System.err.println("Failed to read error output: " + e.getMessage());
}
}
/**
* 初始化 SSE 连接
*/
private void initializeSseConnection(String serverName) {
try {
// 创建 WebClient 用于 SSE 连接
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:3000") // 假设默认端口 3000
.build();
sseClients.put(serverName, webClient);
// 建立 SSE 连接
String sseUrl = "/sse/" + serverName; // SSE 端点
Disposable subscription = webClient.get()
.uri(sseUrl)
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class)
.subscribe(
event -> handleSseEvent(serverName, event),
error -> System.err.println("SSE 连接错误 [" + serverName + "]: " + error.getMessage()),
() -> System.out.println("SSE 连接完成 [" + serverName + "]")
);
sseSubscriptions.put(serverName, subscription);
System.out.println("✓ SSE 连接建立成功 [" + serverName + "]");
} catch (Exception e) {
System.err.println("✗ 建立 SSE 连接失败 [" + serverName + "]: " + e.getMessage());
}
}
/**
* 处理 SSE 事件
*/
private void handleSseEvent(String serverName, String event) {
try {
System.out.println("收到来自 [" + serverName + "] 的 SSE 事件: " + event);
// 解析 SSE 事件
if (event.startsWith("data: ")) {
String jsonData = event.substring(6); // 移除 "data: " 前缀
Map<String, Object> message = objectMapper.readValue(jsonData, Map.class);
// 处理不同类型的事件
String type = (String) message.get("type");
if ("tool_response".equals(type)) {
mcpToolInvoker.handleSseResponse(serverName, message);
} else if ("tool_error".equals(type)) {
mcpToolInvoker.handleSseError(serverName, message);
} else if ("progress".equals(type)) {
handleProgressEvent(serverName, message);
} else {
System.out.println("[" + serverName + "] 未知事件类型: " + type);
}
}
} catch (Exception e) {
System.err.println("处理 SSE 事件失败 [" + serverName + "]: " + e.getMessage());
}
}
/**
* 处理进度事件
*/
private void handleProgressEvent(String serverName, Map<String, Object> message) {
Object progress = message.get("progress");
Object messageText = message.get("message");
System.out.println("[" + serverName + "] 进度: " + progress + " - " + messageText);
}
/**
* 构建命令列表
*/
private List<String> buildCommandList(String command, List<String> args) {
List<String> commandList = new ArrayList<>();
if (isWindows() && "npx".equalsIgnoreCase(command)) {
commandList.add("cmd.exe");
commandList.add("/c");
commandList.add("npx");
commandList.addAll(args);
} else {
commandList.add(command);
commandList.addAll(args);
}
return commandList;
}
/**
* 检查是否为 Windows 系统
*/
private boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
/**
* 停止 MCP 服务器进程
*/
public boolean stopMcpServer(String serverName) {
// 停止 SSE 连接
Disposable subscription = sseSubscriptions.remove(serverName);
if (subscription != null && !subscription.isDisposed()) {
subscription.dispose();
}
sseClients.remove(serverName);
// 停止进程
Process process = runningProcesses.remove(serverName);
ProcessInfo processInfo = processInfos.remove(serverName);
if (process != null && process.isAlive()) {
process.destroy();
try {
if (!process.waitFor(10, TimeUnit.SECONDS)) {
process.destroyForcibly();
process.waitFor(2, TimeUnit.SECONDS);
}
System.out.println("MCP 服务器 [" + serverName + "] 已停止");
return true;
} catch (InterruptedException e) {
process.destroyForcibly();
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
/**
* 检查 MCP 服务器是否运行
*/
public boolean isMcpServerRunning(String serverName) {
Process process = runningProcesses.get(serverName);
return process != null && process.isAlive();
}
/**
* 进程信息类
*/
public static class ProcessInfo {
private String pid;
private long startTime;
public String getPid() { return pid; }
public void setPid(String pid) { this.pid = pid; }
public long getStartTime() { return startTime; }
public void setStartTime(long startTime) { this.startTime = startTime; }
public long getUptime() {
return System.currentTimeMillis() - startTime;
}
}
}

View File

@@ -0,0 +1,206 @@
package org.ruoyi.mcp.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class McpSSEToolInvoker {
private final Map<String, CompletableFuture<Object>> pendingRequests = new ConcurrentHashMap<>();
private final AtomicLong requestIdCounter = new AtomicLong(0);
/**
* 调用 MCP 工具SSE 模式)
*/
public Object invokeTool(String serverName, Object parameters) {
try {
// 生成请求ID
String requestId = "req_" + requestIdCounter.incrementAndGet();
// 创建 CompletableFuture 等待响应
CompletableFuture<Object> future = new CompletableFuture<>();
pendingRequests.put(requestId, future);
// 构造 MCP 调用请求
Map<String, Object> callRequest = new HashMap<>();
callRequest.put("requestId", requestId);
callRequest.put("serverName", serverName);
callRequest.put("parameters", convertToMap(parameters));
callRequest.put("timestamp", System.currentTimeMillis());
System.out.println("通过 SSE 调用 MCP 工具 [" + serverName + "] 参数: " + parameters);
// 发送请求到 MCP 服务器(通过 HTTP POST
sendSseToolCall(serverName, callRequest);
// 等待响应(超时 30 秒)
Object result = future.get(30, TimeUnit.SECONDS);
System.out.println("MCP 工具 [" + serverName + "] 调用成功,响应: " + result);
return result;
} catch (Exception e) {
System.err.println("调用 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage());
e.printStackTrace();
return Map.of(
"serverName", serverName,
"status", "failed",
"message", "Tool invocation failed: " + e.getMessage(),
"parameters", parameters
);
}
}
/**
* 发送 SSE 工具调用请求
*/
private void sendSseToolCall(String serverName, Map<String, Object> callRequest) {
try {
// 通过 HTTP POST 发送工具调用请求
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:3000")
.build();
String toolCallUrl = "/tool/" + serverName;
webClient.post()
.uri(toolCallUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(callRequest)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(5))
.subscribe(
response -> System.out.println("工具调用请求发送成功: " + response),
error -> System.err.println("工具调用请求发送失败: " + error.getMessage())
);
} catch (Exception e) {
System.err.println("发送 SSE 工具调用请求失败: " + e.getMessage());
}
}
/**
* 处理 SSE 响应
*/
public void handleSseResponse(String serverName, Map<String, Object> message) {
String requestId = (String) message.get("requestId");
if (requestId != null) {
CompletableFuture<Object> future = pendingRequests.remove(requestId);
if (future != null) {
Object data = message.get("data");
future.complete(data != null ? data : message);
}
}
}
/**
* 处理 SSE 错误
*/
public void handleSseError(String serverName, Map<String, Object> message) {
String requestId = (String) message.get("requestId");
if (requestId != null) {
CompletableFuture<Object> future = pendingRequests.remove(requestId);
if (future != null) {
String errorMessage = (String) message.get("message");
future.completeExceptionally(new RuntimeException(errorMessage));
}
}
}
/**
* 流式调用 MCP 工具(支持实时进度)
*/
public Flux<Object> invokeToolStream(String serverName, Object parameters) {
return Flux.create(emitter -> {
try {
// 生成请求ID
String requestId = "req_" + requestIdCounter.incrementAndGet();
// 构造 MCP 调用请求
Map<String, Object> callRequest = new HashMap<>();
callRequest.put("requestId", requestId);
callRequest.put("serverName", serverName);
callRequest.put("parameters", convertToMap(parameters));
callRequest.put("stream", true); // 标记为流式调用
callRequest.put("timestamp", System.currentTimeMillis());
// 创建流式处理器
StreamHandler streamHandler = new StreamHandler(emitter);
pendingRequests.put(requestId + "_stream", null); // 占位符
// 发送流式调用请求
sendSseToolCall(serverName, callRequest);
// 注册流式处理器
registerStreamHandler(requestId, streamHandler);
emitter.onDispose(() -> {
// 清理资源
pendingRequests.remove(requestId + "_stream");
});
} catch (Exception e) {
emitter.error(e);
}
});
}
/**
* 流式处理器
*/
private static class StreamHandler {
private final FluxSink<Object> emitter;
public StreamHandler(FluxSink<Object> emitter) {
this.emitter = emitter;
}
public void onNext(Object data) {
emitter.next(data);
}
public void onComplete() {
emitter.complete();
}
public void onError(Throwable error) {
emitter.error(error);
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> convertToMap(Object parameters) {
if (parameters instanceof Map) {
Map<String, Object> result = new HashMap<>();
Map<?, ?> paramMap = (Map<?, ?>) parameters;
for (Map.Entry<?, ?> entry : paramMap.entrySet()) {
if (entry.getKey() instanceof String) {
result.put((String) entry.getKey(), entry.getValue());
}
}
return result;
}
return new HashMap<>();
}
private void registerStreamHandler(String requestId, StreamHandler streamHandler) {
// 实现流式处理器注册逻辑
}
}

View File

@@ -0,0 +1,61 @@
package org.ruoyi.mcp.config;
import java.util.List;
import java.util.Map;
public class McpServerConfig {
private String command;
private List<String> args;
private Map<String, String> env;
private String Description;
private String workingDirectory;
// getters and setters
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public List<String> getArgs() {
return args;
}
public void setArgs(List<String> args) {
this.args = args;
}
public Map<String, String> getEnv() {
return env;
}
public void setEnv(Map<String, String> env) {
this.env = env;
}
public String getDescription() {
return Description;
}
public void setDescription(String description) {
Description = description;
}
public String getWorkingDirectory() {
return workingDirectory;
}
public void setWorkingDirectory(String workingDirectory) {
this.workingDirectory = workingDirectory;
}
@Override
public String toString() {
return "McpServerConfig{" +
"command='" + command + '\'' +
", args=" + args +
", env=" + env +
", Description='" + Description + '\'' +
", workingDirectory='" + workingDirectory + '\'' +
'}';
}
}

View File

@@ -0,0 +1,27 @@
package org.ruoyi.mcp.config;
import org.ruoyi.mcp.service.McpToolManagementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class McpStartupConfig {
@Autowired
private McpToolManagementService mcpToolManagementService;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
// 应用启动时自动初始化 MCP 工具
try {
System.out.println("Starting MCP tools initialization...");
mcpToolManagementService.initializeMcpTools();
System.out.println("MCP tools initialization completed successfully");
} catch (Exception e) {
System.err.println("Failed to initialize MCP tools: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,113 @@
package org.ruoyi.mcp.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class McpToolInvoker {
private final Map<String, CompletableFuture<Object>> pendingRequests = new ConcurrentHashMap<>();
private final AtomicLong requestIdCounter = new AtomicLong(0);
@Autowired
private McpProcessManager mcpProcessManager;
/**
* 调用 MCP 工具Studio 模式)
*/
public Object invokeTool(String serverName, Object parameters) {
try {
// 生成请求ID
String requestId = "req_" + requestIdCounter.incrementAndGet();
// 创建 CompletableFuture 等待响应
CompletableFuture<Object> future = new CompletableFuture<>();
pendingRequests.put(requestId, future);
// 构造 MCP 调用消息
Map<String, Object> callMessage = new HashMap<>();
callMessage.put("type", "tool_call");
callMessage.put("requestId", requestId);
callMessage.put("serverName", serverName);
callMessage.put("parameters", convertToMap(parameters));
callMessage.put("timestamp", System.currentTimeMillis());
System.out.println("调用 MCP 工具 [" + serverName + "] 参数: " + parameters);
// 发送消息到 MCP 服务器
boolean sent = mcpProcessManager.sendMcpMessage(serverName, callMessage);
if (!sent) {
pendingRequests.remove(requestId);
throw new RuntimeException("无法发送消息到 MCP 服务器: " + serverName);
}
// 等待响应(超时 30 秒)
Object result = future.get(30, TimeUnit.SECONDS);
System.out.println("MCP 工具 [" + serverName + "] 调用成功,响应: " + result);
return result;
} catch (Exception e) {
System.err.println("调用 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage());
e.printStackTrace();
return Map.of(
"serverName", serverName,
"status", "failed",
"message", "Tool invocation failed: " + e.getMessage(),
"parameters", parameters
);
}
}
/**
* 处理 MCP 服务器的响应消息
*/
public void handleMcpResponse(String serverName, Map<String, Object> message) {
String type = (String) message.get("type");
if ("tool_response".equals(type)) {
String requestId = (String) message.get("requestId");
if (requestId != null) {
CompletableFuture<Object> future = pendingRequests.remove(requestId);
if (future != null) {
Object data = message.get("data");
future.complete(data != null ? data : message);
}
}
} else if ("tool_error".equals(type)) {
String requestId = (String) message.get("requestId");
if (requestId != null) {
CompletableFuture<Object> future = pendingRequests.remove(requestId);
if (future != null) {
String errorMessage = (String) message.get("message");
future.completeExceptionally(new RuntimeException(errorMessage));
}
}
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> convertToMap(Object parameters) {
if (parameters instanceof Map) {
Map<String, Object> result = new HashMap<>();
Map<?, ?> paramMap = (Map<?, ?>) parameters;
for (Map.Entry<?, ?> entry : paramMap.entrySet()) {
if (entry.getKey() instanceof String) {
result.put((String) entry.getKey(), entry.getValue());
}
}
return result;
}
return new HashMap<>();
}
}

View File

@@ -0,0 +1,66 @@
package org.ruoyi.mcp.controller;
import org.ruoyi.mcp.config.McpSSEToolInvoker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
@RestController
@RequestMapping("/api/sse")
public class MCPSseController {
@Autowired
private McpSSEToolInvoker mcpToolInvoker;
/**
* SSE 流式响应端点
*/
@GetMapping(value = "/{serverName}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamMcpResponse(@PathVariable String serverName) {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
try {
// 发送连接建立消息
emitter.send(SseEmitter.event()
.name("connected")
.data(Map.of("serverName", serverName, "status", "connected")));
} catch (Exception e) {
emitter.completeWithError(e);
}
return emitter;
}
/**
* 调用 MCP 工具(流式)
*/
@PostMapping("/tool/{serverName}")
public ResponseEntity<?> callMcpTool(
@PathVariable String serverName,
@RequestBody Map<String, Object> request) {
try {
boolean isStream = (Boolean) request.getOrDefault("stream", false);
Object parameters = request.get("parameters");
if (isStream) {
// 流式调用
return ResponseEntity.ok(Map.of("status", "streaming_started"));
} else {
// 普通调用
Object result = mcpToolInvoker.invokeTool(serverName, parameters);
return ResponseEntity.ok(result);
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", e.getMessage()));
}
}
}

View File

@@ -0,0 +1,162 @@
package org.ruoyi.mcp.controller;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.domain.McpInfo;
import org.ruoyi.domain.bo.McpInfoBo;
import org.ruoyi.domain.vo.McpInfoVo;
import org.ruoyi.mcp.config.McpConfig;
import org.ruoyi.mcp.config.McpServerConfig;
import org.ruoyi.mcp.domain.McpInfoRequest;
import org.ruoyi.mcp.service.McpInfoService;
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.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.core.page.TableDataInfo;
/**
* MCP
*
* @author ageerle
* @date Sat Aug 09 16:50:58 CST 2025
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/operator/mcpInfo")
public class McpInfoController extends BaseController {
private final McpInfoService mcpInfoService;
/**
* 查询MCP列表
*/
@SaCheckPermission("operator:mcpInfo:list")
@GetMapping("/list")
public TableDataInfo<McpInfoVo> list(McpInfoBo bo, PageQuery pageQuery) {
return mcpInfoService.queryPageList(bo, pageQuery);
}
/**
* 导出MCP列表
*/
@SaCheckPermission("operator:mcpInfo:export")
@Log(title = "MCP", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(McpInfoBo bo, HttpServletResponse response) {
List<McpInfoVo> list = mcpInfoService.queryList(bo);
ExcelUtil.exportExcel(list, "MCP", McpInfoVo.class, response);
}
/**
* 获取MCP详细信息
*
* @param mcpId 主键
*/
@SaCheckPermission("operator:mcpInfo:query")
@GetMapping("/{mcpId}")
public R<McpInfoVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Integer mcpId) {
return R.ok(mcpInfoService.queryById(mcpId));
}
/**
* 新增MCP
*/
@SaCheckPermission("operator:mcpInfo:add")
@Log(title = "MCP", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody McpInfoBo bo) {
return toAjax(mcpInfoService.insertByBo(bo));
}
/**
* 修改MCP
*/
@SaCheckPermission("operator:mcpInfo:edit")
@Log(title = "MCP", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody McpInfoBo bo) {
return toAjax(mcpInfoService.updateByBo(bo));
}
/**
* 删除MCP
*
* @param mcpIds 主键串
*/
@SaCheckPermission("operator:mcpInfo:remove")
@Log(title = "MCP", businessType = BusinessType.DELETE)
@DeleteMapping("/{mcpIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Integer[] mcpIds) {
return toAjax(mcpInfoService.deleteWithValidByIds(List.of(mcpIds), true));
}
/**
* 添加或更新 MCP 工具
*/
@PostMapping("/tools")
public R<McpInfo> saveToolConfig(@RequestBody McpInfoRequest request) {
return R.ok(mcpInfoService.saveToolConfig(request));
}
/**
* 获取所有活跃服务器名称
*/
@GetMapping("/tools/names")
public R<List<String>> getActiveServerNames() {
return R.ok(mcpInfoService.getActiveServerNames());
}
/**
* 根据名称获取工具配置
*/
@GetMapping("/tools/{serverName}")
public R<McpServerConfig> getToolConfig(@PathVariable String serverName) {
return R.ok(mcpInfoService.getToolConfigByName(serverName));
}
/**
* 启用工具
*/
@PostMapping("/tools/{serverName}/enable")
public Map<String, Object> enableTool(@PathVariable String serverName) {
boolean success = mcpInfoService.enableTool(serverName);
return Map.of("success", success);
}
/**
* 禁用工具
*/
@PostMapping("/tools/{serverName}/disable")
public Map<String, Object> disableTool(@PathVariable String serverName) {
boolean success = mcpInfoService.disableTool(serverName);
return Map.of("success", success);
}
/**
* 删除工具
*/
@DeleteMapping("/tools/{serverName}")
public Map<String, Object> deleteTool(@PathVariable String serverName) {
boolean success = mcpInfoService.deleteToolConfig(serverName);
return Map.of("success", success);
}
}

View File

@@ -0,0 +1,28 @@
package org.ruoyi.mcp.domain;
import java.util.List;
import java.util.Map;
public class McpInfoRequest {
private String serverName;
private String command;
private List<String> args;
private Map<String, String> env;
private String description;
// getters and setters
public String getServerName() { return serverName; }
public void setServerName(String serverName) { this.serverName = serverName; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public List<String> getArgs() { return args; }
public void setArgs(List<String> args) { this.args = args; }
public Map<String, String> getEnv() { return env; }
public void setEnv(Map<String, String> env) { this.env = env; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}

View File

@@ -0,0 +1,68 @@
package org.ruoyi.mcp.service;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.domain.McpInfo;
import org.ruoyi.domain.bo.McpInfoBo;
import org.ruoyi.domain.vo.McpInfoVo;
import org.ruoyi.mcp.config.McpConfig;
import org.ruoyi.mcp.config.McpServerConfig;
import org.ruoyi.mcp.domain.McpInfoRequest;
import java.util.Collection;
import java.util.List;
/**
* MCPService接口
*
* @author ageerle
* @date Sat Aug 09 16:50:58 CST 2025
*/
public interface McpInfoService {
/**
* 查询MCP
*/
McpInfoVo queryById(Integer mcpId);
/**
* 查询MCP列表
*/
TableDataInfo<McpInfoVo> queryPageList(McpInfoBo bo, PageQuery pageQuery);
/**
* 查询MCP列表
*/
List<McpInfoVo> queryList(McpInfoBo bo);
/**
* 新增MCP
*/
Boolean insertByBo(McpInfoBo bo);
/**
* 修改MCP
*/
Boolean updateByBo(McpInfoBo bo);
/**
* 校验并批量删除MCP信息
*/
Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid);
McpServerConfig getToolConfigByName(String serverName);
McpConfig getAllActiveMcpConfig();
List<String> getActiveServerNames();
McpInfo saveToolConfig(McpInfoRequest request);
boolean deleteToolConfig(String serverName);
boolean updateToolStatus(String serverName, Boolean status);
boolean enableTool(String serverName);
boolean disableTool(String serverName);
}

View File

@@ -0,0 +1,134 @@
package org.ruoyi.mcp.service;
import org.ruoyi.domain.McpInfo;
import org.ruoyi.mcp.config.McpConfig;
import org.ruoyi.mcp.config.McpProcessManager;
import org.ruoyi.mcp.config.McpServerConfig;
import org.ruoyi.mcp.domain.McpInfoRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
@Service
public class McpToolManagementService {
@Autowired
private McpInfoService mcpInfoService;
@Autowired
private McpProcessManager mcpProcessManager;
/**
* 初始化所有 MCP 工具(应用启动时调用)
*/
public void initializeMcpTools() {
System.out.println("Initializing MCP tools...");
McpConfig config = mcpInfoService.getAllActiveMcpConfig();
if (config.getMcpServers() != null) {
int successCount = 0;
int totalCount = config.getMcpServers().size();
for (Map.Entry<String, McpServerConfig> entry : config.getMcpServers().entrySet()) {
String serverName = entry.getKey();
McpServerConfig serverConfig = entry.getValue();
System.out.println("Starting MCP server: " + serverName);
System.out.println("Starting MCP serverConfig: " + serverConfig);
// 启动 MCP 服务器进程
boolean started = mcpProcessManager.startMcpServer(serverName,serverConfig);
if (started) {
successCount++;
System.out.println("✓ MCP server [" + serverName + "] started successfully");
} else {
System.err.println("✗ Failed to start MCP server [" + serverName + "]");
}
}
System.out.println("MCP tools initialization completed. " +
successCount + "/" + totalCount + " tools started.");
}
}
/**
* 添加新的 MCP 工具并启动
*/
public boolean addMcpTool(McpInfoRequest request) {
try {
McpInfo tool = mcpInfoService.saveToolConfig(request);
// 启动新添加的工具
McpServerConfig config = new McpServerConfig();
config.setCommand(request.getCommand());
config.setArgs(request.getArgs());
config.setEnv(request.getEnv());
boolean started = mcpProcessManager.startMcpServer(
request.getServerName(),
config
);
return started;
} catch (Exception e) {
System.err.println("Failed to add MCP tool: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 获取 MCP 工具状态
*/
public Map<String, Object> getMcpToolStatus() {
List<String> activeTools = mcpInfoService.getActiveServerNames();
Map<String, Object> status = new HashMap<>();
for (String serverName : activeTools) {
boolean isRunning = mcpProcessManager.isMcpServerRunning(serverName);
McpProcessManager.McpServerProcess processInfo = mcpProcessManager.getProcessInfo(serverName);
Map<String, Object> toolStatus = new HashMap<>();
toolStatus.put("running", isRunning);
toolStatus.put("processInfo", processInfo);
status.put(serverName, toolStatus);
}
return status;
}
/**
* 重启指定的 MCP 工具
*/
public boolean restartMcpTool(String serverName) {
McpServerConfig config = mcpInfoService.getToolConfigByName(serverName);
if (config == null) {
return false;
}
return mcpProcessManager.restartMcpServer(
serverName,
config.getCommand(),
config.getArgs(),
config.getEnv()
);
}
/**
* 停止指定的 MCP 工具
*/
public boolean stopMcpTool(String serverName) {
return mcpProcessManager.stopMcpServer(serverName);
}
/**
* 获取所有运行中的工具
*/
public Set<String> getRunningTools() {
return mcpProcessManager.getRunningMcpServers();
}
}

View File

@@ -0,0 +1,252 @@
package org.ruoyi.mcp.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ruoyi.common.core.utils.MapstructUtils;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.ruoyi.domain.McpInfo;
import org.ruoyi.domain.bo.McpInfoBo;
import org.ruoyi.domain.vo.McpInfoVo;
import org.ruoyi.mapper.McpInfoMapper;
import org.ruoyi.mcp.config.McpConfig;
import org.ruoyi.mcp.config.McpServerConfig;
import org.ruoyi.mcp.domain.McpInfoRequest;
import org.ruoyi.mcp.service.McpInfoService;
import org.springframework.stereotype.Service;
import org.ruoyi.common.core.utils.StringUtils;
import java.util.*;
/**
* MCPService业务层处理
*
* @author ageerle
* @date Sat Aug 09 16:50:58 CST 2025
*/
@RequiredArgsConstructor
@Service
public class McpInfoServiceImpl implements McpInfoService {
private final McpInfoMapper baseMapper;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 查询MCP
*/
@Override
public McpInfoVo queryById(Integer mcpId) {
return baseMapper.selectVoById(mcpId);
}
/**
* 查询MCP列表
*/
@Override
public TableDataInfo<McpInfoVo> queryPageList(McpInfoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<McpInfo> lqw = buildQueryWrapper(bo);
Page<McpInfoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询MCP列表
*/
@Override
public List<McpInfoVo> queryList(McpInfoBo bo) {
LambdaQueryWrapper<McpInfo> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<McpInfo> buildQueryWrapper(McpInfoBo bo) {
LambdaQueryWrapper<McpInfo> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getServerName()), McpInfo::getServerName, bo.getServerName());
lqw.eq(StringUtils.isNotBlank(bo.getTransportType()), McpInfo::getTransportType, bo.getTransportType());
lqw.eq(StringUtils.isNotBlank(bo.getCommand()), McpInfo::getCommand, bo.getCommand());
lqw.eq(bo.getStatus() != null, McpInfo::getStatus, bo.getStatus());
return lqw;
}
/**
* 新增MCP
*/
@Override
public Boolean insertByBo(McpInfoBo bo) {
McpInfo add = MapstructUtils.convert(bo, McpInfo. class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setMcpId(add.getMcpId());
}
return flag;
}
/**
* 修改MCP
*/
@Override
public Boolean updateByBo(McpInfoBo bo) {
McpInfo update = MapstructUtils.convert(bo, McpInfo. class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(McpInfo entity) {
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除MCP
*/
@Override
public Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid) {
if (isValid) {
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 根据服务器名称获取工具配置
*/
@Override
public McpServerConfig getToolConfigByName(String serverName) {
McpInfo tool = baseMapper.selectByServerName(serverName);
if (tool != null) {
return convertToMcpServerConfig(tool);
}
return null;
}
/**
* 获取所有活跃的 MCP 工具配置
*/
@Override
public McpConfig getAllActiveMcpConfig() {
List<McpInfo> activeTools = baseMapper.selectActiveServers();
Map<String, McpServerConfig> servers = new HashMap<>();
for (McpInfo tool : activeTools) {
McpServerConfig serverConfig = convertToMcpServerConfig(tool);
servers.put(tool.getServerName(), serverConfig);
}
McpConfig config = new McpConfig();
config.setMcpServers(servers);
return config;
}
/**
* 获取所有活跃服务器名称
*/
@Override
public List<String> getActiveServerNames() {
return baseMapper.selectActiveServerNames();
}
/**
* 保存或更新 MCP 工具配置
*/
@Override
public McpInfo saveToolConfig(McpInfoRequest request) {
McpInfo existingTool = baseMapper.selectByServerName(request.getServerName());
McpInfo tool;
if (existingTool != null) {
tool = existingTool;
} else {
tool = new McpInfo();
}
tool.setServerName(request.getServerName());
tool.setCommand(request.getCommand());
try {
tool.setArguments(objectMapper.writeValueAsString(request.getArgs()));
if (request.getEnv() != null) {
tool.setEnv(objectMapper.writeValueAsString(request.getEnv()));
}
} catch (Exception e) {
throw new RuntimeException("Failed to serialize JSON data", e);
}
tool.setDescription(request.getDescription());
tool.setStatus(true); // 默认启用
if (existingTool != null) {
baseMapper.updateById(tool);
} else {
baseMapper.insert(tool);
}
return tool;
}
/**
* 删除工具配置
*/
@Override
public boolean deleteToolConfig(String serverName) {
return baseMapper.deleteByServerName(serverName) > 0;
}
/**
* 更新工具状态
*/
@Override
public boolean updateToolStatus(String serverName, Boolean status) {
return baseMapper.updateActiveStatus(serverName, status) > 0;
}
/**
* 启用工具
*/
@Override
public boolean enableTool(String serverName) {
return updateToolStatus(serverName, true);
}
/**
* 禁用工具
*/
@Override
public boolean disableTool(String serverName) {
return updateToolStatus(serverName, false);
}
private McpServerConfig convertToMcpServerConfig(McpInfo tool) {
McpServerConfig config = new McpServerConfig();
config.setCommand(tool.getCommand());
try {
// 解析 args
if (tool.getArguments() != null && !tool.getArguments().isEmpty()) {
List<String> args = objectMapper.readValue(tool.getArguments(), new TypeReference<List<String>>() {});
config.setArgs(args);
} else {
config.setArgs(new ArrayList<>());
}
// 解析 env
if (tool.getEnv() != null && !tool.getEnv().isEmpty()) {
Map<String, String> env = objectMapper.readValue(tool.getEnv(), new TypeReference<Map<String, String>>() {});
config.setEnv(env);
} else {
config.setEnv(new HashMap<>());
}
} catch (Exception e) {
config.setArgs(new ArrayList<>());
config.setEnv(new HashMap<>());
}
return config;
}
}

View File

@@ -0,0 +1,46 @@
-- 菜单 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1954103099019309056, 'MCP', '2000', '1', 'mcpInfo', 'operator/mcpInfo/index', 1, 0, 'C', '0', '0', 'operator:mcpInfo:list', '#', 103, 1, sysdate(), null, null, 'MCP菜单');
-- 按钮 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1954103099019309057, 'MCP查询', 1954103099019309056, '1', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:query', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1954103099019309058, 'MCP新增', 1954103099019309056, '2', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:add', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1954103099019309059, 'MCP修改', 1954103099019309056, '3', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:edit', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1954103099019309060, 'MCP删除', 1954103099019309056, '4', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:remove', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1954103099019309061, 'MCP导出', 1954103099019309056, '5', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:export', '#', 103, 1, sysdate(), null, null, '');
-- mcp_info ddl
CREATE TABLE `mcp_info` (
`mcp_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`server_name` varchar(50) DEFAULT NULL COMMENT '服务器名称',
`transport_type` varchar(255) DEFAULT NULL COMMENT '链接方式',
`command` varchar(255) DEFAULT NULL COMMENT 'Command',
`arguments` varchar(255) DEFAULT NULL COMMENT 'Args',
`env` varchar(255) DEFAULT NULL COMMENT 'Env',
`status` tinyint(1) DEFAULT NULL COMMENT '是否启用',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`mcp_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098808913211393, '000000', 0, 'STDIO', 'STDIO', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:33:56', 1, '2025-08-09 16:34:19', NULL);
INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098960432443394, '000000', 1, 'SSE', 'SSE', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:34:32', NULL, '2025-08-09 16:34:32', NULL);
INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954099421436784642, '000000', 2, 'HTTP', 'HTTP', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:36:22', NULL, '2025-08-09 16:36:22', NULL);
INSERT INTO `ruoyi-ai`.`sys_dict_type` (`dict_id`, `tenant_id`, `dict_name`, `dict_type`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098639622713345, '000000', 'mcp链接方式', 'mcp_transport_type', '0', NULL, NULL, '2025-08-09 16:33:16', NULL, '2025-08-09 16:33:16', NULL);
INSERT INTO `ruoyi-ai`.`mcp_info` (`mcp_id`, `server_name`, `transport_type`, `command`, `arguments`, `env`, `status`, `description`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1, 'howtocook-mcp', 'STDIO', 'npx', '["-y", "howtocook-mcp"]', NULL, 1, NULL, NULL, NULL, '2025-08-11 17:19:25', 1, '2025-08-11 18:24:22', NULL);