25 Commits

Author SHA1 Message Date
ageer
5476a4b0b7 fix: 公众号登录功能同步 2025-04-30 19:34:09 +08:00
ageerle
cd490aa0e5 feat: 新增wechat模块 2025-04-30 11:29:23 +08:00
ageerle
b1ff44df4b feat: sse对话样式优化 2025-04-29 11:44:47 +08:00
ageerle
dc6d00f0fc feat: 查询gpts无需登录 2025-04-29 11:00:34 +08:00
ageerle
990da8da6f feat: 增加工具调用超时时间配置 2025-04-24 17:09:52 +08:00
ageerle
8fc7ad0359 feat: mcp开启后才执行工具调用 2025-04-24 10:31:58 +08:00
ageer
8a89d9eb9c fix: 1.合并知识库对话接口 2.修复web无法查询套餐信息 2025-04-23 23:10:31 +08:00
ageer
abfa9ae97b fix: 1.合并知识库对话接口 2.修复web无法查询套餐信息 2025-04-23 23:09:56 +08:00
ageer
bde2e8ff8e fix: 1.合并知识库对话接口 2.修复web无法查询套餐信息 2025-04-23 23:09:22 +08:00
ageer
4d8eb1850f feat: 附件上传逻辑调整 2025-04-22 21:43:40 +08:00
ageer
5aaa3a5662 fix: 1. 修复No static resource tool/gen/list. 2. 修复新增数据库校验异常 2025-04-22 21:24:00 +08:00
ageerle
8fec96f5b2 feat: 默认关闭mcp功能 2025-04-22 18:01:40 +08:00
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
ageerle
ab2a118ee9 feat: 更新项目说明 2025-04-18 11:11:48 +08:00
lh
744f9b6c7f 更新readme.md 2025-04-17 11:09:00 +08:00
lh
b50c15755d 更新readme.md 2025-04-12 14:19:51 +08:00
lh
09abc0b5af 新增讨论群信息 2025-04-08 14:05:05 +08:00
85 changed files with 2127 additions and 783 deletions

142
README.md
View File

