22 Commits

Author SHA1 Message Date
ageerle
15c306eca2 fix: 根据不同的模型构建对话客户端 2025-04-22 11:28:10 +08:00
ageerle
620ea1fc76 fix: 系统提示词非必填 2025-04-22 11:06:38 +08:00
ageerle
c5c375dc6d fix: 扣费时无法获取用户id 2025-04-22 10:43:54 +08:00
ageerle
1b793e822a fix: 扣费时无法获取用户id 2025-04-22 10:40:36 +08:00
ageerle
6281840f36 fix: 修复后台管理系统登录异常 2025-04-22 09:12:56 +08:00
ageer
f5daa7eb78 feat: 设置server-filesystem默认路径为D盘 防止启动报错 2025-04-21 20:34:05 +08:00
ageer
52cb563383 fix(1.修复/store/appList404 2.修复无法查询数据库 3.修复请求地址'/chat/config/configKey/logoImage',发生系统异常): 2025-04-21 20:16:53 +08:00
ageerle
69efc3261e Merge branch 'dev' 2025-04-21 09:44:09 +08:00
ageerle
fb19fb29cc feat: 移除文件 2025-04-21 09:43:09 +08:00
ageer
9f257cf712 feat: mcp升级依赖 2025-04-20 22:52:09 +08:00
ageer
4af26fe4b4 feat: mcp升级依赖 2025-04-20 22:52:00 +08:00
ageerle
ab2a118ee9 feat: 更新项目说明 2025-04-18 11:11:48 +08:00
ageerle
9cd97a4dc5 feat: mcp支持远程调用 2025-04-18 10:55:42 +08:00
ageerle
788b372e32 feat: mcp支持远程调用 2025-04-17 16:23:32 +08:00
lh
744f9b6c7f 更新readme.md 2025-04-17 11:09:00 +08:00
ageer
761d954ef1 feat: mcp 1.0.0 2025-04-16 21:56:05 +08:00
ageerle
d1006f50ad feat: mcp-1.0.0 2025-04-16 10:25:10 +08:00
ageerle
f24ff5bbdd feat: mcp 测试版本 2025-04-15 17:34:21 +08:00
ageer
a4314dbbde feat: mcp测试版 2025-04-14 23:10:17 +08:00
ageerle
188dc1e55e feat: mcp 测试版本 2025-04-14 17:23:33 +08:00
lh
b50c15755d 更新readme.md 2025-04-12 14:19:51 +08:00
lh
09abc0b5af 新增讨论群信息 2025-04-08 14:05:05 +08:00
90 changed files with 1015 additions and 3420 deletions

125
README.md
View File

