30 Commits

Author SHA1 Message Date
lindaxia
e58aeb5361 更新README.md 2025-07-02 11:04:35 +08:00
lindaxia
b4306289f0 fix weaviate向量库根据数据类进行删除 2025-07-01 18:50:12 +08:00
lindaxia
d9c47bd983 修复删除知识库清空相关表 2025-07-01 18:06:49 +08:00
ageerle
dfe8c7dc85 Merge pull request #127 from Cyclones-Y/main
feat(chat): 集成 FastGPT 聊天模型
2025-06-30 23:21:31 +08:00
Yzm
fd94a1772f feat(chat): 集成 FastGPT 聊天模型- 在 ChatModeType 枚举中添加 FASTGPT 选项- 新增 FastGPT 相关的实体类和请求响应类
- 实现 FastGPT聊天服务接口
- 添加 FastGPT SSE 事件监听器
2025-06-30 22:03:31 +08:00
ageerle
c105d47d99 Merge pull request #125 from MuSan-Li/feature_20250626_fix_update_role
修复修改角色时候报错
2025-06-27 10:45:32 +08:00
l90215
4e2ec2dc82 feat: 修复修改角色时候报错&优化一些代码风格 2025-06-26 10:19:08 +08:00
ageerle
2fae8d0ad0 feat: 更新任务规划演示 2025-06-24 14:21:43 +08:00
ageerle
d7c2d1bcf3 feat: 更新任务规划演示 2025-06-24 14:16:40 +08:00
ageerle
122f63dfbd Merge remote-tracking branch 'origin/main' 2025-06-24 13:52:01 +08:00
ageerle
719e968192 feat: 更新任务规划演示 2025-06-24 13:51:51 +08:00
evo
bf790ceb51 Merge branch 'ageerle:main' into main 2025-06-24 11:43:08 +08:00
ageerle
de5488bd8c Merge pull request #123 from abin0515/one-step-script
one-step-script: 修改MacOS上运行快速启动脚本遇到的bug
2025-06-24 10:30:49 +08:00
GH Action - Upstream Sync
c77a245a4d Merge branch 'main' of https://github.com/ageerle/ruoyi-ai 2025-06-24 01:57:42 +00:00
Bin Xiao
6dcd8823cd one-step-script: 修改MacOS上运行快速启动脚本遇到的bug 2025-06-23 18:21:01 -04:00
ageerle
8be480e06c Update README.md 2025-06-23 16:49:13 +08:00
ageerle
11286de676 Merge pull request #118 from MuSan-Li/feature_20250610_add_prompt_template
Feature 20250610 add prompt template
2025-06-23 16:36:40 +08:00
MuSan-Li
5aaf0a672c feat: 删除fork文件 2025-06-12 17:55:19 +08:00
MuSan-Li
0089706336 添加提示词模板字典相关SQL 2025-06-12 17:37:04 +08:00
MuSan-Li
cc129801b9 添加提示词模板 2025-06-12 17:06:06 +08:00
GH Action - Upstream Sync
e1dc22348c Merge branch 'main' of https://github.com/ageerle/ruoyi-ai 2025-06-07 01:53:26 +00:00
ageerle
f37e4da669 feat: 更新二维码 2025-06-06 10:13:02 +08:00
GH Action - Upstream Sync
3e097d9a68 Merge branch 'main' of https://github.com/ageerle/ruoyi-ai 2025-06-06 01:54:02 +00:00
ageerle
97ae5a46cd feat: 调整sql脚本 2025-06-05 16:16:32 +08:00
ageerle
baa664ac4f feat: 图片识别功能优化 2025-06-05 16:00:06 +08:00
ageerle
353fbf26b8 Merge pull request #115 from Code-Mr-Jiu/main
上传图片支持使用后台image分类下通义千问模型
2025-06-05 13:53:20 +08:00
ageerle
f79b4ec012 Merge pull request #114 from Code-Mr-Jiu/jiuyi-dev
上传图片支持使用后台image分类模型
2025-06-05 13:53:09 +08:00
酒亦
0a73cb4e17 上传图片支持使用后台image分类下通义千问模型 2025-06-05 12:05:28 +08:00
酒亦
d635e30b4a 上传图片支持使用后台image分类模型 2025-06-02 08:11:22 +08:00
evo
ca50d1ddfb Create main.yml 2025-05-30 16:05:04 +08:00
31 changed files with 1977 additions and 23 deletions

View File