@@ -36,48 +36,85 @@
## 目录
- [系统体验](#系统体验)
- [源码地址](#源码地址)
- [特色功能](#特色功能)
- [配套文档](#项目文档)
- [核心功能](#核心功能)
- [项目演示](#项目演示)
- [后台管理](#后台管理)
- [管理](#管理)
- [用户端](#用户端)
- [小程序端](#小程序端)
- [开发前的配置要求](#开发前的配置要求)
- [文件目录说明](#文件目录说明)
- [使用到的框架](#使用到的框架)
- [开发环境](#开发环境)
- [项目结构](#项目结构)
- [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
### 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
[1]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
### 特色功能
[2]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
[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支持
### 如何使用
1. ruoyi-admin\src\main\resources\application.yml中mcp.client.enabled改为true
2. application.yml中配置openai api-key(用于推理使用那个工具,并构建工具所需参数)
3. 启动[ruoyi-mcp-server]
4. [mcp-server.json]中配置fileSystem.command(npx本地安装路径)
5. 指定fileSystem操作目录(本地必须存在指定的目录)
6. 配置search1api.env.SEARCH1API_KEY 申请地址https://www.search1api.com/
7. 详情教程https://blog.csdn.net/weixin_42416319/article/details/147385808
<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 +137,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 +145,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 +201,14 @@ RuoYi-AI
```
### 注意事项
- vben模板
Qvben5 的模板默认是没有的吗?
Avben模板是收费的 请联系vben-vue-plus作者获取。
### 版本控制
该项目使用Git进行版本管理。您可以在repository参看当前可用版本。
@@ -170,20 +221,16 @@ RuoYi-AI
### 项目现状
目前项目还处于早期阶段距离成熟还有很长的路要走。由于个人精力有限项目的发展速度受到了一定的限制。为了加快项目的进度我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者还是刚刚入门的小白我都热烈欢迎你们提交Pull RequestPR。即使代码修改得很少或者存在一些错误都没有关系。我会认真审核每一位贡献者的代码并和大家一起完善项目。
目前项目还处于早期阶段距离成熟还有很长的路要走。由于个人精力有限项目的发展速度受到了一定的限制。为了加快项目的进度我真诚地希望更多人能够参与到项目中来。无论是经验丰富的开发者还是刚刚入门的小白我都热烈欢迎你们提交Pull RequestPR👏👏👏。即使代码修改得很少,或者存在一些错误,都没有关系。我会认真审核每一位贡献者的代码,并和大家一起完善项目⛽️⛽️⛽️
### 开发计划
- 智能体管理
| 主题 | 方向 | 时间节点 |
| --- |-----------------------------------|--------|
| 前端简化版 | 与element-plus-x框架合作推出基于该框架的前端简化版 | 2025.5 |
| agent2agent | Agent2Agent协议支持 | 2025.6 |
| 流程编排 | 通过可视化界面和灵活的配置方式快速构建AI应用 | 2025.7 |
通过设置提示词、插件、知识库等用户可以快速构建一个AI应用。这将极大地简化AI应用的开发流程降低开发门槛使更多企业能够轻松地利用AI技术。
<div>
<img src="image/13.png" alt="drawing" width="600px" height="300px"/>
</div>
- 流程编排
通过流程编排功能,用户可以将不同的模型按照业务逻辑进行有序连接。这将解决单一模型能力不足的问题,充分发挥多个模型的协同作用,从而更好地满足企业的复杂业务需求。
- 感谢
@@ -193,7 +240,7 @@ RuoYi-AI
#### 如何参与开源项目
贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献都是**非常感谢**的。
贡献使开源社区成为一个学习、激励和创造的绝佳场所。你所作的任何贡献,我们都非常感谢!🙏
1. Fork 这个项目
2. 创建你的功能分支 (`git checkout -b feature/dev`)
@@ -231,4 +278,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: 227 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

10
pom.xml
View File

@@ -337,11 +337,11 @@
<version>${revision}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.ruoyi</groupId>-->
<!-- <artifactId>ruoyi-demo</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-wechat</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -42,12 +42,6 @@
<artifactId>mssql-jdbc</artifactId>
</dependency>
<!-- demo模块 -->
<!-- <dependency>-->
<!-- <groupId>org.ruoyi</groupId>-->
<!-- <artifactId>ruoyi-demo</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
@@ -58,6 +52,16 @@
<artifactId>ruoyi-chat</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-generator</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-wechat</artifactId>
</dependency>
</dependencies>
<build>

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

@@ -25,7 +25,7 @@ spring:
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
url: jdbc:mysql://127.0.0.1:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
username: ry-vue
password: xx

View File

@@ -323,11 +323,11 @@ wechat:
spring:
ai:
openai:
api-key: sk-xxx
api-key: sk-xx
base-url: https://api.pandarobot.chat/
mcp:
client:
enabled: true
enabled: false
name: ruoyi-ai-mcp
sse:
connections:
@@ -335,4 +335,5 @@ spring:
url: http://127.0.0.1:8081
stdio:
servers-configuration: classpath:mcp-server.json
request-timeout: 300s

View File

@@ -5,7 +5,7 @@
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"D:\\software"
"D:\\"
]
},
"search1api": {
@@ -15,7 +15,7 @@
"search1api-mcp"
],
"env": {
"SEARCH1API_KEY": "92A3D8F1-9BFA-485A-90E9-7680914CB666"
"SEARCH1API_KEY": "xx"
}
}
}

View File

@@ -1,2 +0,0 @@
/mvnw text eol=lf
*.cmd text eol=crlf

View File

@@ -1,19 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

View File

@@ -1,19 +0,0 @@
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.4/maven-plugin)
* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.4/maven-plugin/build-image.html)
* [Model Context Protocol Server](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html)
### Maven Parent overrides
Due to Maven's design, elements are inherited from the parent POM to the project POM.
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the
parent.
To prevent this, the project POM contains empty overrides for these elements.
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.

View File

@@ -1,259 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

View File

@@ -1,149 +0,0 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

View File

@@ -1,13 +0,0 @@
package org.ruoyi.mcpserve;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class RuoyiMcpServeApplicationTests {
@Test
void contextLoads() {
}
}

View File

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

View File

@@ -0,0 +1,19 @@
package org.ruoyi.service;
/**
* 企业微信聊天管理Service接口
*
* @author ageerle
* @date 2025-04-08
*/
public interface IChatVxService {
/**
* 企业微信应用回复
* @param prompt 提示词
* @return 回复内容
*/
String chat(String prompt);
}

View File

@@ -0,0 +1,37 @@
package org.ruoyi.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.openai.OpenAiStreamClient;
import org.ruoyi.service.IChatVxService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
@RequiredArgsConstructor
public class ChatVxServiceImpl implements IChatVxService {
private final OpenAiStreamClient openAiStreamClient;
@Override
public String chat(String prompt) {
List<Message> messageList = new ArrayList<>();
Message message = Message.builder().role(Message.Role.USER).content(prompt).build();
messageList.add(message);
ChatCompletion chatCompletion = ChatCompletion
.builder()
.messages(messageList)
.model("gpt-4o-mini")
.stream(false)
.build();
ChatCompletionResponse chatCompletionResponse = openAiStreamClient.chatCompletion(chatCompletion);
return chatCompletionResponse.getChoices().get(0).getMessage().getContent().toString();
}
}

View File

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

View File

@@ -21,7 +21,7 @@ import jakarta.validation.constraints.*;
public class KnowledgeInfoBo extends BaseEntity {
/**
*
* 主键
*/
@NotNull(message = "不能为空", groups = { EditGroup.class })
private Long id;
@@ -29,13 +29,13 @@ public class KnowledgeInfoBo extends BaseEntity {
/**
* 知识库ID
*/
@NotBlank(message = "知识库ID不能为空", groups = { AddGroup.class, EditGroup.class })
@NotBlank(message = "知识库ID不能为空", groups = {EditGroup.class })
private String kid;
/**
* 用户ID
*/
@NotNull(message = "用户ID不能为空", groups = { AddGroup.class, EditGroup.class })
@NotNull(message = "用户ID不能为空", groups = {EditGroup.class })
private Long uid;
/**
@@ -53,25 +53,21 @@ public class KnowledgeInfoBo extends BaseEntity {
/**
* 描述
*/
@NotBlank(message = "描述不能为空", groups = { AddGroup.class, EditGroup.class })
private String description;
/**
* 知识分隔符
*/
@NotBlank(message = "知识分隔符不能为空", groups = { AddGroup.class, EditGroup.class })
private String knowledgeSeparator;
/**
* 提问分隔符
*/
@NotBlank(message = "提问分隔符不能为空", groups = { AddGroup.class, EditGroup.class })
private String questionSeparator;
/**
* 重叠字符数
*/
@NotNull(message = "重叠字符数不能为空", groups = { AddGroup.class, EditGroup.class })
private Long overlapChar;
/**
@@ -101,8 +97,6 @@ public class KnowledgeInfoBo extends BaseEntity {
/**
* 备注
*/
@NotBlank(message = "备注不能为空", groups = { AddGroup.class, EditGroup.class })
private String remark;
}

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

@@ -21,6 +21,7 @@
<module>ruoyi-chat</module>
<module>ruoyi-system</module>
<module>ruoyi-generator</module>
<module>ruoyi-wechat</module>
</modules>
<properties>

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;
/**
* 查询配置信息列表
*/
@@ -71,12 +75,19 @@ public class ChatConfigController extends BaseController {
/**
* 新增配置信息
*/
@SaCheckPermission("system:config:add")
@Log(title = "配置信息", businessType = BusinessType.INSERT)
@SaCheckPermission("system:config:edit")
@Log(title = "新增或者修改配置信息", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody ChatConfigBo bo) {
return toAjax(chatConfigService.insertByBo(bo));
@PostMapping("/saveOrUpdate")
public R<Void> saveOrUpdate(@RequestBody List<ChatConfigBo> boList) {
for (ChatConfigBo chatConfigBo : boList) {
if(chatConfigBo.getId() == null){
chatConfigService.insertByBo(chatConfigBo);
}else {
chatConfigService.updateByBo(chatConfigBo);
}
}
return toAjax(true);
}
/**
@@ -102,4 +113,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

@@ -39,7 +39,6 @@ public class ChatGptsController extends BaseController {
/**
* 查询应用管理列表
*/
@SaCheckPermission("system:gpts:list")
@GetMapping("/list")
public TableDataInfo<ChatGptsVo> list(ChatGptsBo bo, PageQuery pageQuery) {
return chatGptsService.queryPageList(bo, pageQuery);

View File

@@ -45,6 +45,14 @@ public class ChatPackagePlanController extends BaseController {
return chatPackagePlanService.queryPageList(bo, pageQuery);
}
/**
* 查询套餐列表-不分页
*/
@GetMapping("/listPlan")
public R<List<ChatPackagePlanVo>> listPlan() {
return R.ok(chatPackagePlanService.queryList(new ChatPackagePlanBo()));
}
/**
* 导出套餐管理列表
*/

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

@@ -4,8 +4,8 @@ import lombok.Getter;
@Getter
public enum BillingType {
TOKEN("1", "token扣费"), // token扣费
TIMES("2", "次数扣费"); // 次数扣费
TOKEN("1", "token扣费"),
TIMES("2", "次数扣费");
private final String code;
private final String description;

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.jetbrains.annotations.NotNull;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.chat.util.SSEUtil;
import org.ruoyi.common.chat.entity.chat.ChatCompletionResponse;
import org.ruoyi.common.chat.request.ChatRequest;
import org.ruoyi.common.core.utils.SpringUtils;
@@ -84,10 +85,10 @@ public class SSEEventSourceListener extends EventSourceListener {
modelName = completionResponse.getModel();
}
stringBuffer.append(content);
emitter.send(content);
emitter.send(data);
}
} catch (Exception e) {
emitter.completeWithError(e);
log.error(e.getMessage(), e);
}
}

View File

@@ -48,11 +48,4 @@ public interface ISseService {
UploadFileResponse upload(MultipartFile file);
/**
* 企业应用回复
* @param prompt 提示词
* @return 回复内容
*/
String wxCpChat(String prompt);
}

View File

@@ -54,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);
@@ -69,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扣费
@@ -89,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);
@@ -97,12 +103,6 @@ public class ChatCostServiceImpl implements IChatCostService {
chatToken.setUserId(chatMessageBo.getUserId());
chatTokenService.editToken(chatToken);
}
Object userId = LocalCache.CACHE.get("userId");
if(userId!=null){
chatMessageBo.setUserId((Long) userId);
}else {
chatMessageBo.setUserId(getUserId());
}
// 保存消息记录
chatMessageService.insertByBo(chatMessageBo);
}

View File

@@ -2,16 +2,20 @@ 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.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
@@ -20,9 +24,16 @@ import java.util.List;
@Service
@Slf4j
public class OpenAIServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
private OpenAiStreamClient openAiStreamClient;
@Value("${spring.ai.mcp.client.enabled}")
private Boolean enabled;
private final ChatClient chatClient;
public OpenAIServiceImpl(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpSyncClients) {
@@ -35,10 +46,14 @@ public class OpenAIServiceImpl implements IChatService {
@Override
public SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter) {
String toolString = mcpChat(chatRequest.getPrompt());
Message userMessage = Message.builder().content("工具返回信息:"+toolString).role(Message.Role.USER).build();
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
List<Message> messages = chatRequest.getMessages();
messages.add(userMessage);
if (enabled) {
String toolString = mcpChat(chatRequest.getPrompt());
Message userMessage = Message.builder().content("工具返回信息:"+toolString).role(Message.Role.USER).build();
messages.add(userMessage);
}
SSEEventSourceListener listener = new SSEEventSourceListener(emitter);
ChatCompletion completion = ChatCompletion
.builder()

View File

@@ -11,10 +11,9 @@ import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.service.chat.IChatCostService;
import org.ruoyi.chat.service.chat.ISseService;
import org.ruoyi.chat.util.IpUtil;
import org.ruoyi.chat.util.SSEUtil;
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;
@@ -71,25 +70,24 @@ public class SseServiceImpl implements ISseService {
@Override
public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) {
SseEmitter sseEmitter = new SseEmitter();
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);
} catch (Exception e) {
log.error(e.getMessage(),e);
sseEmitter.completeWithError(e);
SSEUtil.sendErrorEvent(sseEmitter,e.getMessage());
}
return sseEmitter;
}
@@ -119,7 +117,6 @@ public class SseServiceImpl implements ISseService {
count++;
RedisUtils.setCacheObject(redisKey, count);
}
}
/**
@@ -145,8 +142,7 @@ public class SseServiceImpl implements ISseService {
if(StringUtils.isEmpty(sysPrompt)){
sysPrompt ="你是一个由RuoYI-AI开发的人工智能助手名字叫熊猫助手。你擅长中英文对话能够理解并处理各种问题提供安全、有帮助、准确的回答。" +
"当前时间:"+ DateUtils.getDate()+
"#注意:回复之前注意结合上下文内容。 ";
"#注意:回复之前注意结合上下文和工具返回内容进行回复。";
}
// 设置系统默认提示词
Message sysMessage = Message.builder().content(sysPrompt).role(Message.Role.SYSTEM).build();
@@ -273,20 +269,4 @@ public class SseServiceImpl implements ISseService {
return file;
}
@Override
public String wxCpChat(String prompt) {
List<Message> messageList = new ArrayList<>();
Message message = Message.builder().role(Message.Role.USER).content(prompt).build();
messageList.add(message);
ChatCompletion chatCompletion = ChatCompletion
.builder()
.messages(messageList)
.model("gpt-4o-mini")
.stream(false)
.build();
ChatCompletionResponse chatCompletionResponse = openAiStreamClient.chatCompletion(chatCompletion);
return chatCompletionResponse.getChoices().get(0).getMessage().getContent().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("权限不足");
}
}
}
}

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