@@ -36,48 +36,75 @@
## 目录
- [系统体验](#系统体验)
- [源码地址](#源码地址)
- [特色功能](#特色功能)
- [配套文档](#项目文档)
- [核心功能](#核心功能)
- [项目演示](#项目演示)
- [后台管理](#后台管理)
- [管理](#管理)
- [用户端](#用户端)
- [小程序端](#小程序端)
- [开发前的配置要求](#开发前的配置要求)
- [文件目录说明](#文件目录说明)
- [使用到的框架](#使用到的框架)
- [开发环境](#开发环境)
- [项目结构](#项目结构)
- [ruoyi-ai](#ruoyi-ai)
- [注意事项](#注意事项)
- [vben模板](#vben模板)
- [贡献者](#贡献者)
- [如何参与开源项目](#如何参与开源项目)
- [版本控制](#版本控制)
- [作者](#作者)
- [鸣谢](#鸣谢)
- [技术讨论群](#技术讨论群)
### 系统体验
- 用户端https://web.pandarobot.chat
- 管理端https://admin.pandarobot.chat
用户名: admin 密码admin123
### 源码地址
- 项目文档: https://doc.pandarobot.chat
- 前端-后台管理: https://github.com/ageerle/ruoyi-admin
- 前端-用户端: https://github.com/ageerle/ruoyi-web
- 小程序端: https://github.com/ageerle/ruoyi-uniapp
- 演示地址: https://web.pandarobot.chat
- 后台管理: https://admin.pandarobot.chat
- 用户名: admin 密码admin123
[1]github
- 前端服务-用户端: https://github.com/ageerle/ruoyi-web
- 前端服务-管理端: https://github.com/ageerle/ruoyi-admin
- 前端服务-小程序端: https://github.com/ageerle/ruoyi-uniapp
- 后端服务:https://github.com/ageerle/ruoyi-ai
### gitcode源码地址
- https://gitcode.com/ageerle/ruoyi-ai
- https://gitcode.com/ageerle/ruoyi-web
- https://gitcode.com/ageerle/ruoyi-admin
- https://gitcode.com/ageerle/ruoyi-uniapp
[2]gitee
- 前端服务-用户端: https://gitee.com/ageerle/ruoyi-web
- 前端服务-管理端: https://gitee.com/ageerle/ruoyi-admin
- 前端服务-小程序端: https://gitee.com/ageerle/ruoyi-uniapp
- 后端服务:https://gitee.com/ageerle/ruoyi-ai
### 特色功能
[3]gitcode
- 前端服务-用户端https://gitcode.com/ageerle/ruoyi-web
- 前端服务-管理端: https://gitcode.com/ageerle/ruoyi-admin
- 前端服务-小程序端: https://gitcode.com/ageerle/ruoyi-uniapp
- 后端服务https://gitcode.com/ageerle/ruoyi-ai
### 配套文档
- 配套文档: https://doc.pandarobot.chat
- 项目部署文档https://doc.pandarobot.chat/guide/introduction/
### 核心功能
1. 全套开源系统提供完整的前端应用、后台管理以及小程序应用基于MIT协议开箱即用。
2. 本地RAG方案集成Milvus/Weaviate向量库、本地向量化模型与Ollama实现本地化RAG
2. 本地RAG方案集成Milvus/Weaviate向量库、本地向量化模型与Ollama实现本地化RAG
3. 丰富插件功能支持联网、SQL查询插件及Text2API插件扩展系统能力与应用场景。
4. 内置SSE、websocket等网络协议支持对接多种大语言模型同时还集成了MidJourney和DALLE AI绘画功能
5. 强大的多媒体功能支持AI翻译、PPT制作、语音克隆和翻唱等
6. 扩展功能:支持将大模型接入个人或企业微信
7. 支付功能:支持易支付、微信支付等多种支付方式
4. 内置SSE、websocket等网络协议支持对接多种大语言模型同时还集成了MidJourney和DALLE AI绘画功能
5. 强大的多媒体功能支持AI翻译、PPT制作、语音克隆和翻唱等
6. 扩展功能:支持将大模型接入个人或企业微信
7. 支付功能:支持易支付、微信支付等多种支付方式
### 项目演示
#### 后台管理
#### mcp支持(需要切换dev分支 下周发布正式版)
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/mcp-01.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/mcp-02.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/mcp-03.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/mcp-04.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>
#### 管理端
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/02.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
<img src="image/03.png" alt="drawing" style="width: 600px; height: 300px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
@@ -100,7 +127,7 @@
<img src="image/07.png" alt="drawing" style="width: 320px; height: 600px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>
### 开发前的配置要求
### 开发环境
1. jdk 17
2. mysql 5.7、8.0
@@ -108,8 +135,14 @@
4. maven 3.8+
5. nodejs 20+ & pnpm
### 文件目录说明
RuoYi-AI
- 附-部署配套视频https://www.bilibili.com/video/BV1jDXkYWEba
<div>
<img src="image/教程搭建.png" alt="drawing" width="600px" height="300px"/>
</div>
### 项目结构
- RuoYi-AI
```
├─ ruoyi-admin // 管理模块
@@ -158,6 +191,14 @@ RuoYi-AI
```
### 注意事项
- vben模板
Qvben5 的模板默认是没有的吗?
Avben模板是收费的 请联系vben-vue-plus作者获取。
### 版本控制
该项目使用Git进行版本管理。您可以在repository参看当前可用版本。
@@ -170,17 +211,10 @@ RuoYi-AI
### 项目现状
目前项目还处于早期阶段距离成熟还有很长的路要走。由于个人精力有限项目的发展速度受到了一定的限制。为了加快项目的进度我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者还是刚刚入门的小白我都热烈欢迎你们提交Pull RequestPR。即使代码修改得很少或者存在一些错误都没有关系。我会认真审核每一位贡献者的代码并和大家一起完善项目。
目前项目还处于早期阶段距离成熟还有很长的路要走。由于个人精力有限项目的发展速度受到了一定的限制。为了加快项目的进度我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者还是刚刚入门的小白我都热烈欢迎你们提交Pull RequestPR👏👏👏。即使代码修改得很少,或者存在一些错误,都没有关系。我会认真审核每一位贡献者的代码,并和大家一起完善项目⛽️⛽️⛽️
### 开发计划
- 智能体管理
通过设置提示词、插件、知识库等用户可以快速构建一个AI应用。这将极大地简化AI应用的开发流程降低开发门槛使更多企业能够轻松地利用AI技术。
<div>
<img src="image/13.png" alt="drawing" width="600px" height="300px"/>
</div>
- 流程编排
通过流程编排功能,用户可以将不同的模型按照业务逻辑进行有序连接。这将解决单一模型能力不足的问题,充分发挥多个模型的协同作用,从而更好地满足企业的复杂业务需求。
@@ -193,7 +227,7 @@ RuoYi-AI
#### 如何参与开源项目
贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献都是**非常感谢**的。
贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献,我们都非常感谢!🙏
1. Fork 这个项目
2. 创建你的功能分支 (`git checkout -b feature/dev`)
@@ -231,4 +265,23 @@ RuoYi-AI
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
### 附:技术讨论群
#### 全面开放,欢迎加入
🏠 wxruoyi-ai加人备注ruoyi-ai
🏠 qq1603234088 加人备注ruoyi-ai
👏👏👏 ruoyi-ai官方交流1群qq区1034554687
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/QQ区-官方交流1群.png" alt="drawing" style="width: 400px; height: 400px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>
👏👏👏 ruoyi-ai官方交流4群微信区
<div style="display: flex; flex-wrap: wrap; gap: 20px; justify-content: center;">
<img src="image/WX区-官方交流4群.jpg" alt="drawing" style="width: 400px; height: 400px; border: 2px solid #ddd; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"/>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

BIN
image/mcp-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
image/mcp-02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
image/mcp-03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
image/mcp-04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
image/qq-msg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

BIN
image/wx-msg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
image/wx-msg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
image/教程搭建.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

View File

@@ -63,6 +63,8 @@ public class AuthController {
body.getUsername(), body.getPassword(),
body.getCode(), body.getUuid());
loginVo.setToken(token);
// 兼容后台管理登录
loginVo.setAccess_token(token);
loginVo.setUserInfo(LoginHelper.getLoginUser());
return R.ok(loginVo);
}

View File

@@ -322,14 +322,17 @@ wechat:
spring:
ai:
ollama:
base-url: http://localhost:11434
openai:
api-key: sk-xxx
base-url: https://api.pandarobot.chat/
mcp:
client:
enabled: true
name: call-mcp-server
name: ruoyi-ai-mcp
sse:
connections:
server1:
url: http://127.0.0.1:6040
server:
url: http://127.0.0.1:8081
stdio:
servers-configuration: classpath:mcp-server.json

View File

@@ -0,0 +1,22 @@
{
"mcpServers": {
"fileSystem": {
"command": "C:\\Program Files\\nodejs\\npx.cmd",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"D:\\"
]
},
"search1api": {
"command": "C:\\Program Files\\nodejs\\npx.cmd",
"args": [
"-y",
"search1api-mcp"
],
"env": {
"SEARCH1API_KEY": "xx"
}
}
}
}

View File

@@ -41,6 +41,11 @@ public class ChatRequest {
*/
private Boolean search = Boolean.FALSE;
/**
* 是否开启mcp
*/
private Boolean isMcp = Boolean.FALSE;
/**
* 知识库id
*/
@@ -49,7 +54,7 @@ public class ChatRequest {
/**
* 用户id
*/
private String userId;
private Long userId;
/**
* 应用ID

View File

@@ -1,31 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-ai</artifactId>
<version>${revision}</version>
<relativePath>../../pom.xml</relativePath>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-mcp-server</artifactId>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<version>0.0.1-SNAPSHOT</version>
<name>ruoyi-mcp-serve</name>
<description>ruoyi-mcp-serve</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M7</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
@@ -34,31 +47,30 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-system-api</artifactId>
<exclusions>
<exclusion>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-translation</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,16 +0,0 @@
package org.ruoyi.mcp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author ageer
*/
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}

View File

@@ -1,100 +0,0 @@
package org.ruoyi.mcp.config;
import org.ruoyi.mcp.service.McpCustomService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* @author ageer
*/
@Configuration
@EnableWebMvc
public class McpServerConfig implements WebMvcConfigurer {
@Bean
public ToolCallbackProvider openLibraryTools(McpCustomService mcpService) {
return MethodToolCallbackProvider.builder().toolObjects(mcpService).build();
}
@Bean
public List<McpServerFeatures.SyncResourceRegistration> resourceRegistrations() {
// Create a resource registration for system information
var systemInfoResource = new McpSchema.Resource(
"system://info",
"System Information",
"Provides basic system information including Java version, OS, etc.",
"application/json", null
);
var resourceRegistration = new McpServerFeatures.SyncResourceRegistration(systemInfoResource, (request) -> {
try {
var systemInfo = Map.of(
"javaVersion", System.getProperty("java.version"),
"osName", System.getProperty("os.name"),
"osVersion", System.getProperty("os.version"),
"osArch", System.getProperty("os.arch"),
"processors", Runtime.getRuntime().availableProcessors(),
"timestamp", System.currentTimeMillis());
String jsonContent = new ObjectMapper().writeValueAsString(systemInfo);
return new McpSchema.ReadResourceResult(
List.of(new McpSchema.TextResourceContents(request.uri(), "application/json", jsonContent)));
}
catch (Exception e) {
throw new RuntimeException("Failed to generate system info", e);
}
});
return List.of(resourceRegistration);
}
@Bean
public List<McpServerFeatures.SyncPromptRegistration> promptRegistrations() {
var prompt = new McpSchema.Prompt("greeting", "A friendly greeting prompt",
List.of(new McpSchema.PromptArgument("name", "The name to greet", true)));
var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> {
String nameArgument = (String) getPromptRequest.arguments().get("name");
if (nameArgument == null) {
nameArgument = "friend";
}
var userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER,
new McpSchema.TextContent("Hello " + nameArgument + "! How can I assist you today?"));
return new McpSchema.GetPromptResult("A personalized greeting message", List.of(userMessage));
});
return List.of(promptRegistration);
}
@Bean
public Consumer<List<McpSchema.Root>> rootsChangeConsumer() {
return roots -> {
System.out.println("rootsChange");
};
}
}

View File

@@ -1,27 +0,0 @@
package org.ruoyi.mcp.service;
import lombok.RequiredArgsConstructor;
import org.ruoyi.system.domain.vo.SysUserVo;
import org.ruoyi.system.mapper.SysUserMapper;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
/**
* @author ageer
*/
@Service
@RequiredArgsConstructor
public class McpCustomService {
private final SysUserMapper userMapper;
public record User(String userName, Double userBalance) {
}
@Tool(description = "根据用户名称查询用户信息")
public User getUserBalance(String username) {
SysUserVo sysUserVo = userMapper.selectUserByUserName(username);
return new User(sysUserVo.getUserName(),sysUserVo.getUserBalance());
}
}

View File

@@ -0,0 +1,25 @@
package org.ruoyi.mcpserve;
import org.ruoyi.mcpserve.service.ToolService;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
/**
* @author ageer
*/
@SpringBootApplication
public class RuoyiMcpServeApplication {
public static void main(String[] args) {
SpringApplication.run(RuoyiMcpServeApplication.class, args);
}
@Bean
public ToolCallbackProvider systemTools(ToolService toolService) {
return MethodToolCallbackProvider.builder().toolObjects(toolService).build();
}
}

View File

@@ -0,0 +1,34 @@
package org.ruoyi.mcpserve.service;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* @author ageer
*/
@Service
public class ToolService {
@Tool(description = "获取一个指定前缀的随机数")
public String add(@ToolParam(description = "字符前缀") String prefix) {
// 定义日期格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMMdd");
//根据当前时间获取yyMMdd格式的时间字符串
String format = LocalDate.now().format(formatter);
//生成随机数
String replace = prefix + UUID.randomUUID().toString().replace("-", "");
return format + replace;
}
@Tool(description = "获取当前时间")
public LocalDateTime getCurrentTime() {
return LocalDateTime.now();
}
}

View File

@@ -1,97 +0,0 @@
--- # 监控中心配置
spring.boot.admin.client:
# 增加客户端开关
enabled: false
url: http://localhost:9090/admin
instance:
service-host-type: IP
username: ruoyi
password: 123456
--- # 数据源配置
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
dynamic:
# 性能分析插件(有性能损耗 不建议生产环境使用)
p6spy: true
# 设置默认的数据源或者数据源组,默认值即为 master
primary: master
# 严格模式 匹配不到数据源则报错
strict: true
datasource:
# 主库数据源
master:
type: ${spring.datasource.type}
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://43.139.70.230:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: ry-vue
password: xx
hikari:
# 最大连接池数量
maxPoolSize: 20
# 最小空闲线程数量
minIdle: 10
# 配置获取连接等待超时的时间
connectionTimeout: 30000
# 校验超时时间
validationTimeout: 5000
# 空闲连接存活最大时间默认10分钟
idleTimeout: 600000
# 此属性控制池中连接的最长生命周期值0表示无限生命周期默认30分钟
maxLifetime: 1800000
# 连接测试query配置检测连接是否有效
connectionTestQuery: SELECT 1
# 多久检查一次连接的活性
keepaliveTime: 30000
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring.data:
redis:
# 地址
host: 127.0.0.1
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码(如没有密码请注释掉)
# password: 123456
# 连接超时时间
timeout: 10S
redisson:
# redis key前缀
keyPrefix:
# 线程池数量
threads: 4
# Netty线程池数量
nettyThreads: 8
# 单节点配置
singleServerConfig:
# 客户端名称
clientName: ${ruoyi.name}
# 最小空闲连接数
connectionMinimumIdleSize: 8
# 连接池大小
connectionPoolSize: 32
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
--- # sms 短信
sms:
enabled: false
# 阿里云 dysmsapi.aliyuncs.com
# 腾讯云 sms.tencentcloudapi.com
endpoint: "dysmsapi.aliyuncs.com"
accessKeyId: xxxxxxx
accessKeySecret: xxxxxx
signName: 测试
# 腾讯专用
sdkAppId:

View File

@@ -1,10 +0,0 @@
spring:
application:
name: mcp-server
ai:
mcp:
server:
name: webmvc-mcp-server
version: 1.0.0
type: SYNC
sse-message-endpoint: /mcp/messages

View File

@@ -1,332 +1,10 @@
# 项目相关配置
ruoyi:
# 名称
name: "ruoyi"
# 版本
version: ${revision}
# 版权年份
copyrightYear: 2025
# 实例演示开关
demoEnabled: true
# 获取ip地址开关
addressEnabled: false
captcha:
enable: false
# 页面 <参数设置> 可开启关闭 验证码校验
# 验证码类型 math 数组计算 char 字符验证
type: MATH
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
category: CIRCLE
# 数字验证码位数
numberLength: 1
# 字符验证码长度
charLength: 4
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 6040
servlet:
# 应用的访问路径
context-path: /
# undertow 配置
undertow:
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512
# 是否分配的直接内存
direct-buffers: true
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io: 8
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 256
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
# Spring配置
spring:
application:
name: ${ruoyi.name}
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: @profiles.active@
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 50MB
# 设置总上传的文件大小
max-request-size: 200MB
mvc:
format:
date-time: yyyy-MM-dd HH:mm:ss
jackson:
# 日期格式化
date-format: yyyy-MM-dd HH:mm:ss
serialization:
# 格式化输出
indent_output: false
# 忽略无法转换的对象
fail_on_empty_beans: false
deserialization:
# 允许对象忽略json中不存在的属性
fail_on_unknown_properties: false
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# token有效期 设为7天 (必定过期) 单位: 秒
timeout: 604800
# token临时有效期 (指定时间无操作就过期) 单位: 秒
activity-timeout: 604800
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# 是否尝试从header里读取token
is-read-header: true
# 是否尝试从cookie里读取token
is-read-cookie: false
# token前缀
token-prefix: "Bearer"
# jwt秘钥
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
# security配置
security:
# 排除路径
excludes:
# 支付回调
- /pay/returnUrl
- /pay/notifyUrl
# 上传文件
- /resource/oss/upload
# 重置密码
- /auth/reset/password
# 聊天接口
- /chat
# 静态资源
- /*.html
- /**/*.html
- /**/*.css
- /**/*.js
# 公共路径
- /favicon.ico
- /error
# swagger 文档配置
- /*/api-docs
- /*/api-docs/**
# actuator 监控配置
- /actuator
- /actuator/**
# 多租户配置
tenant:
# 是否开启
enable: false
# 排除表
excludes:
- sys_menu
- sys_tenant
- sys_tenant_package
- sys_role_dept
- sys_role_menu
- sys_user_post
- sys_user_role
# MyBatisPlus配置
# https://baomidou.com/config/
mybatis-plus:
# 不支持多包, 如有需要可在注解配置 或 提升扫包等级
# 例如 com.**.**.mapper
mapperPackage: org.ruoyi.**.mapper
# 对应的 XML 文件位置
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 实体扫描多个package用逗号或者分号分隔
typeAliasesPackage: org.ruoyi.**.domain
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
checkConfigLocation: false
configuration:
# 自动驼峰命名规则camel case映射
mapUnderscoreToCamelCase: true
# MyBatis 自动映射策略
# NONE不启用 PARTIAL只对非嵌套 resultMap 自动映射 FULL对所有 resultMap 自动映射
autoMappingBehavior: FULL
# MyBatis 自动映射时未知列或未知属性处理策
# NONE不做处理 WARNING打印相关警告 FAILING抛出异常和详细信息
autoMappingUnknownColumnBehavior: NONE
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
global-config:
# 是否打印 Logo banner
banner: true
dbConfig:
# 主键类型
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
idType: ASSIGN_ID
# 逻辑已删除值
logicDeleteValue: 2
# 逻辑未删除值
logicNotDeleteValue: 0
# 字段验证策略之 insert,在 insert 的时候的字段验证策略
# IGNORED 忽略 NOT_NULL 非NULL NOT_EMPTY 非空 DEFAULT 默认 NEVER 不加入 SQL
insertStrategy: NOT_NULL
# 字段验证策略之 update,在 update 的时候的字段验证策略
updateStrategy: NOT_NULL
# 字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件
where-strategy: NOT_NULL
# 数据加密
mybatis-encryptor:
# 是否开启加密
enable: false
# 默认加密算法
algorithm: BASE64
# 编码方式 BASE64/HEX。默认BASE64
encode: BASE64
# 安全秘钥 对称算法的秘钥 如AESSM4
password:
# 公私钥 非对称算法的公私钥 如SM2RSA
publicKey:
privateKey:
# Swagger配置
swagger:
info:
# 标题
title: '标题:${ruoyi.name}多租户管理系统_接口文档'
# 描述
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
# 版本
version: '版本号: ${ruoyi.version}'
# 作者信息
contact:
name: ageerle
email: ageerle@163.com
url: https://gitee.com/ageerle/ruoyi-ai
components:
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
springdoc:
api-docs:
# 是否开启接口文档
enabled: true
swagger-ui:
# 持久化认证数据
persistAuthorization: true
#这里定义了两个分组,可定义多个,也可以不定义
group-configs:
- group: 1.演示模块
packages-to-scan: org.ruoyi.demo
- group: 2.通用模块
packages-to-scan: org.ruoyi.web
- group: 3.系统模块
packages-to-scan: org.ruoyi.system
- group: 4.代码生成模块
packages-to-scan: org.ruoyi.generator
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 全局线程池相关配置
thread-pool:
# 是否开启线程池
enabled: false
# 队列最大长度
queueCapacity: 128
# 线程池维护线程所允许的空闲时间
keepAliveSeconds: 300
--- # 分布式锁 lock4j 全局配置
lock4j:
# 获取分布式锁超时时间,默认为 3000 毫秒
acquire-timeout: 3000
# 分布式锁的超时时间,默认为 30 秒
expire: 30000
--- # Actuator 监控端点的配置项
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
logfile:
external-file: ./logs/sys-console.log
# websocket
websocket:
enabled: true
# 路径
path: '/resource/websocket'
# 设置访问源地址
allowedOrigins: '*'
# 微信小程序配置信息
wx:
miniapp:
configs:
- appid: # 你的appid
secret: # 你的secret
token: #微信小程序消息服务器配置的token
aesKey: #微信小程序消息服务器配置的EncodingAESKey
msgDataFormat: JSON
# 企业微信应用
wechat:
cp:
corpId:
appConfigs:
- agentId:
secret: ''
token: ''
aesKey: ''
port: 8081
spring:
ai:
openai:
api-key: sk-xX
base-url: https://api.pandarobot.chat
ollama:
base-url: http://localhost:11434
init:
pull-model-strategy: always
timeout: 60s
max-retries: 1
mcp:
client:
enabled: true
name: call-mcp-server
sse:
connections:
server1:
url: http://127.0.0.1:8080
server:
name: ruoyi-mcp-serve
version: 1.0.0

View File

@@ -16,6 +16,7 @@
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-ai.version>1.0.0-M7</spring-ai.version>
</properties>
<dependencyManagement>
@@ -23,7 +24,7 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0-M6</version>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -32,6 +33,13 @@
<!-- 对话基础模块 -->
<dependencies>
<!-- <dependency>-->
<!-- <groupId>io.modelcontextprotocol.sdk</groupId>-->
<!-- <artifactId>mcp-spring-webflux</artifactId>-->
<!-- <version>0.8.0</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-chat</artifactId>
@@ -55,22 +63,12 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-mcp</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-openai</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>

View File

@@ -65,7 +65,6 @@ public class ChatModelBo extends BaseEntity {
/**
* 系统提示词
*/
@NotBlank(message = "系统提示词不能为空", groups = { AddGroup.class, EditGroup.class })
private String systemPrompt;
/**

View File

@@ -45,7 +45,7 @@ public class KnowledgeFragment extends BaseEntity {
/**
* 片段索引下标
*/
private Long idx;
private Integer idx;
/**
* 文档内容

View File

@@ -0,0 +1,16 @@
package org.ruoyi.domain.bo;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
/**
* @author ageer
*/
@Data
public class KnowledgeInfoUploadBo {
private String kid;
private MultipartFile file;
}

View File

@@ -5,6 +5,7 @@ import org.ruoyi.domain.bo.KnowledgeAttachBo;
import org.ruoyi.domain.vo.KnowledgeAttachVo;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.core.page.PageQuery;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
import java.util.List;
@@ -46,4 +47,17 @@ public interface IKnowledgeAttachService {
* 校验并批量删除知识库附件信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 删除知识附件
*/
void removeKnowledgeAttach(String docId);
/**
* 翻译文件
*
* @param file 文件
* @param targetLanguage 目标语音
*/
String translationByFile(MultipartFile file, String targetLanguage);
}

View File

@@ -2,6 +2,7 @@ package org.ruoyi.service;
import org.ruoyi.domain.bo.KnowledgeInfoBo;
import org.ruoyi.domain.bo.KnowledgeInfoUploadBo;
import org.ruoyi.domain.vo.KnowledgeInfoVo;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.core.page.PageQuery;
@@ -46,4 +47,19 @@ public interface IKnowledgeInfoService {
* 校验并批量删除知识库信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 新增知识库
*/
void saveOne(KnowledgeInfoBo bo);
/**
* 删除知识库
*/
void removeKnowledge(String id);
/**
* 上传附件
*/
void upload(KnowledgeInfoUploadBo bo);
}

View File

@@ -9,13 +9,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.ruoyi.domain.vo.KnowledgeAttachVo;
import org.ruoyi.mapper.KnowledgeFragmentMapper;
import org.springframework.stereotype.Service;
import org.ruoyi.domain.bo.KnowledgeAttachBo;
import org.ruoyi.domain.KnowledgeAttach;
import org.ruoyi.mapper.KnowledgeAttachMapper;
import org.ruoyi.service.IKnowledgeAttachService;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Collection;
@@ -31,6 +34,7 @@ import java.util.Collection;
public class KnowledgeAttachServiceImpl implements IKnowledgeAttachService {
private final KnowledgeAttachMapper baseMapper;
private final KnowledgeFragmentMapper fragmentMapper;
/**
* 查询知识库附件
@@ -111,4 +115,64 @@ public class KnowledgeAttachServiceImpl implements IKnowledgeAttachService {
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public void removeKnowledgeAttach(String docId) {
Map<String,Object> map = new HashMap<>();
map.put("doc_id",docId);
baseMapper.deleteByMap(map);
fragmentMapper.deleteByMap(map);
}
@Override
public String translationByFile(MultipartFile file, String targetLanguage) {
/*String fileName = file.getOriginalFilename();
String docType = fileName.substring(fileName.lastIndexOf(".")+1);
String content = "";
ResourceLoader resourceLoader = resourceLoaderFactory.getLoaderByFileType(docType);
try {
content = resourceLoader.getContent(file.getInputStream());
} catch (IOException e) {
throw new BaseException("该文件类型暂不支持!");
}
// 翻译模型固定为gpt-4o-mini
String model = "gpt-4o-mini";
ChatMessageBo chatMessageBo = new ChatMessageBo();
chatMessageBo.setUserId(getUserId());
chatMessageBo.setModelName(model);
chatMessageBo.setContent(content);
chatMessageBo.setDeductCost(0.01);
chatMessageBo.setTotalTokens(0);
OpenAiStreamClient openAiStreamClient = chatConfig.getOpenAiStreamClient();
List<Message> messageList = new ArrayList<>();
Message sysMessage = Message.builder().role(Message.Role.SYSTEM).content("你是一位精通各国语言的翻译大师\n" +
"\n" +
"请将用户输入词语翻译成{" + targetLanguage + "}\n" +
"\n" +
"==示例输出==\n" +
"**原文** : <这里显示要翻译的原文信息>\n" +
"**翻译** : <这里显示翻译之后的结果>\n" +
"**总结** : <这里是对关键信息一个总结>\n" +
"**提取的关键信息** : <这里返回关键信息>\n" +
"==示例结束==\n" +
"\n" +
"注意请严格按示例进行输出返回markdown格式").build();
messageList.add(sysMessage);
Message message = Message.builder().role(Message.Role.USER).content(content).build();
messageList.add(message);
ChatCompletionResponse chatCompletionResponse = null;
try {
ChatCompletion chatCompletion = ChatCompletion
.builder()
.messages(messageList)
.model(model)
.stream(false)
.build();
chatCompletionResponse = openAiStreamClient.chatCompletion(chatCompletion);
}catch (Exception e) {
throw new BaseException("调用大模型失败,请检查密钥是否正确!");
}
return chatCompletionResponse.getChoices().get(0).getMessage().getContent().toString();*/
return "接口开发中!";
}
}

View File

@@ -1,120 +0,0 @@
package org.ruoyi.service.impl;
import org.ruoyi.common.core.utils.MapstructUtils;
import org.ruoyi.common.core.utils.StringUtils;
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.vo.KnowledgeInfoVo;
import org.springframework.stereotype.Service;
import org.ruoyi.domain.bo.KnowledgeInfoBo;
import org.ruoyi.domain.KnowledgeInfo;
import org.ruoyi.mapper.KnowledgeInfoMapper;
import org.ruoyi.service.IKnowledgeInfoService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
/**
* 知识库Service业务层处理
*
* @author ageerle
* @date 2025-04-08
*/
@RequiredArgsConstructor
@Service
public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
private final KnowledgeInfoMapper baseMapper;
/**
* 查询知识库
*/
@Override
public KnowledgeInfoVo queryById(Long id){
return baseMapper.selectVoById(id);
}
/**
* 查询知识库列表
*/
@Override
public TableDataInfo<KnowledgeInfoVo> queryPageList(KnowledgeInfoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<KnowledgeInfo> lqw = buildQueryWrapper(bo);
Page<KnowledgeInfoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询知识库列表
*/
@Override
public List<KnowledgeInfoVo> queryList(KnowledgeInfoBo bo) {
LambdaQueryWrapper<KnowledgeInfo> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<KnowledgeInfo> buildQueryWrapper(KnowledgeInfoBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<KnowledgeInfo> lqw = Wrappers.lambdaQuery();
lqw.eq(StringUtils.isNotBlank(bo.getKid()), KnowledgeInfo::getKid, bo.getKid());
lqw.eq(bo.getUid() != null, KnowledgeInfo::getUid, bo.getUid());
lqw.like(StringUtils.isNotBlank(bo.getKname()), KnowledgeInfo::getKname, bo.getKname());
lqw.eq(bo.getShare() != null, KnowledgeInfo::getShare, bo.getShare());
lqw.eq(StringUtils.isNotBlank(bo.getDescription()), KnowledgeInfo::getDescription, bo.getDescription());
lqw.eq(StringUtils.isNotBlank(bo.getKnowledgeSeparator()), KnowledgeInfo::getKnowledgeSeparator, bo.getKnowledgeSeparator());
lqw.eq(StringUtils.isNotBlank(bo.getQuestionSeparator()), KnowledgeInfo::getQuestionSeparator, bo.getQuestionSeparator());
lqw.eq(bo.getOverlapChar() != null, KnowledgeInfo::getOverlapChar, bo.getOverlapChar());
lqw.eq(bo.getRetrieveLimit() != null, KnowledgeInfo::getRetrieveLimit, bo.getRetrieveLimit());
lqw.eq(bo.getTextBlockSize() != null, KnowledgeInfo::getTextBlockSize, bo.getTextBlockSize());
lqw.eq(StringUtils.isNotBlank(bo.getVector()), KnowledgeInfo::getVector, bo.getVector());
lqw.eq(StringUtils.isNotBlank(bo.getVectorModel()), KnowledgeInfo::getVectorModel, bo.getVectorModel());
return lqw;
}
/**
* 新增知识库
*/
@Override
public Boolean insertByBo(KnowledgeInfoBo bo) {
KnowledgeInfo add = MapstructUtils.convert(bo, KnowledgeInfo.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改知识库
*/
@Override
public Boolean updateByBo(KnowledgeInfoBo bo) {
KnowledgeInfo update = MapstructUtils.convert(bo, KnowledgeInfo.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(KnowledgeInfo entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除知识库
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
}

View File

@@ -46,4 +46,10 @@ public interface IChatConfigService {
* 校验并批量删除配置信息信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 查询系统参数
*/
List<ChatConfigVo> getSysConfigValue(String category);
}

View File

@@ -129,4 +129,18 @@ public class ChatConfigServiceImpl implements ConfigService, IChatConfigService
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 根据配置类型和配置key获取值
*
* @param category
* @return
*/
@Override
public List<ChatConfigVo> getSysConfigValue(String category) {
ChatConfigBo bo = new ChatConfigBo();
bo.setCategory(category);
LambdaQueryWrapper<ChatConfig> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
}

View File

@@ -113,12 +113,6 @@
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-system-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
<version>1.0.0-M6</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -36,7 +36,7 @@ public class ChatConfig {
return openAiStreamClient;
}
public OpenAiStreamClient createOpenAiStreamClient(String apiHost, String apiKey) {
public static OpenAiStreamClient createOpenAiStreamClient(String apiHost, String apiKey) {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient okHttpClient = new OkHttpClient.Builder()

View File

@@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.common.core.service.ConfigService;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.core.page.TableDataInfo;
@@ -31,11 +32,14 @@ import org.ruoyi.common.log.enums.BusinessType;
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/chatConfig")
@RequestMapping("/chat/config")
public class ChatConfigController extends BaseController {
private final IChatConfigService chatConfigService;
private final ConfigService configService;
/**
* 查询配置信息列表
*/
@@ -102,4 +106,24 @@ public class ChatConfigController extends BaseController {
@PathVariable Long[] ids) {
return toAjax(chatConfigService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 根据参数键名查询系统参数值
*
* @param configKey 参数Key
*/
@GetMapping(value = "/configKey/{configKey}")
public R<String> getConfigKey(@PathVariable String configKey) {
return R.ok(configService.getConfigValue("sys",configKey));
}
/**
* 查询系统参数
*
*/
@GetMapping(value = "/sysConfigKey")
public R<List<ChatConfigVo>> getSysConfigKey() {
return R.ok(chatConfigService.getSysConfigValue("sys"));
}
}

View File

@@ -0,0 +1,44 @@
package org.ruoyi.chat.controller.chat;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.domain.bo.ChatAppStoreBo;
import org.ruoyi.domain.vo.ChatAppStoreVo;
import org.ruoyi.service.IChatAppStoreService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 应用商店
*
* @author Lion Li
* @date 2024-03-19
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/store")
public class ChatStoreController extends BaseController {
private final IChatAppStoreService appStoreService;
/**
* 应用商店
*/
@GetMapping("/appList")
public R<List<ChatAppStoreVo>> appList(ChatAppStoreBo bo) {
return R.ok(appStoreService.queryList(bo));
}
/**
* 收藏应用
*/
@PostMapping("/copyApp")
public R<String> copyApp() {
return R.ok();
}
}

View File

@@ -0,0 +1,154 @@
package org.ruoyi.chat.controller.knowledge;
import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.domain.bo.KnowledgeAttachBo;
import org.ruoyi.domain.bo.KnowledgeFragmentBo;
import org.ruoyi.domain.bo.KnowledgeInfoBo;
import org.ruoyi.domain.bo.KnowledgeInfoUploadBo;
import org.ruoyi.domain.vo.KnowledgeAttachVo;
import org.ruoyi.domain.vo.KnowledgeFragmentVo;
import org.ruoyi.domain.vo.KnowledgeInfoVo;
import org.ruoyi.service.IKnowledgeAttachService;
import org.ruoyi.service.IKnowledgeFragmentService;
import org.ruoyi.service.IKnowledgeInfoService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* @author ageer
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/knowledge")
public class KnowledgeController extends BaseController {
private final IKnowledgeInfoService knowledgeInfoService;
private final IKnowledgeAttachService attachService;
private final IKnowledgeFragmentService fragmentService;
/**
* 根据用户信息查询本地知识库
*/
@GetMapping("/list")
public TableDataInfo<KnowledgeInfoVo> list(KnowledgeInfoBo bo, PageQuery pageQuery) {
if (!StpUtil.isLogin()) {
throw new SecurityException("请先去登录!");
}
bo.setUid(LoginHelper.getUserId());
return knowledgeInfoService.queryPageList(bo, pageQuery);
}
/**
* 新增知识库
*/
@Log(title = "知识库", businessType = BusinessType.INSERT)
@PostMapping("/save")
public R<Void> save(@Validated(AddGroup.class) @RequestBody KnowledgeInfoBo bo) {
knowledgeInfoService.saveOne(bo);
return R.ok();
}
/**
* 删除知识库
*/
@PostMapping("/remove/{id}")
public R<String> remove(@PathVariable String id) {
knowledgeInfoService.removeKnowledge(id);
return R.ok("删除知识库成功!");
}
/**
* 修改知识库
*/
@Log(title = "知识库", businessType = BusinessType.UPDATE)
@PostMapping("/edit")
public R<Void> edit(@RequestBody KnowledgeInfoBo bo) {
return toAjax(knowledgeInfoService.updateByBo(bo));
}
/**
* 导出知识库列表
*/
@Log(title = "知识库", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(KnowledgeInfoBo bo, HttpServletResponse response) {
List<KnowledgeInfoVo> list = knowledgeInfoService.queryList(bo);
ExcelUtil.exportExcel(list, "知识库", KnowledgeInfoVo.class, response);
}
/**
* 查询知识附件信息
*/
@GetMapping("/detail/{kid}")
public TableDataInfo<KnowledgeAttachVo> attach(KnowledgeAttachBo bo, PageQuery pageQuery, @PathVariable String kid) {
bo.setKid(kid);
return attachService.queryPageList(bo, pageQuery);
}
/**
* 上传知识库附件
*/
@PostMapping(value = "/attach/upload")
public R<String> upload(KnowledgeInfoUploadBo bo) {
knowledgeInfoService.upload(bo);
return R.ok("上传知识库附件成功!");
}
/**
* 获取知识库附件详细信息
*
* @param id 主键
*/
@GetMapping("attach/info/{id}")
public R<KnowledgeAttachVo> getAttachInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(attachService.queryById(id));
}
/**
* 删除知识库附件
*/
@PostMapping("attach/remove/{kid}")
public R<Void> removeAttach(@NotEmpty(message = "主键不能为空")
@PathVariable String kid) {
attachService.removeKnowledgeAttach(kid);
return R.ok();
}
/**
* 查询知识片段
*/
@GetMapping("/fragment/list/{docId}")
public TableDataInfo<KnowledgeFragmentVo> fragmentList(KnowledgeFragmentBo bo, PageQuery pageQuery, @PathVariable String docId) {
bo.setDocId(docId);
return fragmentService.queryPageList(bo, pageQuery);
}
/**
* 上传文件翻译
*/
@PostMapping("/translationByFile")
@ResponseBody
public String translationByFile(@RequestParam("file") MultipartFile file, String targetLanguage) {
return attachService.translationByFile(file, targetLanguage);
}
}

View File

@@ -0,0 +1,26 @@
package org.ruoyi.chat.enums;
import lombok.Getter;
@Getter
public enum ChatModeType {
OLLAMA("ollama", "本地部署模型"), // token扣费
CHAT("chat", "中转模型"), // 次数扣费
VECTOR("vector", "知识库向量模型"); // 次数扣费
private final String code;
private final String description;
ChatModeType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
}

View File

@@ -18,7 +18,7 @@ import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Objects;
@@ -34,13 +34,13 @@ import java.util.Objects;
public class SSEEventSourceListener extends EventSourceListener {
@Autowired(required = false)
public SSEEventSourceListener(ResponseBodyEmitter emitter) {
public SSEEventSourceListener(SseEmitter emitter) {
this.emitter = emitter;
}
private ResponseBodyEmitter emitter;
private SseEmitter emitter;
private StringBuilder stringBuffer;
private StringBuilder stringBuffer = new StringBuilder();
private String modelName;
@@ -61,7 +61,6 @@ public class SSEEventSourceListener extends EventSourceListener {
@Override
public void onEvent(@NotNull EventSource eventSource, String id, String type, String data) {
try {
if ("[DONE]".equals(data)) {
//成功响应
emitter.complete();
@@ -72,25 +71,23 @@ public class SSEEventSourceListener extends EventSourceListener {
chatCostService.deductToken(chatRequest);
return;
}
// 解析返回内容
ObjectMapper mapper = new ObjectMapper();
ChatCompletionResponse completionResponse = mapper.readValue(data, ChatCompletionResponse.class);
if(completionResponse == null || CollectionUtil.isEmpty(completionResponse.getChoices())){
return;
}
Object content = completionResponse.getChoices().get(0).getDelta().getContent();
if(content == null){
content = completionResponse.getChoices().get(0).getDelta().getReasoningContent();
if(content == null) return;
if(content != null ){
if(StringUtils.isEmpty(modelName)){
modelName = completionResponse.getModel();
}
stringBuffer.append(content);
emitter.send(content);
}
if(StringUtils.isEmpty(modelName)){
modelName = completionResponse.getModel();
}
stringBuffer.append(content);
emitter.send(data);
} catch (Exception e) {
log.error("sse信息推送失败{}内容:{}",e.getMessage(),data);
eventSource.cancel();
emitter.completeWithError(e);
}
}

View File

@@ -40,4 +40,9 @@ public interface IChatCostService {
* 判断用户是否付费
*/
void checkUserGrade();
/**
* 获取登录用户id
*/
Long getUserId();
}

View File

@@ -18,9 +18,4 @@ public interface IChatService {
SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter);
/**
* 客户端发送消息到服务端
* @param chatRequest 请求对象
*/
SseEmitter mcpChat(ChatRequest chatRequest,SseEmitter emitter);
}

View File

@@ -55,11 +55,4 @@ public interface ISseService {
*/
String wxCpChat(String prompt);
/**
* 联网查询
*
* @param prompt 提示词
* @return 查询内容
*/
String webSearch (String prompt);
}

View File

@@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.enums.BillingType;
import org.ruoyi.chat.enums.UserGradeType;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.common.chat.config.LocalCache;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.common.chat.utils.TikTokensUtil;
import org.ruoyi.common.core.domain.model.LoginUser;
@@ -53,8 +54,14 @@ public class ChatCostServiceImpl implements IChatCostService {
ChatMessageBo chatMessageBo = new ChatMessageBo();
Object userId = LocalCache.CACHE.get("userId");
if(userId!=null){
chatMessageBo.setUserId((Long) userId);
}else {
chatMessageBo.setUserId(getUserId());
}
// 计算总token数
ChatToken chatToken = chatTokenService.queryByUserId(getUserId(), modelName);
ChatToken chatToken = chatTokenService.queryByUserId(chatMessageBo.getUserId(), modelName);
if (chatToken == null) {
chatToken = new ChatToken();
chatToken.setToken(0);
@@ -68,17 +75,17 @@ public class ChatCostServiceImpl implements IChatCostService {
if (token2 > 0) {
// 保存剩余tokens
chatToken.setModelName(modelName);
chatToken.setUserId(getUserId());
chatToken.setUserId(chatMessageBo.getUserId());
chatToken.setToken(token2);
chatTokenService.editToken(chatToken);
} else {
chatTokenService.resetToken(getUserId(), modelName);
chatTokenService.resetToken(chatMessageBo.getUserId(), modelName);
}
ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName);
double cost = chatModelVo.getModelPrice();
if (BillingType.TIMES.getCode().equals(chatModelVo.getModelType())) {
// 按次数扣费
deductUserBalance(getUserId(), cost);
deductUserBalance(chatMessageBo.getUserId(), cost);
chatMessageBo.setDeductCost(cost);
}else {
// 按token扣费
@@ -88,7 +95,7 @@ public class ChatCostServiceImpl implements IChatCostService {
}
chatMessageBo.setContent(chatRequest.getPrompt());
} else {
deductUserBalance(getUserId(), 0.0);
deductUserBalance(chatMessageBo.getUserId(), 0.0);
chatMessageBo.setDeductCost(0d);
chatMessageBo.setRemark("不满1kToken,计入下一次!");
chatToken.setToken(totalTokens);

View File

@@ -7,24 +7,13 @@ import io.github.ollama4j.models.chat.OllamaChatRequestBuilder;
import io.github.ollama4j.models.chat.OllamaChatRequestModel;
import io.github.ollama4j.models.generate.OllamaStreamHandler;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.chat.util.SSEUtil;
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.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.ollama.api.OllamaModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
@@ -35,27 +24,11 @@ import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
public class OllamaServiceImpl implements IChatService {
public class OllamaServiceImpl {
@Autowired
private IChatModelService chatModelService;
@Autowired
private IChatModelService chatModelService;
private final ChatClient chatClient;
private final ChatMemory chatMemory = new InMemoryChatMemory();
public OllamaServiceImpl(ChatClient.Builder chatClientBuilder,ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
.defaultTools(tools)
.defaultOptions(
OllamaOptions.builder()
.model(OllamaModel.QWEN_2_5_7B)
.temperature(0.4)
.build())
.build();
}
@Override
public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
String host = chatModelVo.getApiHost();
@@ -100,44 +73,4 @@ public class OllamaServiceImpl implements IChatService {
return emitter;
}
@Override
public SseEmitter mcpChat(ChatRequest chatRequest, SseEmitter emitter) {
List<Message> msgList = chatRequest.getMessages();
// 添加记忆
for (int i = 0; i < msgList.size(); i++) {
org.springframework.ai.chat.messages.Message springAiMessage = new UserMessage(msgList.get(i).getContent().toString());
chatMemory.add(String.valueOf(i),springAiMessage);
}
var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, chatRequest.getUserId(), 10);
this.chatClient.prompt(chatRequest.getPrompt())
.advisors(messageChatMemoryAdvisor)
.stream()
.chatResponse()
.subscribe(
chatResponse -> {
try {
emitter.send(chatResponse, MediaType.APPLICATION_JSON);
} catch (IOException e) {
e.printStackTrace();
}
},
error -> {
try {
emitter.completeWithError(error);
} catch (Exception e) {
e.printStackTrace();
}
},
() -> {
try {
emitter.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
);
return emitter;
}
}

View File

@@ -0,0 +1,65 @@
package org.ruoyi.chat.service.chat.impl;
import io.modelcontextprotocol.client.McpSyncClient;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.chat.listener.SSEEventSourceListener;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.service.IChatModelService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
@Service
@Slf4j
public class OpenAIServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
private OpenAiStreamClient openAiStreamClient;
private final ChatClient chatClient;
public OpenAIServiceImpl(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpSyncClients) {
this.chatClient = chatClientBuilder
.defaultOptions(
OpenAiChatOptions.builder().model("gpt-4o-mini").build())
.defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients))
.build();
}
@Override
public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
String toolString = mcpChat(chatRequest.getPrompt());
Message userMessage = Message.builder().content("工具返回信息:"+toolString).role(Message.Role.USER).build();
List<Message> messages = chatRequest.getMessages();
messages.add(userMessage);
SSEEventSourceListener listener = new SSEEventSourceListener(emitter);
ChatCompletion completion = ChatCompletion
.builder()
.messages(messages)
.model(chatRequest.getModel())
.stream(true)
.build();
openAiStreamClient.streamChatCompletion(completion, listener);
return emitter;
}
public String mcpChat(String prompt){
return this.chatClient.prompt(prompt).call().content();
}
}

View File

@@ -2,38 +2,32 @@ package org.ruoyi.chat.service.chat.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.ServiceException;
import com.zhipu.oapi.ClientV4;
import com.zhipu.oapi.service.v4.tools.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import okhttp3.ResponseBody;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.chat.service.chat.ISseService;
import org.ruoyi.chat.util.IpUtil;
import org.ruoyi.chat.util.SSEUtil;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.common.chat.config.LocalCache;
import org.ruoyi.common.chat.entity.Tts.TextToSpeech;
import org.ruoyi.common.chat.entity.chat.ChatCompletion;
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
import org.ruoyi.common.chat.entity.chat.Message;
import org.ruoyi.common.chat.entity.files.UploadFileResponse;
import org.ruoyi.common.chat.entity.whisper.WhisperResponse;
import org.ruoyi.common.chat.openai.OpenAiStreamClient;
import org.ruoyi.common.core.service.ConfigService;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.common.core.utils.DateUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.core.utils.file.FileUtils;
import org.ruoyi.common.core.utils.file.MimeTypeUtils;
import org.ruoyi.common.redis.utils.RedisUtils;
import org.ruoyi.domain.vo.ChatModelVo;
import org.ruoyi.service.EmbeddingService;
import org.ruoyi.service.IChatModelService;
import org.ruoyi.service.VectorStoreService;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
@@ -51,11 +45,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Service
@Slf4j
@@ -68,33 +58,38 @@ public class SseServiceImpl implements ISseService {
private final VectorStoreService vectorStore;
private final ConfigService configService;
private final IChatCostService chatCostService;
private final IChatService chatService;
private final IChatModelService chatModelService;
private final OpenAIServiceImpl openAIService;
private static final String requestIdTemplate = "company-%d";
private final OllamaServiceImpl ollamaService;
private ChatModelVo chatModelVo;
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
SseEmitter sseEmitter = new SseEmitter(0L);
try {
// 构建消息列表增加联网、知识库等内容
// 构建消息列表
buildChatMessageList(chatRequest);
if (!StpUtil.isLogin()) {
// 未登录用户限制对话次数
checkUnauthenticatedUserChatLimit(request);
}else {
LocalCache.CACHE.put("userId", chatCostService.getUserId());
chatRequest.setUserId(chatCostService.getUserId());
// 保存消息记录 并扣除费用
chatCostService.deductToken(chatRequest);
}
// 根据模型名称前缀调用不同的处理逻辑
switchModelAndHandle(chatRequest,sseEmitter);
// 未登录用户限制对话次数
checkUnauthenticatedUserChatLimit(request);
// 保存消息记录 并扣除费用
chatCostService.deductToken(chatRequest);
} catch (Exception e) {
String message = e.getMessage();
SSEUtil.sendErrorEvent(sseEmitter, message);
return sseEmitter;
log.error(e.getMessage(),e);
sseEmitter.completeWithError(e);
}
return sseEmitter;
}
@@ -106,8 +101,7 @@ public class SseServiceImpl implements ISseService {
* @throws ServiceException 如果当日免费次数已用完
*/
public void checkUnauthenticatedUserChatLimit(HttpServletRequest request) throws ServiceException {
// 未登录用户限制对话次数
if (!StpUtil.isLogin()) {
String clientIp = IpUtil.getClientIp(request);
// 访客每天默认只能对话5次
int timeWindowInSeconds = 5;
@@ -125,25 +119,18 @@ public class SseServiceImpl implements ISseService {
count++;
RedisUtils.setCacheObject(redisKey, count);
}
}
}
/**
* 根据模型名称前缀调用不同的处理逻辑
*/
private void switchModelAndHandle(ChatRequest chatRequest,SseEmitter emitter) {
String model = chatRequest.getModel();
// 如果模型名称以ollama开头则调用ollama中部署的本地模型
if (model.startsWith("ollama-")) {
String[] parts = chatRequest.getModel().split("ollama-", 2);
if (parts.length > 1) {
chatRequest.setModel(parts[1]);
chatService.mcpChat(chatRequest,emitter);
} else {
throw new IllegalArgumentException("Invalid ollama model name: " + chatRequest.getModel());
}
} else if (model.startsWith("gpt-4-gizmo")) {
chatRequest.setModel("gpt-4-gizmo");
// 调用ollama中部署的本地模型
if (ChatModeType.OLLAMA.getCode().equals(chatModelVo.getCategory())) {
ollamaService.chat(chatRequest,emitter);
} else {
openAIService.chat(chatRequest,emitter);
}
}
@@ -151,19 +138,23 @@ public class SseServiceImpl implements ISseService {
* 构建消息列表
*/
private void buildChatMessageList(ChatRequest chatRequest){
chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
// 获取对话消息列表
List<Message> messages = chatRequest.getMessages();
String sysPrompt = chatRequest.getSysPrompt();
String sysPrompt = chatModelVo.getSystemPrompt();
if(StringUtils.isEmpty(sysPrompt)){
sysPrompt ="你是一个由RuoYI-AI开发的人工智能助手名字叫熊猫助手。你擅长中英文对话能够理解并处理各种问题提供安全、有帮助、准确的回答。" +
"当前时间:"+ DateUtils.getDate();
"当前时间:"+ DateUtils.getDate()+
"#注意:回复之前注意结合上下文和工具返回内容。";
}
// 设置系统默认提示词
Message sysMessage = Message.builder().content(sysPrompt).role(Message.Role.SYSTEM).build();
messages.add(0,sysMessage);
chatRequest.setSysPrompt(sysPrompt);
// 查询向量库相关信息加入到上下文
if(chatRequest.getKid()!=null){
if(StringUtils.isNotEmpty(chatRequest.getKid())){
List<Message> knMessages = new ArrayList<>();
String content = messages.get(messages.size() - 1).getContent().toString();
List<String> nearestList;
@@ -190,11 +181,6 @@ public class SseServiceImpl implements ISseService {
}
// 设置对话信息
chatRequest.setPrompt(chatString);
// 加载联网信息
if(chatRequest.getSearch()){
Message message = Message.builder().role(Message.Role.ASSISTANT).content("联网信息:"+webSearch(chatString)).build();
messages.add(message);
}
}
@@ -303,58 +289,4 @@ public class SseServiceImpl implements ISseService {
return chatCompletionResponse.getChoices().get(0).getMessage().getContent().toString();
}
@Override
public String webSearch (String prompt) {
String zpValue = configService.getConfigValue("zhipu", "key");
if(StringUtils.isEmpty(zpValue)){
throw new IllegalStateException("请在chat_config中配置智谱key信息");
}else {
ClientV4 client = new ClientV4.Builder(zpValue)
.networkConfig(300, 100, 100, 100, TimeUnit.SECONDS)
.connectionPool(new okhttp3.ConnectionPool(8, 1, TimeUnit.SECONDS))
.build();
SearchChatMessage jsonNodes = new SearchChatMessage();
jsonNodes.setRole(Message.Role.USER.getName());
jsonNodes.setContent(prompt);
String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
WebSearchParamsRequest chatCompletionRequest = WebSearchParamsRequest.builder()
.model("web-search-pro")
.stream(Boolean.TRUE)
.messages(Collections.singletonList(jsonNodes))
.requestId(requestId)
.build();
WebSearchApiResponse webSearchApiResponse = client.webSearchProStreamingInvoke(chatCompletionRequest);
List<ChoiceDelta> choices = new ArrayList<>();
if (webSearchApiResponse.isSuccess()) {
AtomicBoolean isFirst = new AtomicBoolean(true);
AtomicReference<WebSearchPro> lastAccumulator = new AtomicReference<>();
webSearchApiResponse.getFlowable().map(result -> result)
.doOnNext(accumulator -> {
{
if (isFirst.getAndSet(false)) {
log.info("Response: ");
}
ChoiceDelta delta = accumulator.getChoices().get(0).getDelta();
if (delta != null && delta.getToolCalls() != null) {
log.info("tool_calls: {}", mapper.writeValueAsString(delta.getToolCalls()));
}
choices.add(delta);
}
})
.doOnComplete(() -> System.out.println("Stream completed."))
.doOnError(throwable -> System.err.println("Error: " + throwable))
.blockingSubscribe();
WebSearchPro chatMessageAccumulator = lastAccumulator.get();
webSearchApiResponse.setFlowable(null);
webSearchApiResponse.setData(chatMessageAccumulator);
}
return choices.get(1).getToolCalls().toString();
}
}
}

View File

@@ -0,0 +1,232 @@
package org.ruoyi.chat.service.knowledge;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.ruoyi.chain.loader.ResourceLoader;
import org.ruoyi.chain.loader.ResourceLoaderFactory;
import org.ruoyi.common.core.domain.model.LoginUser;
import org.ruoyi.common.core.utils.MapstructUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.domain.KnowledgeAttach;
import org.ruoyi.domain.KnowledgeFragment;
import org.ruoyi.domain.KnowledgeInfo;
import org.ruoyi.domain.bo.KnowledgeInfoBo;
import org.ruoyi.domain.bo.KnowledgeInfoUploadBo;
import org.ruoyi.domain.vo.KnowledgeInfoVo;
import org.ruoyi.mapper.KnowledgeAttachMapper;
import org.ruoyi.mapper.KnowledgeFragmentMapper;
import org.ruoyi.mapper.KnowledgeInfoMapper;
import org.ruoyi.service.EmbeddingService;
import org.ruoyi.service.IKnowledgeInfoService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.*;
/**
* 知识库Service业务层处理
*
* @author ageerle
* @date 2025-04-08
*/
@RequiredArgsConstructor
@Service
public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
private final KnowledgeInfoMapper baseMapper;
private final EmbeddingService embeddingService;
private final ResourceLoaderFactory resourceLoaderFactory;
private final KnowledgeFragmentMapper fragmentMapper;
private final KnowledgeAttachMapper attachMapper;
/**
* 查询知识库
*/
@Override
public KnowledgeInfoVo queryById(Long id){
return baseMapper.selectVoById(id);
}
/**
* 查询知识库列表
*/
@Override
public TableDataInfo<KnowledgeInfoVo> queryPageList(KnowledgeInfoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<KnowledgeInfo> lqw = buildQueryWrapper(bo);
Page<KnowledgeInfoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询知识库列表
*/
@Override
public List<KnowledgeInfoVo> queryList(KnowledgeInfoBo bo) {
LambdaQueryWrapper<KnowledgeInfo> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<KnowledgeInfo> buildQueryWrapper(KnowledgeInfoBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<KnowledgeInfo> lqw = Wrappers.lambdaQuery();
lqw.eq(StringUtils.isNotBlank(bo.getKid()), KnowledgeInfo::getKid, bo.getKid());
lqw.eq(bo.getUid() != null, KnowledgeInfo::getUid, bo.getUid());
lqw.like(StringUtils.isNotBlank(bo.getKname()), KnowledgeInfo::getKname, bo.getKname());
lqw.eq(bo.getShare() != null, KnowledgeInfo::getShare, bo.getShare());
lqw.eq(StringUtils.isNotBlank(bo.getDescription()), KnowledgeInfo::getDescription, bo.getDescription());
lqw.eq(StringUtils.isNotBlank(bo.getKnowledgeSeparator()), KnowledgeInfo::getKnowledgeSeparator, bo.getKnowledgeSeparator());
lqw.eq(StringUtils.isNotBlank(bo.getQuestionSeparator()), KnowledgeInfo::getQuestionSeparator, bo.getQuestionSeparator());
lqw.eq(bo.getOverlapChar() != null, KnowledgeInfo::getOverlapChar, bo.getOverlapChar());
lqw.eq(bo.getRetrieveLimit() != null, KnowledgeInfo::getRetrieveLimit, bo.getRetrieveLimit());
lqw.eq(bo.getTextBlockSize() != null, KnowledgeInfo::getTextBlockSize, bo.getTextBlockSize());
lqw.eq(StringUtils.isNotBlank(bo.getVector()), KnowledgeInfo::getVector, bo.getVector());
lqw.eq(StringUtils.isNotBlank(bo.getVectorModel()), KnowledgeInfo::getVectorModel, bo.getVectorModel());
return lqw;
}
/**
* 新增知识库
*/
@Override
public Boolean insertByBo(KnowledgeInfoBo bo) {
KnowledgeInfo add = MapstructUtils.convert(bo, KnowledgeInfo.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改知识库
*/
@Override
public Boolean updateByBo(KnowledgeInfoBo bo) {
KnowledgeInfo update = MapstructUtils.convert(bo, KnowledgeInfo.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(KnowledgeInfo entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除知识库
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOne(KnowledgeInfoBo bo) {
KnowledgeInfo knowledgeInfo = MapstructUtils.convert(bo, KnowledgeInfo.class);
if (StringUtils.isBlank(bo.getKid())){
String kid = RandomUtil.randomString(10);
if (knowledgeInfo != null) {
knowledgeInfo.setKid(kid);
knowledgeInfo.setUid(LoginHelper.getLoginUser().getUserId());
}
baseMapper.insert(knowledgeInfo);
embeddingService.createSchema(String.valueOf(knowledgeInfo.getId()));
}else {
baseMapper.updateById(knowledgeInfo);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeKnowledge(String id) {
Map<String,Object> map = new HashMap<>();
map.put("kid",id);
List<KnowledgeInfoVo> knowledgeInfoList = baseMapper.selectVoByMap(map);
check(knowledgeInfoList);
// 删除向量库信息
knowledgeInfoList.forEach(knowledgeInfoVo -> {
embeddingService.removeByKid(String.valueOf(knowledgeInfoVo.getId()));
});
// 删除附件和知识片段
fragmentMapper.deleteByMap(map);
attachMapper.deleteByMap(map);
// 删除知识库
baseMapper.deleteByMap(map);
}
@Override
public void upload(KnowledgeInfoUploadBo bo) {
storeContent(bo.getFile(), bo.getKid());
}
public void storeContent(MultipartFile file, String kid) {
String fileName = file.getOriginalFilename();
List<String> chunkList = new ArrayList<>();
KnowledgeAttach knowledgeAttach = new KnowledgeAttach();
knowledgeAttach.setKid(kid);
String docId = RandomUtil.randomString(10);
knowledgeAttach.setDocId(docId);
knowledgeAttach.setDocName(fileName);
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());
chunkList = resourceLoader.getChunkList(content, kid);
List<KnowledgeFragment> knowledgeFragmentList = new ArrayList<>();
if (CollUtil.isNotEmpty(chunkList)) {
for (int i = 0; i < chunkList.size(); i++) {
String fid = RandomUtil.randomString(16);
fids.add(fid);
KnowledgeFragment knowledgeFragment = new KnowledgeFragment();
knowledgeFragment.setKid(kid);
knowledgeFragment.setDocId(docId);
knowledgeFragment.setFid(fid);
knowledgeFragment.setIdx(i);
knowledgeFragment.setContent(chunkList.get(i));
knowledgeFragment.setCreateTime(new Date());
knowledgeFragmentList.add(knowledgeFragment);
}
}
fragmentMapper.insertBatch(knowledgeFragmentList);
} catch (IOException e) {
e.printStackTrace();
}
knowledgeAttach.setContent(content);
knowledgeAttach.setCreateTime(new Date());
attachMapper.insert(knowledgeAttach);
embeddingService.storeEmbeddings(chunkList,kid,docId,fids);
}
public void check(List<KnowledgeInfoVo> knowledgeInfoList){
LoginUser loginUser = LoginHelper.getLoginUser();
for (KnowledgeInfoVo knowledgeInfoVo : knowledgeInfoList) {
if(!knowledgeInfoVo.getUid().equals(loginUser.getUserId())){
throw new SecurityException("权限不足");
}
}
}
}

View File

@@ -1,6 +1,7 @@
package org.ruoyi.chat.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
@@ -19,7 +20,7 @@ public class SSEUtil {
* @param sseEmitter sse事件对象
* @param errorMessage 错误信息
*/
public static void sendErrorEvent(SseEmitter sseEmitter, String errorMessage) {
public static void sendErrorEvent(ResponseBodyEmitter sseEmitter, String errorMessage) {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.name("error")
.data(errorMessage);

Some files were not shown because too many files have changed in this diff Show More