@@ -114,7 +114,7 @@
- 演示账号: demo 密码demo123
- 管理端https://admin.pandarobot.chat
- 演示账号: admin 密码admin123
- 商业版体验商业版请联系下方小助手获取演示地址预计6月份上线
### 源码地址
[1]github
@@ -135,8 +135,9 @@
#### 1. 全栈式开源系统
- 全套开源系统:提供完整的前端应用、后台管理,基于MIT协议开箱即用。
#### 2. 本地化 RAG 方案
- 基于 **Langchain4j** 框架,支持 Milvus/Weaviate/Qdrant 向量库,结合 BGE-large-zh-v1.5 本地向量化模型 实现高效文档检索与知识库构建。
- 支持 本地 LLM 接入,结合私有知识库实现安全可控的问答系统,避免依赖云端服务的隐私风险。
- 基于 **Langchain4j** 框架,支持 Milvus/Weaviate/Qdrant 向量库,结合 BGE-large-zh-v1.5 本地向量化模型 实现高效文档检索与知识库构建。
- 支持 本地 LLM 接入,结合私有知识库实现安全可控的问答系统,避免依赖云端服务的隐私风险。
- 支持 ollama、vLLm等平台部署模型。
#### 3. 多模态 AI 引擎与工具集成
- 智能对话:支持 OpenAI GPT-4、Azure、ChatGLM 等主流模型,内置 SSE/WebSocket 协议实现低延迟交互,兼容 **扣子**、**DIFY** 等平台 API 调用。
- **Spring AI MCP** 支持:通过注解快速定义本地工具,支持调用 MCP 广场 的海量 MCP Server 服务,扩展模型能力边界。
@@ -149,6 +150,36 @@
### 项目演示
#### 任务规划
### 任务规划演示
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0;">
<!-- 任务1: 自动创建项目 -->
<div style="text-align: center; padding: 15px; border-radius: 8px; background: #f8f9fa; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<h3 style="margin-top: 0; margin-bottom: 15px;">自动创建项目</h3>
<div style="margin-top: 10px;">
<a href="https://www.bilibili.com/video/BV1CgKuzFEt6" target="_blank" style="display: inline-block; background: #3498db; color: white; padding: 8px 15px; border-radius: 4px; text-decoration: none; font-weight: 500;">
观看视频
</a>
</div>
</div>
<!-- 任务2: 生成旅游攻略 -->
<div style="text-align: center; padding: 15px; border-radius: 8px; background: #f8f9fa; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<h3 style="margin-top: 0; margin-bottom: 15px;">生成旅游攻略</h3>
<div style="margin-top: 10px;">
<a href="https://www.bilibili.com/video/BV1kuKuzfERC" target="_blank" style="display: inline-block; background: #3498db; color: white; padding: 8px 15px; border-radius: 4px; text-decoration: none; font-weight: 500;">
观看视频
</a>
</div>
</div>
</div>
#### mcp支持
<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);"/>
@@ -285,13 +316,6 @@
5. 打开拉取请求
6. pr请提交到GitHub上会定时同步到gitee
#### 项目文档
1. 项目文档基于vitepress构建
2. 按照[如何参与开源项目](#如何参与开源项目)拉取https://github.com/ageerle/ruoyi-doc
3. 安装依赖npm install
4. 启动项目npm run docs:dev
5. 主页路径docs/guide/introduction/index.md
### 鸣谢
- [chatgpt-java](https://github.com/Grt1228/chatgpt-java)
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

After

Width:  |  Height:  |  Size: 267 KiB

View File

@@ -0,0 +1,15 @@
package org.ruoyi.common.chat.entity.chat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FastGPTAnswerResponse {
private String id;
private String object;
private long created;
private String model;
private List<FastGPTChatChoice> choices;
}

View File

@@ -0,0 +1,25 @@
package org.ruoyi.common.chat.entity.chat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FastGPTChatChoice implements Serializable {
private long index;
/**
* 请求参数stream为true返回是delta
*/
@JsonProperty("delta")
private Message delta;
/**
* 请求参数stream为false返回是message
*/
@JsonProperty("message")
private Message message;
@JsonProperty("finish_reason")
private String finishReason;
}

View File

@@ -0,0 +1,41 @@
package org.ruoyi.common.chat.entity.chat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.io.Serializable;
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class FastGPTChatCompletion extends ChatCompletion implements Serializable {
/**
* 是否使用FastGPT提供的上下文
*/
private String chatId;
/**
* 是否返回详细信息;stream模式下会通过event进行区分非stream模式结果保存在responseData中.
*/
private boolean detail;
/**
* 运行时变量
* 模块变量一个对象会替换模块中输入fastgpt框内容里的{{key}}
*/
private Variables variables;
/**
* responseChatItemId: string | undefined 。
* 如果传入,则会将该值作为本次对话的响应消息的 ID
* FastGPT 会自动将该 ID 存入数据库。请确保,
* 在当前chatId下responseChatItemId是唯一的。
*/
private String responseChatItemId;
}

View File

@@ -0,0 +1,20 @@
package org.ruoyi.common.chat.entity.chat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Variables implements Serializable {
private String uid;
private String name;
}

View File

@@ -0,0 +1,47 @@
package org.ruoyi.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.core.domain.BaseEntity;
/**
* 提示词模板对象 prompt_template
*
* @author evo
* @date 2025-06-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("prompt_template")
public class PromptTemplate extends BaseEntity {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 提示词模板名称
*/
private String templateName;
/**
* 提示词模板内容
*/
private String templateContent;
/**
* 提示词分类knowledge 知识库类型chat 对话类型draw绘画类型 ...
*/
private String category;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,53 @@
package org.ruoyi.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.core.domain.BaseEntity;
import org.ruoyi.domain.PromptTemplate;
/**
* 提示词模板业务对象 prompt_template
*
* @author evo
* @date 2025-06-12
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = PromptTemplate.class, reverseConvertGenerate = false)
public class PromptTemplateBo extends BaseEntity {
/**
* 主键
*/
@NotNull(message = "主键不能为空", groups = {EditGroup.class})
private Long id;
/**
* 提示词模板名称
*/
@NotBlank(message = "提示词模板名称不能为空", groups = {AddGroup.class, EditGroup.class})
private String templateName;
/**
* 提示词模板内容
*/
@NotBlank(message = "提示词模板内容不能为空", groups = {AddGroup.class, EditGroup.class})
private String templateContent;
/**
* 提示词分类knowledge 知识库类型chat 对话类型draw绘画类型 ...
*/
@NotBlank(message = "提示词分类", groups = {AddGroup.class, EditGroup.class})
private String category;
/**
* 备注
*/
@NotBlank(message = "备注不能为空", groups = {AddGroup.class, EditGroup.class})
private String remark;
}

View File

@@ -0,0 +1,52 @@
package org.ruoyi.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.ruoyi.domain.PromptTemplate;
import java.io.Serializable;
/**
* 提示词模板视图对象 prompt_template
*
* @author evo
* @date 2025-06-12
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = PromptTemplate.class)
public class PromptTemplateVo implements Serializable {
/**
* 主键
*/
@ExcelProperty(value = "主键")
private Long id;
/**
* 提示词模板名称
*/
@ExcelProperty(value = "提示词模板名称")
private String templateName;
/**
* 提示词模板内容
*/
@ExcelProperty(value = "提示词模板内容")
private String templateContent;
/**
* 提示词分类knowledge 知识库类型chat 对话类型draw绘画类型 ...
*/
@ExcelProperty(value = "提示词分类")
private String category;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
}

View File

@@ -0,0 +1,16 @@
package org.ruoyi.mapper;
import org.mapstruct.Mapper;
import org.ruoyi.core.mapper.BaseMapperPlus;
import org.ruoyi.domain.PromptTemplate;
import org.ruoyi.domain.vo.PromptTemplateVo;
/**
* 提示词模板Mapper接口
*
* @author evo
* @date 2025-06-12
*/
public interface PromptTemplateMapper extends BaseMapperPlus<PromptTemplate, PromptTemplateVo> {
}

View File

@@ -53,9 +53,13 @@ public interface IChatModelService {
* 通过模型名称获取模型信息
*/
ChatModelVo selectModelByName(String modelName);
/**
* 通过模型分类获取模型信息
*/
ChatModelVo selectModelByCategory(String image);
/**
* 获取ppt模型信息
*/
ChatModel getPPT();
}

View File

@@ -0,0 +1,49 @@
package org.ruoyi.service;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.domain.bo.PromptTemplateBo;
import org.ruoyi.domain.vo.PromptTemplateVo;
import java.util.Collection;
import java.util.List;
/**
* 提示词模板Service接口
*
* @author evo
* @date 2025-06-12
*/
public interface IPromptTemplateService {
/**
* 查询提示词模板
*/
PromptTemplateVo queryById(Long id);
/**
* 查询提示词模板列表
*/
TableDataInfo<PromptTemplateVo> queryPageList(PromptTemplateBo bo, PageQuery pageQuery);
/**
* 查询提示词模板列表
*/
List<PromptTemplateVo> queryList(PromptTemplateBo bo);
/**
* 新增提示词模板
*/
Boolean insertByBo(PromptTemplateBo bo);
/**
* 修改提示词模板
*/
Boolean updateByBo(PromptTemplateBo bo);
/**
* 校验并批量删除提示词模板信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -129,6 +129,13 @@ public class ChatModelServiceImpl implements IChatModelService {
public ChatModelVo selectModelByName(String modelName) {
return baseMapper.selectVoOne(Wrappers.<ChatModel>lambdaQuery().eq(ChatModel::getModelName, modelName));
}
/**
* 通过模型分类获取模型信息
*/
@Override
public ChatModelVo selectModelByCategory(String category) {
return baseMapper.selectVoOne(Wrappers.<ChatModel>lambdaQuery().eq(ChatModel::getCategory, category));
}
@Override
public ChatModel getPPT() {

View File

@@ -0,0 +1,112 @@
package org.ruoyi.service.impl;
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.common.core.utils.MapstructUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.domain.PromptTemplate;
import org.ruoyi.domain.bo.PromptTemplateBo;
import org.ruoyi.domain.vo.PromptTemplateVo;
import org.ruoyi.mapper.PromptTemplateMapper;
import org.ruoyi.service.IPromptTemplateService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
/**
* 提示词模板Service业务层处理
*
* @author evo
* @date 2025-06-12
*/
@Service
@RequiredArgsConstructor
public class PromptTemplateServiceImpl implements IPromptTemplateService {
private final PromptTemplateMapper baseMapper;
/**
* 查询提示词模板
*/
@Override
public PromptTemplateVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 查询提示词模板列表
*/
@Override
public TableDataInfo<PromptTemplateVo> queryPageList(PromptTemplateBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<PromptTemplate> lqw = buildQueryWrapper(bo);
Page<PromptTemplateVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询提示词模板列表
*/
@Override
public List<PromptTemplateVo> queryList(PromptTemplateBo bo) {
LambdaQueryWrapper<PromptTemplate> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<PromptTemplate> buildQueryWrapper(PromptTemplateBo bo) {
LambdaQueryWrapper<PromptTemplate> lqw = Wrappers.lambdaQuery();
lqw.eq(StringUtils.isNotBlank(bo.getTemplateName()),
PromptTemplate::getTemplateName, bo.getTemplateName());
lqw.like(StringUtils.isNotBlank(bo.getTemplateContent()),
PromptTemplate::getTemplateContent, bo.getTemplateContent());
lqw.eq(StringUtils.isNotBlank(bo.getCategory()),
PromptTemplate::getCategory, bo.getCategory());
return lqw;
}
/**
* 新增提示词模板
*/
@Override
public Boolean insertByBo(PromptTemplateBo bo) {
PromptTemplate add = MapstructUtils.convert(bo, PromptTemplate.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改提示词模板
*/
@Override
public Boolean updateByBo(PromptTemplateBo bo) {
PromptTemplate update = MapstructUtils.convert(bo, PromptTemplate.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(PromptTemplate entity) {
//TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除提示词模板
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
}

View File

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

View File

@@ -10,6 +10,9 @@ import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.weaviate.WeaviateEmbeddingStore;
import io.weaviate.client.Config;
import io.weaviate.client.WeaviateClient;
import io.weaviate.client.base.Result;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -83,10 +86,20 @@ public class VectorStoreServiceImpl implements VectorStoreService {
@Override
public void removeById(String id, String modelName) {
createSchema(id, modelName);
// 根据条件删除向量数据
embeddingStore.remove(id);
@SneakyThrows
public void removeById(String id, String modelName) {
String protocol = configService.getConfigValue("weaviate", "protocol");
String host = configService.getConfigValue("weaviate", "host");
String className = configService.getConfigValue("weaviate", "classname");
String finalClassName = className + id;
WeaviateClient client = new WeaviateClient(new Config(protocol, host));
Result<Boolean> result = client.schema().classDeleter().withClassName(finalClassName).run();
if (result.hasErrors()) {
log.error("失败删除向量: " + result.getError());
throw new ServiceException("失败删除向量数据!");
} else {
log.info("成功删除向量数据: " + result.getResult());
}
}
/**

View File

@@ -440,7 +440,7 @@ public class SysRoleServiceImpl implements ISysRoleService {
return;
}
// 角色关联的在线用户量过大会导致redis阻塞卡顿 谨慎操作
keys.parallelStream().forEach(key -> {
keys.forEach(key -> {
String token = StringUtils.substringAfterLast(key, ":");
// 如果已经过期则跳过
if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {

View File

@@ -325,9 +325,9 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
@Transactional(rollbackFor = Exception.class)
public int updateUser(SysUserBo user) {
// 新增用户与角色管理
//insertUserRole(user, true);
insertUserRole(user, true);
// 新增用户与岗位管理
//insertUserPost(user, true);
insertUserPost(user, true);
SysUser sysUser = MapstructUtils.convert(user, SysUser.class);
// 防止错误更新后导致的数据误删除
int flag = baseMapper.updateById(sysUser);

View File

@@ -0,0 +1,111 @@
package org.ruoyi.chat.controller.chat;
import cn.dev33.satoken.annotation.SaCheckPermission;
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.core.validate.EditGroup;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.domain.bo.PromptTemplateBo;
import org.ruoyi.domain.vo.PromptTemplateVo;
import org.ruoyi.service.IPromptTemplateService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 提示词模板
*
* @author evo
* @date 2025-06-12
*/
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/promptTemplate")
public class PromptTemplateController extends BaseController {
private final IPromptTemplateService promptTemplateService;
/**
* 查询提示词模板列表
*/
@SaCheckPermission("system:promptTemplate:list")
@GetMapping("/list")
public TableDataInfo<PromptTemplateVo> list(PromptTemplateBo bo, PageQuery pageQuery) {
return promptTemplateService.queryPageList(bo, pageQuery);
}
/**
* 导出提示词模板列表
*/
@SaCheckPermission("system:promptTemplate:export")
@Log(title = "提示词模板", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(PromptTemplateBo bo, HttpServletResponse response) {
List<PromptTemplateVo> list = promptTemplateService.queryList(bo);
ExcelUtil.exportExcel(list, "提示词模板", PromptTemplateVo.class, response);
}
/**
* 获取提示词模板详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:promptTemplate:query")
@GetMapping("/{id}")
public R<PromptTemplateVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(promptTemplateService.queryById(id));
}
/**
* 新增提示词模板
*/
@SaCheckPermission("system:promptTemplate:add")
@Log(title = "提示词模板", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody PromptTemplateBo bo) {
return toAjax(promptTemplateService.insertByBo(bo));
}
/**
* 修改提示词模板
*/
@SaCheckPermission("system:promptTemplate:edit")
@Log(title = "提示词模板", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody PromptTemplateBo bo) {
return toAjax(promptTemplateService.updateByBo(bo));
}
/**
* 删除提示词模板
*
* @param ids 主键串
*/
@SaCheckPermission("system:promptTemplate:remove")
@Log(title = "提示词模板", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(promptTemplateService.deleteWithValidByIds(List.of(ids), true));
}
}

View File

@@ -15,7 +15,11 @@ public enum ChatModeType {
QIANWEN("qianwen", "通义千问"),
VECTOR("vector", "知识库向量模型");
VECTOR("vector", "知识库向量模型"),
IMAGE("image", "图片识别模型"),
FASTGPT("fastgpt", "FASTGPT");
private final String code;
private final String description;

View File

@@ -0,0 +1,70 @@
package org.ruoyi.chat.listener;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Objects;
@Slf4j
@Component
@RequiredArgsConstructor
public class FastGPTSSEEventSourceListener extends EventSourceListener {
private SseEmitter emitter;
@Autowired(required = false)
public FastGPTSSEEventSourceListener(SseEmitter emitter) {
this.emitter = emitter;
}
@Override
public void onOpen(EventSource eventSource, Response response) {
log.info("FastGPT sse连接成功");
}
@Override
public void onEvent(@NotNull EventSource eventSource, String id, String type, @NotNull String data) {
try {
log.debug("事件类型为: {}", type);
log.debug("事件数据为: {}", data);
if ("flowResponses".equals(type)){
emitter.send(data);
emitter.complete();
} else {
emitter.send(data);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void onClosed(EventSource eventSource) {
log.info("FastGPT sse连接关闭");
}
@Override
@SneakyThrows
public void onFailure(EventSource eventSource, Throwable t, Response response) {
if (Objects.isNull(response)) {
return;
}
ResponseBody body = response.body();
if (Objects.nonNull(body)) {
log.error("FastGPT sse连接异常data{},异常:{}", body.string(), t);
} else {
log.error("FastGPT sse连接异常data{},异常:{}", response, t);
}
eventSource.cancel();
}
}

View File

@@ -169,6 +169,7 @@ public class ChatCostServiceImpl implements IChatCostService {
/**
* 获取用户Id
*/
@Override
public Long getUserId() {
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser == null) {

View File

@@ -0,0 +1,52 @@
package org.ruoyi.chat.service.chat.impl;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.chat.enums.ChatModeType;
import org.ruoyi.chat.listener.FastGPTSSEEventSourceListener;
import org.ruoyi.chat.service.chat.IChatService;
import org.ruoyi.common.chat.entity.chat.FastGPTChatCompletion;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
/**
* FastGpt 聊天管理
* 项目整体沿用Openai接口范式根据FastGPT文档增加相应的参数
*
* @author yzm
*/
@Service
public class FastGPTServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
@Override
public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
OpenAiStreamClient openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
List<Message> messages = chatRequest.getMessages();
FastGPTSSEEventSourceListener listener = new FastGPTSSEEventSourceListener(emitter);
FastGPTChatCompletion completion = FastGPTChatCompletion
.builder()
.messages(messages)
// 开启后sse会返回event值
.detail(true)
.stream(true)
.build();
openAiStreamClient.streamChatCompletion(completion, listener);
return emitter;
}
@Override
public String getCategory() {
return ChatModeType.FASTGPT.getCode();
}
}

View File

@@ -0,0 +1,151 @@
package org.ruoyi.chat.service.chat.impl;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.chat.config.ChatConfig;
import org.ruoyi.chat.enums.ChatModeType;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*;
/**
* 图片识别模型
*/
@Service
@Slf4j
public class ImageServiceImpl implements IChatService {
@Autowired
private IChatModelService chatModelService;
@SneakyThrows
// @Override
// public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
// ChatModelVo chatModelVo = chatModelService.selectModelByCategory("image");
//
// // 发送流式消息
//
// MultiModalConversation conv = new MultiModalConversation();
// MultiModalMessage systemMessage = MultiModalMessage.builder().role(Role.SYSTEM.getValue())
// .content(Arrays.asList(
// Collections.singletonMap("text",chatRequest.getSysPrompt()))).build();
// // 获取用户消息内容
// List<Message> messages = chatRequest.getMessages();
// MultiModalMessage userMessage = null;
// //漫长的格式转换
// // 遍历消息列表,提取文本内容
// if (messages != null && !messages.isEmpty()) {
// Object content = messages.get(messages.size() - 1).getContent();
// List<Map<String, Object>> contentList = new ArrayList<>();
// StringBuilder textContent = new StringBuilder();
// if (content instanceof List<?>) {
// for (Object item : (List<?>) content) {
// if (item instanceof Map<?, ?> mapItem) {
// String type = (String) mapItem.get("type");
// if ("text".equals(type)) {
// String text = (String) mapItem.get("text");
// if (text != null) {
// textContent.append(text).append(" ");
// }
// } else if ("image_url".equals(type)) {
// Map<String, String> imageUrl = (Map<String, String>) mapItem.get("image_url");
// contentList.add(Collections.singletonMap("image", imageUrl.get("url")));
// }
// }
// }
// }
// // 将拼接后的文本内容添加到 contentList
// if (textContent.length() > 0) {
// contentList.add(Collections.singletonMap("text", textContent.toString().trim()));
// }
// userMessage = MultiModalMessage.builder()
// .role(Role.USER.getValue())
// .content(contentList)
// .build();
// }
// MultiModalConversationParam param = MultiModalConversationParam.builder()
// .apiKey(chatModelVo.getApiKey())
// .model(chatModelVo.getModelName())
// .messages(Arrays.asList(systemMessage, userMessage))
// .incrementalOutput(true)
// .build();
//
//
// try {
// final QwenStreamingResponseBuilder responseBuilder = new QwenStreamingResponseBuilder(param.getModel(),param.getIncrementalOutput() );
// conv.streamCall(param, new ResultCallback<>() {
// @SneakyThrows
// public void onEvent(MultiModalConversationResult result) {
//
// String delta = responseBuilder.append(result);
// if (Utils.isNotNullOrEmpty(delta)) {
//
// emitter.send(delta);
// log.info("收到消息片段: {}", delta);
// }
// }
// public void onComplete() {
// emitter.complete();
// log.info("消息结束", responseBuilder.build());
// }
// public void onError(Exception e) {
// log.info("请求失败", e.getMessage());
// }
// });
// } catch (NoApiKeyException e) {
// emitter.send("请先配置API密钥");
// throw new RuntimeException(e);
// } catch (UploadFileException e) {
// throw new RuntimeException(e);
// }
//
//
// return emitter;
// }
@Override
public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) {
// 从数据库获取 image 类型的模型配置
ChatModelVo chatModelVo = chatModelService.selectModelByCategory(ChatModeType.IMAGE.getCode());
if (chatModelVo == null) {
log.error("未找到 image 类型的模型配置");
emitter.completeWithError(new IllegalStateException("未找到 image 类型的模型配置"));
return emitter;
}
// 创建 OpenAI 流客户端
OpenAiStreamClient openAiStreamClient = ChatConfig.createOpenAiStreamClient(chatModelVo.getApiHost(), chatModelVo.getApiKey());
List<Message> messages = chatRequest.getMessages();
// 创建 SSE 事件源监听器
SSEEventSourceListener listener = new SSEEventSourceListener(emitter, chatRequest.getUserId(), chatRequest.getSessionId());
// 构建聊天完成请求
ChatCompletion completion = ChatCompletion
.builder()
.messages(messages)
.model(chatModelVo.getModelName()) // 使用数据库中配置的模型名称
.stream(true)
.build();
// 发起流式聊天完成请求
openAiStreamClient.streamChatCompletion(completion, listener);
return emitter;
}
@Override
public String getCategory() {
return ChatModeType.IMAGE.getCode();
}
}

View File

@@ -35,6 +35,8 @@ public class QianWenAiChatServiceImpl implements IChatService {
.modelName(chatModelVo.getModelName())
.build();
// 发送流式消息
try {
model.chat(chatRequest.getPrompt(), new StreamingChatResponseHandler() {

View File

@@ -125,7 +125,16 @@ public class SseServiceImpl implements ISseService {
*/
private void buildChatMessageList(ChatRequest chatRequest){
String sysPrompt;
chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
// 矫正模型名称 如果是gpt-image 则查询image类型模型 获取模型名称
if(chatRequest.getModel().equals("gpt-image")) {
chatModelVo = chatModelService.selectModelByCategory("image");
if (chatModelVo == null) {
log.error("未找到image类型的模型配置");
throw new IllegalStateException("未找到image类型的模型配置");
}
}else{
chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
}
// 获取对话消息列表
List<Message> messages = chatRequest.getMessages();
// 查询向量库相关信息加入到上下文

View File

@@ -179,13 +179,14 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService {
Map<String,Object> map = new HashMap<>();
KnowledgeInfo knowledgeInfo = baseMapper.selectById(id);
check(knowledgeInfo);
map.put("kid",knowledgeInfo.getKid());
map.put("kid",knowledgeInfo.getId());
// 删除向量数据
vectorStoreService.removeById(String.valueOf(knowledgeInfo.getId()),knowledgeInfo.getVectorModelName());
// 删除附件和知识片段
fragmentMapper.deleteByMap(map);
attachMapper.deleteByMap(map);
// 删除知识库
map.put("kid",knowledgeInfo.getKid());
baseMapper.deleteByMap(map);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -132,8 +132,8 @@ INSERT INTO `chat_model` VALUES (1828324413241466880, '000000', 'vector', 'quent
INSERT INTO `chat_model` VALUES (1828324413241466881, '000000', 'vector', 'baai/bge-m3', 'baai/bge-m3', 0.01, '2', '1', NULL, 'https://api.ppinfra.com/v3/openai', 'sk-xx', NULL, 103, 1, '2024-08-27 14:51:23', 1, '2025-05-24 17:33:11', 'BGE-M3 是一款具备多维度能力的文本嵌入模型可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中BGE-M3展现出显著优势其性能在MIRACL、MKQA等国际基准测试中位居前列。此外针对长文档检索场景该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。');
INSERT INTO `chat_model` VALUES (1859570229117022211, '000000', 'chat', 'deepseek/deepseek-v3-0324', 'deepseek/deepseek-v3-0324', 0.1, '1', '0', '', 'https://api.ppinfra.com/v3/openai/chat/completions', 'sk-xx', NULL, 103, 1, '2024-11-21 20:11:06', 1, '2025-05-24 17:56:22', 'DeepSeek V3 0324 是深度求索DeepSeek团队旗舰级对话模型系列的最新版本采用混合专家Mixture-of-Experts, MoE架构参数量达685B参数。');
INSERT INTO `chat_model` VALUES (1859570229117022212, '000000', 'chat', 'deepseek/deepseek-r1', 'deepseek/deepseek-r1', 0.1, '1', '0', '', 'https://api.ppinfra.com/v3/openai/chat/completions', 'sk-xx', NULL, 103, 1, '2024-11-21 20:11:06', 1, '2025-05-24 17:56:14', 'DeepSeek R1是DeepSeek团队发布的最新开源模型具备非常强悍的推理性能尤其在数学、编程和推理任务上达到了与OpenAI的o1模型相当的水平。');
INSERT INTO `chat_model` VALUES (1926215622017482754, '000000', 'chat', 'gpt-4o-mini', 'gpt-4o-mini', 0.1, '1', '0', NULL, 'https://api.pandarobot.chat/v1/chat/completions/', 'sk-xx', NULL, 103, 1, '2025-05-24 17:56:06', 1, '2025-05-24 17:56:06', 'gpt 多模态模型');
INSERT INTO `chat_model` VALUES (1926215622017482755, '000000', 'chat', 'gpt-4-all', 'gpt-4-all', 0.5, '2', '0', NULL, 'https://api.pandarobot.chat/v1/chat/completions/', 'sk-xx', NULL, 103, 1, '2025-05-24 17:56:06', 1, '2025-05-24 17:59:21', 'gpt 逆向多模态模型');
INSERT INTO `chat_model` VALUES (1930184891812147202, '000000', 'image', 'qwen/qwen2.5-vl-72b-instruct', 'qwen/qwen2.5-vl-72b-instruct', 0.003, '2', '0', NULL, 'https://api.ppinfra.com/v3/openai/chat/completions', 'xx', NULL, 103, 1, '2025-06-04 16:48:34', 1, '2025-06-04 16:48:34', '视觉模型');
-- ----------------------------
-- Table structure for chat_pay_order
@@ -492,6 +492,7 @@ INSERT INTO `sys_dict_data` VALUES (1776109770934677506, '000000', 0, 'token计
INSERT INTO `sys_dict_data` VALUES (1776109853377916929, '000000', 0, '次数计费', '2', 'sys_model_billing', '', 'success', 'N', '0', 103, 1, '2024-04-05 12:49:22', 1, '2024-04-05 12:49:22', '');
INSERT INTO `sys_dict_data` VALUES (1780264338471858177, '000000', 0, '未支付', '1', 'pay_state', '', 'info', 'N', '0', 103, 1, '2024-04-16 23:57:49', 1, '2024-04-16 23:58:29', '');
INSERT INTO `sys_dict_data` VALUES (1780264431589601282, '000000', 2, '已支付', '2', 'pay_state', '', 'success', 'N', '0', 103, 1, '2024-04-16 23:58:11', 1, '2024-04-16 23:58:21', '');
INSERT INTO `sys_dict_data` VALUES (1933094189606670338, '000000', 0, '知识库', 'vector', 'prompt_template_type', null, '', 'N', '0', 103, 1, '2025-06-12 17:29:05', 1, '2025-06-12 17:29:05', null);
-- ----------------------------
-- Table structure for sys_dict_type
@@ -536,6 +537,8 @@ INSERT INTO `sys_dict_type` VALUES (1775756736895438849, '000000', '用户等级
INSERT INTO `sys_dict_type` VALUES (1776109665045278721, '000000', '模型计费方式', 'sys_model_billing', '0', 103, 1, '2024-04-05 12:48:37', 1, '2024-04-08 11:22:18', '模型计费方式');
INSERT INTO `sys_dict_type` VALUES (1780263881368219649, '000000', '支付状态', 'pay_state', '0', 103, 1, '2024-04-16 23:56:00', 1, '2025-03-29 15:21:57', '支付状态');
INSERT INTO `sys_dict_type` VALUES (1904565568803217409, '000000', '状态类型', 'status_type', '0', 103, 1, '2025-03-26 00:06:31', 1, '2025-03-26 00:06:31', NULL);
INSERT INTO `sys_dict_type` VALUES (1933093946274123777, '000000', '提示词模板分类', 'prompt_template_type', '0', 103, 1, '2025-06-12 17:28:07', 1, '2025-06-12 17:28:07', null);
-- ----------------------------
-- Table structure for sys_file_info
@@ -713,6 +716,12 @@ INSERT INTO `sys_menu` VALUES (1906674838461321219, '配置信息新增', 190667
INSERT INTO `sys_menu` VALUES (1906674838461321220, '配置信息修改', 1906674838461321217, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:edit', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, '');
INSERT INTO `sys_menu` VALUES (1906674838461321221, '配置信息删除', 1906674838461321217, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:remove', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, '');
INSERT INTO `sys_menu` VALUES (1906674838461321222, '配置信息导出', 1906674838461321217, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:export', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, '');
INSERT INTO `sys_menu` VALUES (1929170702299045890, '提示词模板', 1775500307898949634, '1', 'promptTemplate', 'system/promptTemplate/index', '', 1, 0, 'C', '0', '0', 'system:promptTemplate:list', 'fluent:prompt-16-filled', 103, 1, sysdate(), null, null, '提示词模板菜单');
INSERT INTO `sys_menu` VALUES (1929170702299045891, '提示词模板查询', 1929170702299045890, '1', '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:query', '#', 103, 1, sysdate(), null, null, '');
INSERT INTO `sys_menu` VALUES (1929170702299045892, '提示词模板新增', 1929170702299045890, '2', '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:add', '#', 103, 1, sysdate(), null, null, '');
INSERT INTO `sys_menu` VALUES (1929170702299045893, '提示词模板修改', 1929170702299045890, '3', '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:edit', '#', 103, 1, sysdate(), null, null, '');
INSERT INTO `sys_menu` VALUES (1929170702299045894, '提示词模板删除', 1929170702299045890, '4', '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:remove', '#', 103, 1, sysdate(), null, null, '');
INSERT INTO `sys_menu` VALUES (1929170702299045895, '提示词模板导出', 1929170702299045890, '5', '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:export', '#', 103, 1, sysdate(), null, null, '');
-- ----------------------------
-- Table structure for sys_notice
@@ -2510,4 +2519,24 @@ INSERT INTO `sys_user_role` VALUES (1871910972567822337, 1);
INSERT INTO `sys_user_role` VALUES (1897620177094057985, 1);
INSERT INTO `sys_user_role` VALUES (1925795787894333441, 1729685491108446210);
# 提示词模板表
DROP TABLE IF EXISTS `prompt_template`;
CREATE TABLE prompt_template
(
id bigint auto_increment comment '主键'
primary key,
template_name varchar(128) null comment '提示词模板名称',
template_content text null comment '提示词模板内容',
category varchar(16) NULL COMMENT '提示词分类knowledge 知识库类型chat 对话类型draw绘画类型 ...',
create_dept bigint null comment '创建部门',
create_by bigint null comment '创建者',
create_time datetime null comment '创建时间',
update_by bigint null comment '更新者',
update_time datetime null comment '更新时间',
remark varchar(256) null comment '备注'
) ENGINE = InnoDB
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT = '提示词模板表'
ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1 @@