mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-23 03:27:26 +08:00
Compare commits
45 Commits
199d2b439e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 97c76675e7 | |||
| d335ba8612 | |||
| 3cd987b2e9 | |||
| 03f27067d4 | |||
| e94cf00ec0 | |||
| c71e6a6d25 | |||
| 030dc503eb | |||
| 918b59120e | |||
| e2402c75b0 | |||
| 3735ca1687 | |||
| be466f0b03 | |||
| 4092eed2a2 | |||
| 901f05ed21 | |||
| 41877a6e8b | |||
| b7a3da89ca | |||
| de4a8ce652 | |||
| e533af285c | |||
| 23dc7b3f03 | |||
| e57aef0708 | |||
| 42087c0bf8 | |||
| 606edcc82f | |||
| 9082e986f1 | |||
| 40cd525bba | |||
| d6b6cf079e | |||
| 1b688e7cd2 | |||
| 58915a6410 | |||
| b67096dc54 | |||
| 67795493bd | |||
| e1c8f00bf2 | |||
| 17a81e89a5 | |||
| bcbf775756 | |||
| 462365890e | |||
| b686f9fbd1 | |||
| 872f84d006 | |||
| 26087172df | |||
| 281c3ff3c8 | |||
| 38d44c2487 | |||
| 8c88f8adf5 | |||
| 526734c5a5 | |||
| 44b48ad916 | |||
| 1a3ae4f61c | |||
| 859c509f08 | |||
| 0704f187af | |||
| adc191f03e | |||
| f6f2ef6316 |
@ -3,9 +3,6 @@ AIGC应用平台介绍
|
|||||||
|
|
||||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||||
|
|
||||||
> JDK说明:AI流程编排引擎暂时不支持jdk21,所以目前只能使用jdk8或者jdk17启动项目。
|
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||||
|
|
||||||
@ -109,6 +106,10 @@ JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发
|
|||||||
| ChatGTP | √ |
|
| ChatGTP | √ |
|
||||||
| Qwq | √ |
|
| Qwq | √ |
|
||||||
| 智库 | √ |
|
| 智库 | √ |
|
||||||
|
| claude | √ |
|
||||||
|
| vl模型 | √ |
|
||||||
|
| 千帆大模型 | √ |
|
||||||
|
| 通义千问 | √ |
|
||||||
| Ollama本地搭建大模型 | √ |
|
| Ollama本地搭建大模型 | √ |
|
||||||
| 等等。。 | √ |
|
| 等等。。 | √ |
|
||||||
|
|
||||||
|
|||||||
@ -1,126 +0,0 @@
|
|||||||
|
|
||||||
JeecgBoot低代码平台(商业版介绍)
|
|
||||||
===============
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
项目介绍
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
<h3 align="center">企业级AI低代码平台</h3>
|
|
||||||
|
|
||||||
|
|
||||||
JeecgBoot是一款集成AI应用的,基于BPM流程的低代码平台,旨在帮助企业快速实现低代码开发和构建个性化AI应用,支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”)!
|
|
||||||
|
|
||||||
前后端分离架构Ant Design&Vue3,SpringBoot,SpringCloud Alibaba,Mybatis-plus,Shiro。强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE, 帮助Java项目解决80%的重复工作,让开发更多关注业务,提高效率、节省成本,同时又不失灵活性!低代码能力:Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能:AI知识库问答、AI模型管理、AI流程编排、AI聊天等,支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
|
|
||||||
|
|
||||||
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发:Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)
|
|
||||||
|
|
||||||
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手,AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
|
|
||||||
|
|
||||||
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
|
||||||
|
|
||||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### JeecgBoot商业版与同类产品区别
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
- 灵活性:jeecgboot基于开源技术栈,设计初考虑到可插拔性和集成灵活性,确保平台的智能性与灵活性,避免因平台过于庞大而导致的扩展困难。
|
|
||||||
- 流程管理:支持一个表单挂接多个流程,同时一个流程可以连接多个表单,增强了流程的灵活性和复杂性管理。
|
|
||||||
- 符合中国国情的流程:针对中国市场的特定需求,jeecgboot能够实现各种符合中国国情的业务流程。
|
|
||||||
- 强大的表单设计器:jeecgboot的表单设计器与敲敲云共享,具备高质量和智能化的特点,能够满足零代码应用的需求,业内同类产品中不多见。
|
|
||||||
- 报表功能:自主研发的报表工具,拥有独立知识产权,功能上比业内老牌产品如帆软更智能,操作简便。
|
|
||||||
- BI产品整合:提供大屏、仪表盘、门户等功能,完美解决这些需求,并支持移动面板的设计与渲染。
|
|
||||||
- 自主研发的模块:jeecgboot的所有模块均为自主研发,具有独立的知识产权。
|
|
||||||
- 颗粒度和功能细致:在功能细致度和颗粒度上,jeecgboot远超同类产品,尤其在零代码能力方面表现突出。
|
|
||||||
- 零代码应用管理:最新版支持与敲敲云的零代码应用管理能力的集成,使得jeecgboot既具备低代码,又具备零代码的应用能力,业内独一无二。
|
|
||||||
- 强大的代码生成器:作为开源代码生成器的先锋,jeecgboot在代码生成的智能化和在线低代码与代码生成的结合方面,优势明显。
|
|
||||||
- 精细化权限管理:提供行级和列级的数据权限控制,满足企业在ERP和OA领域对权限管理的严格需求。
|
|
||||||
- 多平台支持的APP:目前采用uniapp3实现,支持小程序、H5、App及鸿蒙、鸿蒙Next、Electron桌面应用等多种终端。
|
|
||||||
|
|
||||||
> 综上所述,jeecgboot不仅在功能上具备丰富性和灵活性,还在技术架构、权限管理和用户体验等方面展现出明显的优势,是一个综合性能强大的低代码平台。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
商业版演示
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
JeecgBoot vs 敲敲云
|
|
||||||
> - JeecgBoot是低代码产品拥有系列低代码能力,比如流程设计、表单设计、大屏设计,代码生成器,适合半开发模式(开发+低代码结合),也可以集成零代码应用管理模块.
|
|
||||||
> - 敲敲云是零代码产品,完全不写代码,通过配置搭建业务系统,其在jeecgboot基础上研发而成,删除了online、代码生成、OA等需要编码功能,只保留应用管理功能和聊天、日程、文件三个OA组件.
|
|
||||||
|
|
||||||
|
|
||||||
- JeecgBoot低代码: https://boot3.jeecg.com
|
|
||||||
- 敲敲云零代码:https://app.qiaoqiaoyun.com
|
|
||||||
- APP演示(多端): http://jeecg.com/appIndex
|
|
||||||
|
|
||||||
|
|
||||||
### 流程视频介绍
|
|
||||||
|
|
||||||
[](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 商业版功能简述
|
|
||||||
|
|
||||||
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
|
|
||||||
|
|
||||||
```
|
|
||||||
│─更多商业功能
|
|
||||||
│ ├─流程设计器
|
|
||||||
│ ├─简流设计器(类钉钉版)
|
|
||||||
│ ├─门户设计(NEW)
|
|
||||||
│ ├─表单设计器
|
|
||||||
│ ├─大屏设计器
|
|
||||||
│ └─我的任务
|
|
||||||
│ └─历史流程
|
|
||||||
│ └─历史流程
|
|
||||||
│ └─流程实例管理
|
|
||||||
│ └─流程监听管理
|
|
||||||
│ └─流程表达式
|
|
||||||
│ └─我发起的流程
|
|
||||||
│ └─我的抄送
|
|
||||||
│ └─流程委派、抄送、跳转
|
|
||||||
│ └─OA办公组件
|
|
||||||
│ └─零代码应用管理(无需编码,在线搭建应用系统)
|
|
||||||
│ ├─积木报表企业版(含jimureport、jimubi)
|
|
||||||
│ ├─AI流程设计器源码
|
|
||||||
│ ├─Online全模块功能和源码
|
|
||||||
│ ├─AI写文章(CMS)
|
|
||||||
│ ├─AI表单字段建议(表单设计器)
|
|
||||||
│ ├─OA办公协同组件
|
|
||||||
│ ├─在线聊天功能
|
|
||||||
│ ├─设计表单移动适配
|
|
||||||
│ ├─设计表单支持外部填报
|
|
||||||
│ ├─设计表单AI字段建议
|
|
||||||
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
|
|
||||||
│ └─。。。
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### 流程设计
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### 表单设计器
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
|
[中文](./README.md) | English
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -7,12 +7,12 @@
|
|||||||
JEECG BOOT AI Low Code Platform
|
JEECG BOOT AI Low Code Platform
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Current version: 3.9.0 (Release date: 2025-12-01)
|
Current version: 3.9.1 (Release date: 2026-01-22)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://www.jeecg.com)
|
[](http://www.jeecg.com)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
19
README.md
19
README.md
@ -1,14 +1,15 @@
|
|||||||
|
中文 | [English](./README.en-US.md)
|
||||||
|
|
||||||
JeecgBoot AI低代码平台
|
JeecgBoot AI低代码平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
当前最新版本: 3.9.1(发布日期:2026-01-22)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||||
[](https://jeecg.com)
|
[](https://jeecg.com)
|
||||||
[](https://jeecg.blog.csdn.net)
|
[](https://jeecg.blog.csdn.net)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
[](https://github.com/jeecgboot/JeecgBoot)
|
[](https://github.com/jeecgboot/JeecgBoot)
|
||||||
|
|
||||||
@ -232,20 +233,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
开源版与企业版区别?
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 协议附加补充条款:允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(但在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
|
|
||||||
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如:Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
|
|
||||||
- JeecgBoot未来发展方向是:零代码平台的建设,也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) ,无需编码即可通过拖拽快速搭建企业级应用,与JeecgBoot低代码平台形成互补,满足从简单业务到复杂系统的全场景开发需求,目前已经开源,[欢迎下载](https://qiaoqiaoyun.com/downloadCode)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Jeecg Boot 产品功能蓝图
|
### Jeecg Boot 产品功能蓝图
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
|
|
||||||
JeecgBoot 低代码开发平台
|
JeecgBoot 低代码开发平台
|
||||||
===============
|
===============
|
||||||
|
|
||||||
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
当前最新版本: 3.9.1(发布日期: 2026-01-22)
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||||
[](http://jeecg.com/aboutusIndex)
|
[](http://jeecg.com/aboutusIndex)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<version>3.9.0</version>
|
<version>3.9.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>jeecg-boot-base-core</artifactId>
|
<artifactId>jeecg-boot-base-core</artifactId>
|
||||||
@ -305,7 +305,7 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- mini文件存储服务 -->
|
<!-- minio文件存储服务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.minio</groupId>
|
<groupId>io.minio</groupId>
|
||||||
<artifactId>minio</artifactId>
|
<artifactId>minio</artifactId>
|
||||||
|
|||||||
@ -33,4 +33,10 @@ public class AiragFlowDTO implements Serializable {
|
|||||||
* 输入参数
|
* 输入参数
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> inputParams;
|
private Map<String, Object> inputParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否流式返回
|
||||||
|
*/
|
||||||
|
private boolean isStream;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,4 +47,8 @@ public interface TenantConstant {
|
|||||||
*/
|
*/
|
||||||
String APP_ADMIN = "appAdmin";
|
String APP_ADMIN = "appAdmin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加SignatureCheck注解POST请求的URL
|
||||||
|
*/
|
||||||
|
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,7 @@ package org.jeecg.common.system.util;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.system.annotation.EnumDict;
|
import org.jeecg.common.system.annotation.EnumDict;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
import org.jeecg.common.util.SpringContextUtils;
|
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
@ -13,6 +11,7 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
|||||||
import org.springframework.core.type.classreading.MetadataReader;
|
import org.springframework.core.type.classreading.MetadataReader;
|
||||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@ -183,10 +182,10 @@ public class ResourceUtil {
|
|||||||
for (DictModel dm : dictItemList) {
|
for (DictModel dm : dictItemList) {
|
||||||
String value = dm.getValue();
|
String value = dm.getValue();
|
||||||
if (keySet.contains(value)) {
|
if (keySet.contains(value)) {
|
||||||
List<DictModel> list = new ArrayList<>();
|
// 修复bug:获取或创建该dictCode对应的list,而不是每次都创建新的list
|
||||||
|
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
|
||||||
list.add(new DictModel(value, dm.getText()));
|
list.add(new DictModel(value, dm.getText()));
|
||||||
map.put(code, list);
|
//break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,9 @@ public class CommonUtils {
|
|||||||
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
|
||||||
String dbPath = null;
|
String dbPath = null;
|
||||||
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
String fileName = "image" + Math.round(Math.random() * 100000000000L);
|
||||||
fileName += "." + PoiPublicUtil.getFileExtendName(data);
|
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
||||||
|
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
|
||||||
|
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的,导致不展示---
|
||||||
try {
|
try {
|
||||||
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
||||||
File file = new File(basePath + File.separator + bizPath + File.separator );
|
File file = new File(basePath + File.separator + bizPath + File.separator );
|
||||||
|
|||||||
@ -46,7 +46,7 @@ public class RestUtil {
|
|||||||
|
|
||||||
public static String getBaseUrl() {
|
public static String getBaseUrl() {
|
||||||
String basepath = getDomain() + getPath();
|
String basepath = getDomain() + getPath();
|
||||||
log.info(" RestUtil.getBaseUrl: " + basepath);
|
log.debug(" RestUtil.getBaseUrl: " + basepath);
|
||||||
return basepath;
|
return basepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ public class RestUtil {
|
|||||||
* @return ResponseEntity<responseType>
|
* @return ResponseEntity<responseType>
|
||||||
*/
|
*/
|
||||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
|
||||||
log.info(" RestUtil --- request --- url = "+ url);
|
log.debug(" RestUtil --- request --- url = "+ url);
|
||||||
if (StringUtils.isEmpty(url)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
}
|
}
|
||||||
@ -230,7 +230,7 @@ public class RestUtil {
|
|||||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||||
headers.setContentLength(contentLength);
|
headers.setContentLength(contentLength);
|
||||||
log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
log.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 发送请求
|
// 发送请求
|
||||||
@ -252,7 +252,7 @@ public class RestUtil {
|
|||||||
*/
|
*/
|
||||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
||||||
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
||||||
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
log.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||||
|
|
||||||
if (StringUtils.isEmpty(url)) {
|
if (StringUtils.isEmpty(url)) {
|
||||||
throw new RuntimeException("url 不能为空");
|
throw new RuntimeException("url 不能为空");
|
||||||
@ -302,7 +302,7 @@ public class RestUtil {
|
|||||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||||
headers.setContentLength(contentLength);
|
headers.setContentLength(contentLength);
|
||||||
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
log.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1232,4 +1232,25 @@ public class oConvertUtils {
|
|||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String转换long类型
|
||||||
|
*
|
||||||
|
* @param v
|
||||||
|
* @param def
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static long getLong(Object v, long def) {
|
||||||
|
if (v == null) {
|
||||||
|
return def;
|
||||||
|
};
|
||||||
|
if (v instanceof Number) {
|
||||||
|
return ((Number) v).longValue();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(v.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.jeecg.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai配置类,通用的配置可以放到这里面
|
||||||
|
*
|
||||||
|
* @Author: wangshuai
|
||||||
|
* @Date: 2025/12/17 14:00
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
|
||||||
|
public class AiRagConfigBean {
|
||||||
|
public static final String PREFIX = "jeecg.airag";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感节点
|
||||||
|
* stdio mpc命令行功能开启,sql:AI流程SQL节点开启
|
||||||
|
*/
|
||||||
|
private String allowSensitiveNodes = "";
|
||||||
|
}
|
||||||
@ -12,7 +12,6 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author eightmonth@qq.com
|
|
||||||
* 启动程序修改DruidWallConfig配置
|
* 启动程序修改DruidWallConfig配置
|
||||||
* 允许SELECT语句的WHERE子句是一个永真条件
|
* 允许SELECT语句的WHERE子句是一个永真条件
|
||||||
* @author eightmonth
|
* @author eightmonth
|
||||||
|
|||||||
@ -136,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
|
|||||||
return new OpenAPI()
|
return new OpenAPI()
|
||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("JeecgBoot 后台服务API接口文档")
|
.title("JeecgBoot 后台服务API接口文档")
|
||||||
.version("3.9.0")
|
.version("3.9.1")
|
||||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||||
.description("后台API接口")
|
.description("后台API接口")
|
||||||
.termsOfService("NO terms of service")
|
.termsOfService("NO terms of service")
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.jeecg.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务调度器配置
|
||||||
|
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
|
||||||
|
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
|
||||||
|
*
|
||||||
|
* @author jeecg
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class TaskSchedulerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
|
||||||
|
public ThreadPoolTaskScheduler taskScheduler() {
|
||||||
|
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
|
||||||
|
|
||||||
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
|
scheduler.setPoolSize(10);
|
||||||
|
scheduler.setThreadNamePrefix("airag-scheduler-");
|
||||||
|
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
scheduler.setAwaitTerminationSeconds(60);
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -177,6 +177,8 @@ public class ShiroConfig {
|
|||||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
||||||
//仪表盘(按钮通信)
|
//仪表盘(按钮通信)
|
||||||
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
|
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
|
||||||
|
//App vue3版本查询版本接口
|
||||||
|
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
||||||
|
|
||||||
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
||||||
//filterChainDefinitionMap.put("/actuator/**", "anon");
|
//filterChainDefinitionMap.put("/actuator/**", "anon");
|
||||||
@ -188,6 +190,8 @@ public class ShiroConfig {
|
|||||||
// 企业微信证书排除
|
// 企业微信证书排除
|
||||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
||||||
|
|
||||||
|
filterChainDefinitionMap.put("/openapi/call/**", "anon");
|
||||||
|
|
||||||
// 添加自己的过滤器并且取名为jwt
|
// 添加自己的过滤器并且取名为jwt
|
||||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
||||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||||
@ -227,6 +231,11 @@ public class ShiroConfig {
|
|||||||
registration.addUrlPatterns("/airag/app/debug");
|
registration.addUrlPatterns("/airag/app/debug");
|
||||||
registration.addUrlPatterns("/airag/app/prompt/generate");
|
registration.addUrlPatterns("/airag/app/prompt/generate");
|
||||||
registration.addUrlPatterns("/airag/chat/receive/**");
|
registration.addUrlPatterns("/airag/chat/receive/**");
|
||||||
|
// 添加SSE接口的异步支持
|
||||||
|
registration.addUrlPatterns("/airag/extData/evaluator/debug");
|
||||||
|
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
|
||||||
|
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
|
||||||
|
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
|
||||||
//支持异步
|
//支持异步
|
||||||
registration.setAsyncSupported(true);
|
registration.setAsyncSupported(true);
|
||||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
package org.jeecg.config.sign.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名校验注解
|
||||||
|
* 用于方法级别的签名验证,功能等同于yml中的jeecg.signUrls配置
|
||||||
|
* 参考DragSignatureAspect的设计思路,使用AOP切面实现
|
||||||
|
*
|
||||||
|
* @author GitHub Copilot
|
||||||
|
* @since 2025-12-15
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface SignatureCheck {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用签名校验
|
||||||
|
* @return true-启用(默认), false-禁用
|
||||||
|
*/
|
||||||
|
boolean enabled() default true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名校验失败时的错误消息
|
||||||
|
* @return 错误消息
|
||||||
|
*/
|
||||||
|
String errorMessage() default "Sign签名校验失败!";
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package org.jeecg.config.sign.aspect;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.jeecg.config.sign.annotation.SignatureCheck;
|
||||||
|
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于AOP的签名验证切面
|
||||||
|
* 复用SignAuthInterceptor的成熟签名验证逻辑
|
||||||
|
*
|
||||||
|
* @author GitHub Copilot
|
||||||
|
* @since 2025-12-15
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Slf4j
|
||||||
|
@Component("signatureCheckAspect")
|
||||||
|
public class SignatureCheckAspect {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复用SignAuthInterceptor的签名验证逻辑
|
||||||
|
*/
|
||||||
|
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
|
||||||
|
*/
|
||||||
|
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
|
||||||
|
private void signatureCheckPointCut() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始验签
|
||||||
|
*/
|
||||||
|
@Before("signatureCheckPointCut()")
|
||||||
|
public void doSignatureValidation(JoinPoint point) throws Exception {
|
||||||
|
// 获取方法上的注解
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
|
Method method = signature.getMethod();
|
||||||
|
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
|
||||||
|
|
||||||
|
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
|
||||||
|
|
||||||
|
// 如果注解被禁用,直接返回
|
||||||
|
if (!signatureCheck.enabled()) {
|
||||||
|
log.info("签名验证已禁用,跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||||
|
Object bodyParam = null;
|
||||||
|
Object[] args = point.getArgs();
|
||||||
|
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
Object arg = args[i];
|
||||||
|
Annotation[] annotations = parameterAnnotations[i];
|
||||||
|
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
|
||||||
|
if (hasRequestBodyAnnotation) {
|
||||||
|
// 捕获携带@RequestBody注解的参数,供签名校验使用
|
||||||
|
bodyParam = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||||
|
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
log.error("无法获取请求上下文");
|
||||||
|
throw new IllegalArgumentException("无法获取请求上下文");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 直接调用SignAuthInterceptor的验证逻辑
|
||||||
|
signAuthInterceptor.validateSignature(request, bodyParam);
|
||||||
|
log.info("AOP签名验证通过");
|
||||||
|
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 使用注解中配置的错误消息,或者保留原始错误消息
|
||||||
|
String errorMessage = signatureCheck.errorMessage();
|
||||||
|
log.error("AOP签名验证失败: {}", e.getMessage());
|
||||||
|
|
||||||
|
if ("Sign签名校验失败!".equals(errorMessage)) {
|
||||||
|
// 如果是默认错误消息,使用原始的详细错误信息
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
// 如果是自定义错误消息,使用自定义消息
|
||||||
|
throw new IllegalArgumentException(errorMessage, e);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 包装其他异常
|
||||||
|
String errorMessage = signatureCheck.errorMessage();
|
||||||
|
log.error("AOP签名验证异常: {}", e.getMessage());
|
||||||
|
throw new IllegalArgumentException(errorMessage, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package org.jeecg.config.sign.interceptor;
|
package org.jeecg.config.sign.interceptor;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jeecg.common.constant.TenantConstant;
|
||||||
import org.jeecg.common.util.PathMatcherUtil;
|
import org.jeecg.common.util.PathMatcherUtil;
|
||||||
import org.jeecg.config.JeecgBaseConfig;
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
import org.jeecg.config.filter.RequestBodyReserveFilter;
|
||||||
@ -64,6 +65,8 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
|||||||
//------------------------------------------------------------
|
//------------------------------------------------------------
|
||||||
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
||||||
registration.addUrlPatterns(signUrlsArray);
|
registration.addUrlPatterns(signUrlsArray);
|
||||||
|
// 增加注解签名请求
|
||||||
|
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,23 +33,56 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
log.debug("Sign Interceptor request URI = " + request.getRequestURI());
|
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用验证逻辑
|
||||||
|
validateSignature(request);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 验证失败,返回错误响应
|
||||||
|
log.error("Sign 签名校验失败!{}", e.getMessage());
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setContentType("application/json; charset=utf-8");
|
||||||
|
PrintWriter out = response.getWriter();
|
||||||
|
Result<?> result = Result.error(e.getMessage());
|
||||||
|
out.print(JSON.toJSON(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名验证核心逻辑
|
||||||
|
* 提取出来供AOP切面复用
|
||||||
|
* @param request HTTP请求
|
||||||
|
* @throws IllegalArgumentException 验证失败时抛出异常
|
||||||
|
*/
|
||||||
|
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
|
||||||
|
validateSignature(request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名验证核心逻辑
|
||||||
|
* 提取出来供AOP切面复用
|
||||||
|
* @param request HTTP请求
|
||||||
|
* @throws IllegalArgumentException 验证失败时抛出异常
|
||||||
|
*/
|
||||||
|
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
|
||||||
|
try {
|
||||||
|
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
|
||||||
|
|
||||||
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
|
||||||
//获取全部参数(包括URL和body上的)
|
//获取全部参数(包括URL和body上的)
|
||||||
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
|
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
|
||||||
|
log.debug("提取参数: {}", allParams);
|
||||||
|
|
||||||
//对参数进行签名验证
|
//对参数进行签名验证
|
||||||
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
String headerSign = request.getHeader(CommonConstant.X_SIGN);
|
||||||
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
|
||||||
|
|
||||||
if(oConvertUtils.isEmpty(xTimestamp)){
|
if(oConvertUtils.isEmpty(xTimestamp)){
|
||||||
Result<?> result = Result.error("Sign签名校验失败,时间戳为空!");
|
log.error("Sign签名校验失败,时间戳为空!");
|
||||||
log.error("Sign 签名校验失败!Header xTimestamp 为空");
|
throw new IllegalArgumentException("Sign签名校验失败,请求参数不完整!");
|
||||||
//校验失败返回前端
|
|
||||||
response.setCharacterEncoding("UTF-8");
|
|
||||||
response.setContentType("application/json; charset=utf-8");
|
|
||||||
PrintWriter out = response.getWriter();
|
|
||||||
out.print(JSON.toJSON(result));
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//客户端时间
|
//客户端时间
|
||||||
@ -60,15 +93,24 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
//1.校验签名时间(兼容X_TIMESTAMP的新老格式)
|
||||||
if (xTimestamp.length() == length) {
|
if (xTimestamp.length() == length) {
|
||||||
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143)
|
||||||
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
|
long currentTimestamp = DateUtils.getCurrentTimestamp();
|
||||||
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
long timeDiff = currentTimestamp - clientTimestamp;
|
||||||
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
|
||||||
|
|
||||||
|
if (timeDiff > MAX_EXPIRE) {
|
||||||
|
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
|
||||||
|
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
//b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000)
|
||||||
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
|
long currentTime = System.currentTimeMillis();
|
||||||
log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!");
|
long timeDiff = currentTime - clientTimestamp;
|
||||||
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
|
long maxExpireMs = MAX_EXPIRE * length1000;
|
||||||
|
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
|
||||||
|
|
||||||
|
if (timeDiff > maxExpireMs) {
|
||||||
|
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
|
||||||
|
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,19 +118,18 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
|||||||
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
|
||||||
|
|
||||||
if (isSigned) {
|
if (isSigned) {
|
||||||
log.debug("Sign 签名通过!Header Sign : {}",headerSign);
|
log.debug("签名验证通过");
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
log.debug("sign allParams: {}", allParams);
|
log.error("签名验证失败, 参数: {}", allParams);
|
||||||
log.error("request URI = " + request.getRequestURI());
|
throw new IllegalArgumentException("Sign签名校验失败!");
|
||||||
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
}
|
||||||
//校验失败返回前端
|
} catch (IllegalArgumentException e) {
|
||||||
response.setCharacterEncoding("UTF-8");
|
// 重新抛出签名验证异常
|
||||||
response.setContentType("application/json; charset=utf-8");
|
throw e;
|
||||||
PrintWriter out = response.getWriter();
|
} catch (Exception e) {
|
||||||
Result<?> result = Result.error("Sign签名校验失败!");
|
// 包装其他异常(如IOException)
|
||||||
out.print(JSON.toJSON(result));
|
log.error("签名验证异常: {}", e.getMessage());
|
||||||
return false;
|
throw new IllegalArgumentException("Sign签名校验失败:" + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ public class HttpUtils {
|
|||||||
* @date 20210621
|
* @date 20210621
|
||||||
* @param request
|
* @param request
|
||||||
*/
|
*/
|
||||||
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
|
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException {
|
||||||
|
|
||||||
SortedMap<String, String> result = new TreeMap<>();
|
SortedMap<String, String> result = new TreeMap<>();
|
||||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||||
@ -65,8 +65,14 @@ public class HttpUtils {
|
|||||||
Map<String, String> allRequestParam = new HashMap<>(16);
|
Map<String, String> allRequestParam = new HashMap<>(16);
|
||||||
// get请求不需要拿body参数
|
// get请求不需要拿body参数
|
||||||
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
||||||
|
if (bodyParam != null) {
|
||||||
|
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||||
|
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
|
||||||
|
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||||
|
} else {
|
||||||
allRequestParam = getAllRequestParam(request);
|
allRequestParam = getAllRequestParam(request);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 将URL的参数和body参数进行合并
|
// 将URL的参数和body参数进行合并
|
||||||
if (allRequestParam != null) {
|
if (allRequestParam != null) {
|
||||||
for (Map.Entry entry : allRequestParam.entrySet()) {
|
for (Map.Entry entry : allRequestParam.entrySet()) {
|
||||||
|
|||||||
@ -11,7 +11,12 @@ import org.springframework.util.DigestUtils;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签名工具类
|
* 签名工具类
|
||||||
@ -49,12 +54,7 @@ public class SignUtil {
|
|||||||
String paramsJsonStr = JSONObject.toJSONString(params);
|
String paramsJsonStr = JSONObject.toJSONString(params);
|
||||||
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
||||||
//设置签名秘钥
|
//设置签名秘钥
|
||||||
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
String signatureSecret = SignUtil.getSignatureSecret();
|
||||||
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
|
||||||
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
|
||||||
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
|
|
||||||
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
||||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
||||||
@ -63,4 +63,129 @@ public class SignUtil {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过前端签名算法生成签名
|
||||||
|
*
|
||||||
|
* @param url 请求的完整URL(包含查询参数)
|
||||||
|
* @param requestParams 使用 @RequestParam 获取的参数集合
|
||||||
|
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
|
||||||
|
* @return 计算得到的签名(大写MD5),若参数不足返回 null
|
||||||
|
*/
|
||||||
|
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
|
||||||
|
if (oConvertUtils.isEmpty(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 解析URL上的查询参数与路径变量
|
||||||
|
Map<String, String> urlParams = parseQueryString(url);
|
||||||
|
// 合并URL参数与@RequestParam参数,确保数值和布尔类型转换为字符串
|
||||||
|
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
|
||||||
|
// 按需合并@RequestBody参数
|
||||||
|
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
|
||||||
|
mergedParams = mergeObject(mergedParams, requestBodyParams);
|
||||||
|
}
|
||||||
|
// 按键名升序排序,保持与前端一致的签名顺序
|
||||||
|
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
|
||||||
|
// 去除时间戳字段,避免参与签名
|
||||||
|
sortedParams.remove("_t");
|
||||||
|
// 序列化为JSON字符串
|
||||||
|
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
|
||||||
|
// 读取签名秘钥
|
||||||
|
String signatureSecret = getSignatureSecret();
|
||||||
|
// 计算MD5摘要并转大写
|
||||||
|
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析URL中的查询参数,并处理末尾逗号分隔的路径变量片段。
|
||||||
|
*
|
||||||
|
* @param url 请求的完整URL
|
||||||
|
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
|
||||||
|
*/
|
||||||
|
private static Map<String, String> parseQueryString(String url) {
|
||||||
|
Map<String, String> result = new HashMap<>(16);
|
||||||
|
int fragmentIndex = url.indexOf('#');
|
||||||
|
if (fragmentIndex >= 0) {
|
||||||
|
url = url.substring(0, fragmentIndex);
|
||||||
|
}
|
||||||
|
int questionIndex = url.indexOf('?');
|
||||||
|
String paramString = null;
|
||||||
|
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
|
||||||
|
paramString = url.substring(questionIndex + 1);
|
||||||
|
}
|
||||||
|
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
|
||||||
|
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
|
||||||
|
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
|
||||||
|
String lastPathVariable = url.substring(lastSlashIndex + 1);
|
||||||
|
int qIndexInPath = lastPathVariable.indexOf('?');
|
||||||
|
if (qIndexInPath >= 0) {
|
||||||
|
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
|
||||||
|
}
|
||||||
|
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
|
||||||
|
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
|
||||||
|
result.put(X_PATH_VARIABLE, decodedPathVariable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isNotEmpty(paramString)) {
|
||||||
|
String[] pairs = paramString.split(SymbolConstant.AND);
|
||||||
|
for (String pair : pairs) {
|
||||||
|
int equalIndex = pair.indexOf('=');
|
||||||
|
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
|
||||||
|
String key = pair.substring(0, equalIndex);
|
||||||
|
String value = pair.substring(equalIndex + 1);
|
||||||
|
// 解码并统一类型为字符串
|
||||||
|
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
|
||||||
|
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
|
||||||
|
result.put(decodedKey, decodedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
|
||||||
|
*
|
||||||
|
* @param target 初始参数映射
|
||||||
|
* @param source 待合并的参数映射
|
||||||
|
* @return 合并后的新映射
|
||||||
|
*/
|
||||||
|
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
|
||||||
|
Map<String, String> merged = new HashMap<>(16);
|
||||||
|
if (target != null && !target.isEmpty()) {
|
||||||
|
merged.putAll(target);
|
||||||
|
}
|
||||||
|
if (source != null && !source.isEmpty()) {
|
||||||
|
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof Number) {
|
||||||
|
// 数值类型转字符串,保持前后端一致
|
||||||
|
merged.put(key, String.valueOf(value));
|
||||||
|
} else if (value instanceof Boolean) {
|
||||||
|
// 布尔类型转字符串,保持前后端一致
|
||||||
|
merged.put(key, String.valueOf(value));
|
||||||
|
} else if (value != null) {
|
||||||
|
merged.put(key, String.valueOf(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取并校验签名秘钥配置。
|
||||||
|
*
|
||||||
|
* @return 有效的签名秘钥
|
||||||
|
*/
|
||||||
|
private static String getSignatureSecret() {
|
||||||
|
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||||
|
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
|
||||||
|
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||||
|
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
|
||||||
|
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 !!");
|
||||||
|
}
|
||||||
|
return signatureSecret;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,429 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
MCP Stdio 工具 - 修复编码问题
|
||||||
|
确保所有输出都使用UTF-8编码
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from typing import Dict, Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 强制使用UTF-8编码
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# Windows需要特殊处理
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||||
|
else:
|
||||||
|
# Unix-like系统
|
||||||
|
sys.stdin.reconfigure(encoding='utf-8')
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||||
|
os.environ['PYTHONUTF8'] = '1'
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("mcp-tool")
|
||||||
|
|
||||||
|
|
||||||
|
class FixedMCPServer:
|
||||||
|
"""修复编码问题的MCP服务器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.tools = {}
|
||||||
|
self.initialize_tools()
|
||||||
|
|
||||||
|
def initialize_tools(self):
|
||||||
|
"""初始化工具集"""
|
||||||
|
|
||||||
|
# 获取时间
|
||||||
|
self.tools["get_time"] = {
|
||||||
|
"name": "get_time",
|
||||||
|
"description": "获取当前时间",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "时间格式",
|
||||||
|
"enum": ["iso", "timestamp", "human", "chinese"],
|
||||||
|
"default": "iso"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 文本处理工具
|
||||||
|
self.tools["text_process"] = {
|
||||||
|
"name": "text_process",
|
||||||
|
"description": "文本处理工具",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"text": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "输入文本"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "操作类型",
|
||||||
|
"enum": ["length", "upper", "lower", "reverse", "count_words"],
|
||||||
|
"default": "length"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["text"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 数据格式工具
|
||||||
|
self.tools["format_data"] = {
|
||||||
|
"name": "format_data",
|
||||||
|
"description": "格式化数据",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "原始数据"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "格式类型",
|
||||||
|
"enum": ["json", "yaml", "xml"],
|
||||||
|
"default": "json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["data"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""处理请求"""
|
||||||
|
try:
|
||||||
|
method = request.get("method")
|
||||||
|
params = request.get("params", {})
|
||||||
|
|
||||||
|
if method == "tools/list":
|
||||||
|
return self.handle_tools_list()
|
||||||
|
elif method == "tools/call":
|
||||||
|
return self.handle_tool_call(params)
|
||||||
|
elif method == "ping":
|
||||||
|
return {"result": "pong"}
|
||||||
|
else:
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32601,
|
||||||
|
message="Method not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling request: {e}")
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32603,
|
||||||
|
message=f"Internal error: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_tools_list(self) -> Dict[str, Any]:
|
||||||
|
"""列出所有工具 - 确保返回标准JSON"""
|
||||||
|
return {
|
||||||
|
"result": {
|
||||||
|
"tools": list(self.tools.values())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""调用工具 - 修复响应格式"""
|
||||||
|
name = params.get("name")
|
||||||
|
arguments = params.get("arguments", {})
|
||||||
|
|
||||||
|
if name not in self.tools:
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32602,
|
||||||
|
message=f"Tool '{name}' not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if name == "get_time":
|
||||||
|
result = self.execute_get_time(arguments)
|
||||||
|
elif name == "text_process":
|
||||||
|
result = self.execute_text_process(arguments)
|
||||||
|
elif name == "format_data":
|
||||||
|
result = self.execute_format_data(arguments)
|
||||||
|
else:
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32602,
|
||||||
|
message="Tool not implemented"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 确保返回正确的MCP响应格式
|
||||||
|
return self.create_success_response(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Tool execution error: {e}")
|
||||||
|
return self.create_error_response(
|
||||||
|
code=-32603,
|
||||||
|
message=f"Tool execution failed: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""获取时间 - 支持中文"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
try:
|
||||||
|
format_type = args.get("format", "iso")
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
if format_type == "iso":
|
||||||
|
result = now.isoformat()
|
||||||
|
elif format_type == "timestamp":
|
||||||
|
result = now.timestamp()
|
||||||
|
elif format_type == "human":
|
||||||
|
result = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
elif format_type == "chinese":
|
||||||
|
result = now.strftime("%Y年%m月%d日 %H时%M分%S秒")
|
||||||
|
else:
|
||||||
|
result = now.isoformat()
|
||||||
|
logger.info(f"当前系统时间:{result}")
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"format": format_type,
|
||||||
|
"time": result,
|
||||||
|
"timestamp": now.timestamp(),
|
||||||
|
"date": now.strftime("%Y-%m-%d"),
|
||||||
|
"time_12h": now.strftime("%I:%M:%S %p")
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""文本处理"""
|
||||||
|
try:
|
||||||
|
text = args.get("text", "")
|
||||||
|
operation = args.get("operation", "length")
|
||||||
|
|
||||||
|
if operation == "length":
|
||||||
|
result = len(text)
|
||||||
|
result_str = f"文本长度: {result} 个字符"
|
||||||
|
elif operation == "upper":
|
||||||
|
result = text.upper()
|
||||||
|
result_str = f"大写: {result}"
|
||||||
|
elif operation == "lower":
|
||||||
|
result = text.lower()
|
||||||
|
result_str = f"小写: {result}"
|
||||||
|
elif operation == "reverse":
|
||||||
|
result = text[::-1]
|
||||||
|
result_str = f"反转: {result}"
|
||||||
|
elif operation == "count_words":
|
||||||
|
words = len(text.split())
|
||||||
|
result = words
|
||||||
|
result_str = f"单词数: {words}"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"未知操作: {operation}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"operation": operation,
|
||||||
|
"original_text": text,
|
||||||
|
"result": result,
|
||||||
|
"result_str": result_str,
|
||||||
|
"text_length": len(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e),
|
||||||
|
"operation": args.get("operation", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""格式化数据"""
|
||||||
|
try:
|
||||||
|
data_str = args.get("data", "")
|
||||||
|
format_type = args.get("format", "json")
|
||||||
|
|
||||||
|
# 尝试解析为JSON
|
||||||
|
try:
|
||||||
|
data = json.loads(data_str)
|
||||||
|
is_json = True
|
||||||
|
except:
|
||||||
|
data = data_str
|
||||||
|
is_json = False
|
||||||
|
|
||||||
|
if format_type == "json":
|
||||||
|
if is_json:
|
||||||
|
result = json.dumps(data, ensure_ascii=False, indent=2)
|
||||||
|
else:
|
||||||
|
# 如果不是JSON,包装成JSON
|
||||||
|
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
|
||||||
|
elif format_type == "yaml":
|
||||||
|
import yaml
|
||||||
|
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
|
||||||
|
elif format_type == "xml":
|
||||||
|
# 简单的XML格式化
|
||||||
|
if isinstance(data, dict):
|
||||||
|
result = "<data>"
|
||||||
|
for k, v in data.items():
|
||||||
|
result += f"\n <{k}>{v}</{k}>"
|
||||||
|
result += "\n</data>"
|
||||||
|
else:
|
||||||
|
result = f"<text>{data}</text>"
|
||||||
|
else:
|
||||||
|
result = str(data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"format": format_type,
|
||||||
|
"original": data_str,
|
||||||
|
"formatted": result,
|
||||||
|
"length": len(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e),
|
||||||
|
"format": args.get("format", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""创建成功响应 - 确保符合MCP规范"""
|
||||||
|
# 将数据转换为JSON字符串作为文本内容
|
||||||
|
content_text = json.dumps(data, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"result": {
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": content_text
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isError": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
|
||||||
|
"""创建错误响应"""
|
||||||
|
return {
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def safe_json_dump(data: Dict[str, Any]) -> str:
|
||||||
|
"""安全的JSON序列化,确保UTF-8编码"""
|
||||||
|
try:
|
||||||
|
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
||||||
|
except:
|
||||||
|
# 如果失败,使用ASCII转义
|
||||||
|
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数 - 修复Stdio通信"""
|
||||||
|
logger.info("启动MCP Stdio服务器 (修复编码版)...")
|
||||||
|
|
||||||
|
server = FixedMCPServer()
|
||||||
|
|
||||||
|
# 初始握手消息
|
||||||
|
init_message = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {
|
||||||
|
"tools": {}
|
||||||
|
},
|
||||||
|
"serverInfo": {
|
||||||
|
"name": "fixed-mcp-server",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送初始化响应
|
||||||
|
try:
|
||||||
|
sys.stdout.write(safe_json_dump(init_message) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送初始化消息失败: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("MCP服务器已初始化")
|
||||||
|
|
||||||
|
# 主循环
|
||||||
|
line_num = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = sys.stdin.readline()
|
||||||
|
if not line:
|
||||||
|
logger.info("输入流结束")
|
||||||
|
break
|
||||||
|
|
||||||
|
line = line.strip()
|
||||||
|
line_num += 1
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
request = json.loads(line)
|
||||||
|
logger.info(f"解析请求: {request.get('method', 'unknown')}")
|
||||||
|
|
||||||
|
# 处理请求
|
||||||
|
response = server.handle_request(request)
|
||||||
|
response["jsonrpc"] = "2.0"
|
||||||
|
response["id"] = request.get("id")
|
||||||
|
|
||||||
|
# 发送响应
|
||||||
|
response_json = safe_json_dump(response)
|
||||||
|
sys.stdout.write(response_json + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"JSON解析错误: {e}")
|
||||||
|
error_response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"error": {
|
||||||
|
"code": -32700,
|
||||||
|
"message": f"Parse error at line {line_num}"
|
||||||
|
},
|
||||||
|
"id": None
|
||||||
|
}
|
||||||
|
sys.stdout.write(safe_json_dump(error_response) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("接收到中断信号")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"未处理的错误: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info("MCP服务器已停止")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-boot-module</artifactId>
|
<artifactId>jeecg-boot-module</artifactId>
|
||||||
<version>3.9.0</version>
|
<version>3.9.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<kotlin.version>2.2.0</kotlin.version>
|
<kotlin.version>2.2.0</kotlin.version>
|
||||||
<liteflow.version>2.15.0</liteflow.version>
|
<liteflow.version>2.15.0</liteflow.version>
|
||||||
<apache-tika.version>2.9.1</apache-tika.version>
|
<apache-tika.version>3.2.3</apache-tika.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@ -41,14 +41,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-bom</artifactId>
|
<artifactId>langchain4j-bom</artifactId>
|
||||||
<version>1.3.0</version>
|
<version>1.9.1</version>
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-community-bom</artifactId>
|
<artifactId>langchain4j-community-bom</artifactId>
|
||||||
<version>1.3.0-beta9</version>
|
<version>1.9.1-beta17</version>
|
||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@ -75,7 +75,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<artifactId>jeecg-aiflow</artifactId>
|
<artifactId>jeecg-aiflow</artifactId>
|
||||||
<version>3.9.0</version>
|
<version>3.9.1-beta</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
@ -107,16 +107,16 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>liteflow-script-python</artifactId>
|
<artifactId>liteflow-script-groovy</artifactId>
|
||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||||
|
|
||||||
<!-- aiflow 脚本依赖 -->
|
<!-- aiflow 脚本依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>liteflow-script-groovy</artifactId>
|
<artifactId>liteflow-script-python</artifactId>
|
||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@ -151,6 +151,11 @@
|
|||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-open-ai</artifactId>
|
<artifactId>langchain4j-open-ai</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- langChain4j mcp support -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-mcp</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-ollama</artifactId>
|
<artifactId>langchain4j-ollama</artifactId>
|
||||||
@ -197,7 +202,11 @@
|
|||||||
<artifactId>langchain4j-pgvector</artifactId>
|
<artifactId>langchain4j-pgvector</artifactId>
|
||||||
<version>1.3.0-beta9</version>
|
<version>1.3.0-beta9</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- langChain4j Document Parser -->
|
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.tika</groupId>
|
<groupId>org.apache.tika</groupId>
|
||||||
<artifactId>tika-core</artifactId>
|
<artifactId>tika-core</artifactId>
|
||||||
@ -224,7 +233,12 @@
|
|||||||
<artifactId>tika-parser-text-module</artifactId>
|
<artifactId>tika-parser-text-module</artifactId>
|
||||||
<version>${apache-tika.version}</version>
|
<version>${apache-tika.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- word模版引擎 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.deepoove</groupId>
|
||||||
|
<artifactId>poi-tl</artifactId>
|
||||||
|
<version>1.12.2</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@ -44,4 +44,19 @@ public class AiAppConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
|
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启记忆
|
||||||
|
*/
|
||||||
|
public static final Integer IZ_OPEN_MEMORY = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话标题最大长度
|
||||||
|
*/
|
||||||
|
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI写作的应用id
|
||||||
|
*/
|
||||||
|
public static final String WRITER_APP_ID = "2010634128233779202";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,4 +104,68 @@ public class Prompts {
|
|||||||
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
||||||
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
||||||
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提示词生成角色及通用要求
|
||||||
|
*/
|
||||||
|
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
|
||||||
|
"你是一位AI提示词专家,请根据提供的配置信息,生成针对AI智能体的“使用指南”提示词。\n" +
|
||||||
|
"\n" +
|
||||||
|
"## 通用要求\n" +
|
||||||
|
"1. 生成的内容将作为系统提示词的一部分。\n" +
|
||||||
|
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
|
||||||
|
"3. **只输出提示词内容**,不要包含任何解释、寒暄或Markdown代码块标记。\n" +
|
||||||
|
"4. 语气专业、清晰、指令性强。\n" +
|
||||||
|
"5. 说明内容请使用中文。\n\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量生成提示词
|
||||||
|
*/
|
||||||
|
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
|
||||||
|
"### 输入信息\n" +
|
||||||
|
"**变量列表**:\n" +
|
||||||
|
"%s\n" +
|
||||||
|
"### 要求\n" +
|
||||||
|
"1. 请生成一段**变量使用指南**。\n" +
|
||||||
|
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
|
||||||
|
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}):\n" +
|
||||||
|
" 例如:针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
|
||||||
|
" 例如:针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
|
||||||
|
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
|
||||||
|
"4. **通用更新指令**:请在变量指南的最后,单独生成一条指令,明确指示AI:“当从用户对话中获取到上述变量(<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
|
||||||
|
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆库生成提示词
|
||||||
|
*/
|
||||||
|
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
|
||||||
|
"### 输入信息\n" +
|
||||||
|
"**记忆库描述**:\n" +
|
||||||
|
"%s\n" +
|
||||||
|
"### 要求\n" +
|
||||||
|
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
|
||||||
|
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
|
||||||
|
" - **全自动检索(强制优先)**:\n" +
|
||||||
|
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
|
||||||
|
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
|
||||||
|
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
|
||||||
|
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
|
||||||
|
" - **行为准则**:\n" +
|
||||||
|
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
|
||||||
|
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
|
||||||
|
" - **示例演示**:\n" +
|
||||||
|
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
|
||||||
|
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
|
||||||
|
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai写作提示词
|
||||||
|
*/
|
||||||
|
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai写作回复提示词
|
||||||
|
*/
|
||||||
|
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AI应用
|
* @Description: AI应用
|
||||||
@ -179,4 +178,16 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
|||||||
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据应用ID生成变量和记忆提示词 (SSE)
|
||||||
|
* for: 【QQYUN-14479】提示词单独拆分
|
||||||
|
* @param variables
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/prompt/generateMemoryByAppId")
|
||||||
|
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
|
||||||
|
@RequestParam(name = "memoryId") String memoryId) {
|
||||||
|
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import org.jeecg.common.constant.CommonConstant;
|
|||||||
import org.jeecg.common.util.CommonUtils;
|
import org.jeecg.common.util.CommonUtils;
|
||||||
import org.jeecg.config.shiro.IgnoreAuth;
|
import org.jeecg.config.shiro.IgnoreAuth;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -102,6 +103,19 @@ public class AiragChatController {
|
|||||||
return chatService.getConversations(appId);
|
return chatService.getConversations(appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型获取所有对话
|
||||||
|
*
|
||||||
|
* @return 返回一个Result对象,包含所有对话的信息
|
||||||
|
* @author wangshuai
|
||||||
|
* @date 2025/12/11 11:42
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/getConversationsByType")
|
||||||
|
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
|
||||||
|
return chatService.getConversationsByType(sessionType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*
|
*
|
||||||
@ -113,7 +127,22 @@ public class AiragChatController {
|
|||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@DeleteMapping(value = "/conversation/{id}")
|
@DeleteMapping(value = "/conversation/{id}")
|
||||||
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
||||||
return chatService.deleteConversation(id);
|
return chatService.deleteConversation(id,"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
* @author wangshuai
|
||||||
|
* @date 2025/12/11 20:00
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
|
||||||
|
public Result<?> deleteConversationByType(@PathVariable("id") String id,
|
||||||
|
@PathVariable("sessionType") String sessionType) {
|
||||||
|
return chatService.deleteConversation(id,sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,8 +168,9 @@ public class AiragChatController {
|
|||||||
*/
|
*/
|
||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@GetMapping(value = "/messages")
|
@GetMapping(value = "/messages")
|
||||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
|
||||||
return chatService.getMessages(conversationId);
|
@RequestParam(value = "sessionType", required = false) String sessionType) {
|
||||||
|
return chatService.getMessages(conversationId, sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,7 +183,21 @@ public class AiragChatController {
|
|||||||
@IgnoreAuth
|
@IgnoreAuth
|
||||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
||||||
return chatService.clearMessage(conversationId);
|
return chatService.clearMessage(conversationId, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空消息
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @author wangshuai
|
||||||
|
* @date 2025/12/11 19:06
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
|
||||||
|
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
|
||||||
|
@PathVariable(value = "sessionType") String sessionType) {
|
||||||
|
return chatService.clearMessage(conversationId, sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -217,4 +261,25 @@ public class AiragChatController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai海报生成
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping("/genAiPoster")
|
||||||
|
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
|
||||||
|
String imageUrl = chatService.genAiPoster(chatSendParams);
|
||||||
|
return Result.OK(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成ai写作
|
||||||
|
*
|
||||||
|
* @param aiWriteGenerateVo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping("/genAiWriter")
|
||||||
|
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
|
||||||
|
return chatService.genAiWriter(aiWriteGenerateVo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,6 +173,29 @@ public class AiragApp implements Serializable {
|
|||||||
@Schema(description = "插件")
|
@Schema(description = "插件")
|
||||||
private java.lang.String plugins;
|
private java.lang.String plugins;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启记忆(0 不开启,1开启)
|
||||||
|
*/
|
||||||
|
@Schema(description = "是否开启记忆(0 不开启,1开启)")
|
||||||
|
private java.lang.Integer izOpenMemory;
|
||||||
|
/**
|
||||||
|
* 记忆库,知识库的id
|
||||||
|
*/
|
||||||
|
@Schema(description = "记忆库")
|
||||||
|
private java.lang.String memoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量
|
||||||
|
*/
|
||||||
|
@Schema(description = "变量")
|
||||||
|
private java.lang.String variables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆和变量提示词
|
||||||
|
*/
|
||||||
|
@Schema(description = "记忆和变量提示词")
|
||||||
|
private java.lang.String memoryPrompt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库ids
|
* 知识库ids
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import org.jeecg.common.api.vo.Result;
|
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,4 +20,14 @@ public interface IAiragAppService extends IService<AiragApp> {
|
|||||||
* @date 2025/3/12 14:45
|
* @date 2025/3/12 14:45
|
||||||
*/
|
*/
|
||||||
Object generatePrompt(String prompt,boolean blocking);
|
Object generatePrompt(String prompt,boolean blocking);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据应用id生成提示词
|
||||||
|
*
|
||||||
|
* @param variables
|
||||||
|
* @param memoryId
|
||||||
|
* @param blocking
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package org.jeecg.modules.airag.app.service;
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
@ -59,21 +60,23 @@ public interface IAiragChatService {
|
|||||||
* 获取对话聊天记录
|
* 获取对话聊天记录
|
||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
|
* @param sessionType 类型
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/26 15:16
|
* @date 2025/2/26 15:16
|
||||||
*/
|
*/
|
||||||
Result<?> getMessages(String conversationId);
|
Result<?> getMessages(String conversationId, String sessionType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话
|
* 删除会话
|
||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
|
* @param sessionType
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/3/3 16:55
|
* @date 2025/3/3 16:55
|
||||||
*/
|
*/
|
||||||
Result<?> deleteConversation(String conversationId);
|
Result<?> deleteConversation(String conversationId, String sessionType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新会话标题
|
* 更新会话标题
|
||||||
@ -87,11 +90,12 @@ public interface IAiragChatService {
|
|||||||
/**
|
/**
|
||||||
* 清空消息
|
* 清空消息
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
|
* @param sessionType
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/3/3 19:49
|
* @date 2025/3/3 19:49
|
||||||
*/
|
*/
|
||||||
Result<?> clearMessage(String conversationId);
|
Result<?> clearMessage(String conversationId, String sessionType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化聊天(忽略租户)
|
* 初始化聊天(忽略租户)
|
||||||
@ -111,4 +115,27 @@ public interface IAiragChatService {
|
|||||||
* @date 2025/8/11 17:39
|
* @date 2025/8/11 17:39
|
||||||
*/
|
*/
|
||||||
SseEmitter receiveByRequestId(String requestId);
|
SseEmitter receiveByRequestId(String requestId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型获取会话列表
|
||||||
|
*
|
||||||
|
* @param sessionType
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Result<?> getConversationsByType(String sessionType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成海报图片
|
||||||
|
* @param chatSendParams
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String genAiPoster(ChatSendParams chatSendParams);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成ai创作
|
||||||
|
*
|
||||||
|
* @param chatSendParams
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service;
|
||||||
|
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
|
||||||
|
public interface IAiragVariableService {
|
||||||
|
/**
|
||||||
|
* 更新变量值
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
void updateVariable(String userId, String appId, String name, String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加提示词
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param app
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
String additionalPrompt(String username, AiragApp app);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化变量(仅不存在时设置)
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
|
void initVariable(String userId, String appId, String name, String defaultValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加变量更新工具
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @param aiApp
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.modules.airag.app.service.impl;
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import dev.langchain4j.data.message.AiMessage;
|
import dev.langchain4j.data.message.AiMessage;
|
||||||
@ -10,12 +11,15 @@ import dev.langchain4j.model.output.FinishReason;
|
|||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
import org.jeecg.common.util.UUIDGenerator;
|
import org.jeecg.common.util.UUIDGenerator;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.modules.airag.app.consts.Prompts;
|
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
@ -23,6 +27,8 @@ import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
|||||||
import org.jeecg.modules.airag.common.vo.event.EventData;
|
import org.jeecg.modules.airag.common.vo.event.EventData;
|
||||||
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
||||||
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
@ -31,6 +37,7 @@ import java.io.IOException;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AI应用
|
* @Description: AI应用
|
||||||
@ -45,6 +52,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
@Autowired
|
@Autowired
|
||||||
IAIChatHandler aiChatHandler;
|
IAIChatHandler aiChatHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAiragKnowledgeService airagKnowledgeService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object generatePrompt(String prompt, boolean blocking) {
|
public Object generatePrompt(String prompt, boolean blocking) {
|
||||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||||
@ -62,6 +72,92 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
}
|
}
|
||||||
return Result.OK("success", promptValue);
|
return Result.OK("success", promptValue);
|
||||||
}else{
|
}else{
|
||||||
|
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||||
|
return startSseChat(messages, params);
|
||||||
|
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||||
|
@Override
|
||||||
|
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
|
||||||
|
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
|
||||||
|
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
|
||||||
|
}
|
||||||
|
// 构建变量描述
|
||||||
|
StringBuilder variablesDesc = new StringBuilder();
|
||||||
|
if (oConvertUtils.isNotEmpty(variables)) {
|
||||||
|
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
|
||||||
|
if (variableList != null && !variableList.isEmpty()) {
|
||||||
|
for (AppVariableVo var : variableList) {
|
||||||
|
if (var.getEnable() != null && !var.getEnable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = var.getName();
|
||||||
|
if (oConvertUtils.isNotEmpty(var.getAction())) {
|
||||||
|
String action = var.getAction();
|
||||||
|
if (oConvertUtils.isNotEmpty(name)) {
|
||||||
|
try {
|
||||||
|
// 使用正则替换未被{{}}包裹的变量名
|
||||||
|
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
|
||||||
|
action = action.replaceAll(regex, "{{" + name + "}}");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("变量名替换异常: name={}", name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variablesDesc.append(action).append("\n");
|
||||||
|
} else {
|
||||||
|
variablesDesc.append("- {{").append(name).append("}}");
|
||||||
|
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||||
|
variablesDesc.append(": ").append(var.getDescription());
|
||||||
|
}
|
||||||
|
variablesDesc.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建Prompt
|
||||||
|
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
|
||||||
|
if (!variablesDesc.isEmpty()) {
|
||||||
|
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建记忆状态描述
|
||||||
|
if (oConvertUtils.isNotEmpty(memoryId)) {
|
||||||
|
String memoryDescr = "";
|
||||||
|
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
|
||||||
|
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
|
||||||
|
memoryDescr += "记忆库描述:" + memory.getDescr();
|
||||||
|
}
|
||||||
|
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
|
||||||
|
}
|
||||||
|
|
||||||
|
String prompt = promptBuilder.toString();
|
||||||
|
|
||||||
|
List<ChatMessage> messages = List.of(new UserMessage(prompt));
|
||||||
|
|
||||||
|
AIChatParams params = new AIChatParams();
|
||||||
|
params.setTemperature(0.7);
|
||||||
|
|
||||||
|
if(blocking){
|
||||||
|
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
|
||||||
|
if (promptValue == null || promptValue.isEmpty()) {
|
||||||
|
return Result.error("生成失败");
|
||||||
|
}
|
||||||
|
return Result.OK("success", promptValue);
|
||||||
|
}else{
|
||||||
|
return startSseChat(messages, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送聊天
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
|
||||||
SseEmitter emitter = new SseEmitter(-0L);
|
SseEmitter emitter = new SseEmitter(-0L);
|
||||||
// 异步运行(流式)
|
// 异步运行(流式)
|
||||||
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
||||||
@ -136,7 +232,7 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
|||||||
.start();
|
.start();
|
||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
}
|
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||||
|
|
||||||
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,24 +1,36 @@
|
|||||||
package org.jeecg.modules.airag.app.service.impl;
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import dev.langchain4j.agent.tool.ToolExecutionRequest;
|
import dev.langchain4j.agent.tool.ToolExecutionRequest;
|
||||||
|
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||||
|
import dev.langchain4j.data.document.Document;
|
||||||
import dev.langchain4j.data.image.Image;
|
import dev.langchain4j.data.image.Image;
|
||||||
import dev.langchain4j.data.message.*;
|
import dev.langchain4j.data.message.*;
|
||||||
import dev.langchain4j.model.output.FinishReason;
|
import dev.langchain4j.model.output.FinishReason;
|
||||||
import dev.langchain4j.service.TokenStream;
|
import dev.langchain4j.service.TokenStream;
|
||||||
|
import dev.langchain4j.service.tool.ToolExecutor;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.tika.parser.AutoDetectParser;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.system.api.ISysBaseAPI;
|
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||||
import org.jeecg.common.system.util.JwtUtil;
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
import org.jeecg.common.util.*;
|
import org.jeecg.common.util.*;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.config.vo.Path;
|
||||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||||
|
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||||
|
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||||
@ -35,18 +47,26 @@ import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
|||||||
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||||
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
|
||||||
|
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||||
|
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||||
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
||||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.redis.core.BoundValueOperations;
|
import org.springframework.data.redis.core.BoundValueOperations;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@ -86,6 +106,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
AiragModelMapper airagModelMapper;
|
AiragModelMapper airagModelMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragFlowPluginService airagFlowPluginService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragKnowledgeService airagKnowledgeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragVariableService airagVariableService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重新接收消息
|
* 重新接收消息
|
||||||
*/
|
*/
|
||||||
@ -105,10 +137,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
||||||
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
||||||
}
|
}
|
||||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId, chatSendParams.getSessionType());
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
// 更新标题
|
// 更新标题
|
||||||
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||||
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||||
|
chatConversation.setTitle(userMessage.length() > maxLength ? userMessage.substring(0, maxLength) : userMessage);
|
||||||
}
|
}
|
||||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
// 保存工作流入参配置(如果有)
|
// 保存工作流入参配置(如果有)
|
||||||
@ -116,6 +151,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
|
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
|
||||||
}
|
}
|
||||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
|
//是否保存会话
|
||||||
|
if(null != chatSendParams.getIzSaveSession()){
|
||||||
|
chatConversation.setIzSaveSession(chatSendParams.getIzSaveSession());
|
||||||
|
}
|
||||||
|
// 保存变量
|
||||||
|
saveVariables(app);
|
||||||
// 发送消息
|
// 发送消息
|
||||||
return doChat(chatConversation, topicId, chatSendParams);
|
return doChat(chatConversation, topicId, chatSendParams);
|
||||||
}
|
}
|
||||||
@ -130,7 +171,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
||||||
AiragApp app = appDebugParams.getApp();
|
AiragApp app = appDebugParams.getApp();
|
||||||
app.setId("__DEBUG_APP");
|
app.setId("__DEBUG_APP");
|
||||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId, "");
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
// 保存工作流入参配置(如果有)
|
// 保存工作流入参配置(如果有)
|
||||||
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
||||||
@ -140,7 +183,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
// 发送消息
|
// 发送消息
|
||||||
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
||||||
//保存会话
|
//保存会话
|
||||||
saveChatConversation(chatConversation, true, null);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
saveChatConversation(chatConversation, true, null, "");
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,9 +292,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<?> getMessages(String conversationId) {
|
public Result<?> getMessages(String conversationId, String sessionType) {
|
||||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||||
String key = getConversationCacheKey(conversationId, null);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
String key = getConversationCacheKey(conversationId, null, sessionType);
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return Result.ok(Collections.emptyList());
|
return Result.ok(Collections.emptyList());
|
||||||
}
|
}
|
||||||
@ -273,6 +320,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
.role(msg.getRole())
|
.role(msg.getRole())
|
||||||
.content(msg.getContent())
|
.content(msg.getContent())
|
||||||
.images(msg.getImages())
|
.images(msg.getImages())
|
||||||
|
.files(msg.getFiles())
|
||||||
.datetime(msg.getDatetime())
|
.datetime(msg.getDatetime())
|
||||||
.build();
|
.build();
|
||||||
// 不设置toolExecutionRequests和toolExecutionResult
|
// 不设置toolExecutionRequests和toolExecutionResult
|
||||||
@ -282,21 +330,30 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
result.put("messages", messages);
|
result.put("messages", messages);
|
||||||
result.put("flowInputs", chatConversation.getFlowInputs());
|
result.put("flowInputs", chatConversation.getFlowInputs());
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
if(oConvertUtils.isNotEmpty(sessionType)){
|
||||||
|
result.put("appData", chatConversation.getApp());
|
||||||
|
}
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
return Result.ok(result);
|
return Result.ok(result);
|
||||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<?> clearMessage(String conversationId) {
|
public Result<?> clearMessage(String conversationId, String sessionType) {
|
||||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||||
String key = getConversationCacheKey(conversationId, null);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
String key = getConversationCacheKey(conversationId, null,sessionType);
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return Result.ok(Collections.emptyList());
|
return Result.ok(Collections.emptyList());
|
||||||
}
|
}
|
||||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
||||||
chatConversation.getMessages().clear();
|
chatConversation.getMessages().clear();
|
||||||
saveChatConversation(chatConversation);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
saveChatConversation(chatConversation,sessionType);
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
}
|
}
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
@ -443,9 +500,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<?> deleteConversation(String conversationId) {
|
public Result<?> deleteConversation(String conversationId, String sessionType) {
|
||||||
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
||||||
String key = getConversationCacheKey(conversationId, null);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
String key = getConversationCacheKey(conversationId, null, sessionType);
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
if (oConvertUtils.isNotEmpty(key)) {
|
if (oConvertUtils.isNotEmpty(key)) {
|
||||||
Boolean delete = redisTemplate.delete(key);
|
Boolean delete = redisTemplate.delete(key);
|
||||||
if (delete) {
|
if (delete) {
|
||||||
@ -463,14 +522,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
||||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
||||||
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
||||||
String key = getConversationCacheKey(updateTitleParams.getId(), null);
|
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType());
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
if (chatConversation != null) {
|
||||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||||
saveChatConversation(chatConversation);
|
}
|
||||||
|
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -479,15 +542,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
*
|
*
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
* @param httpRequest
|
* @param httpRequest
|
||||||
|
* @param sessionType 会话类型
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:27
|
* @date 2025/2/25 19:27
|
||||||
*/
|
*/
|
||||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
|
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest, String sessionType) {
|
||||||
if (oConvertUtils.isEmpty(conversationId)) {
|
if (oConvertUtils.isEmpty(conversationId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String key = getConversationDirCacheKey(httpRequest);
|
String key = getConversationDirCacheKey(httpRequest);
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
if(oConvertUtils.isNotEmpty(sessionType)){
|
||||||
|
key = key + ":" + sessionType;
|
||||||
|
}
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
key = key + ":" + conversationId;
|
key = key + ":" + conversationId;
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@ -522,18 +591,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
*
|
*
|
||||||
* @param app
|
* @param app
|
||||||
* @param conversationId
|
* @param conversationId
|
||||||
|
* @param sessionType
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:19
|
* @date 2025/2/25 19:19
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
|
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId, String sessionType) {
|
||||||
if (oConvertUtils.isObjectEmpty(app)) {
|
if (oConvertUtils.isObjectEmpty(app)) {
|
||||||
app = new AiragApp();
|
app = new AiragApp();
|
||||||
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
||||||
}
|
}
|
||||||
ChatConversation chatConversation = null;
|
ChatConversation chatConversation = null;
|
||||||
String key = getConversationCacheKey(conversationId, null);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
String key = getConversationCacheKey(conversationId, null,sessionType);
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
if (oConvertUtils.isNotEmpty(key)) {
|
if (oConvertUtils.isNotEmpty(key)) {
|
||||||
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||||
}
|
}
|
||||||
@ -569,8 +641,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:27
|
* @date 2025/2/25 19:27
|
||||||
*/
|
*/
|
||||||
private void saveChatConversation(ChatConversation chatConversation) {
|
private void saveChatConversation(ChatConversation chatConversation, String sessionType) {
|
||||||
saveChatConversation(chatConversation, false, null);
|
saveChatConversation(chatConversation, false, null, sessionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -581,11 +653,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:27
|
* @date 2025/2/25 19:27
|
||||||
*/
|
*/
|
||||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
|
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest, String sessionType) {
|
||||||
if (null == chatConversation) {
|
if (null == chatConversation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
|
|
||||||
|
//如果是不保存会话直接返回
|
||||||
|
if(null != chatConversation.getIzSaveSession() && !chatConversation.getIzSaveSession()){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
String key = getConversationCacheKey(chatConversation.getId(), httpRequest, sessionType);
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -680,6 +760,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @date 2025/2/25 19:05
|
* @date 2025/2/25 19:05
|
||||||
*/
|
*/
|
||||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
|
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
|
||||||
|
appendMessage(messages, message, chatConversation, topicId, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId, List<String> files, String saveContent) {
|
||||||
|
|
||||||
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
||||||
// 系统消息,放到消息列表最前面,并且不记录历史
|
// 系统消息,放到消息列表最前面,并且不记录历史
|
||||||
@ -709,8 +793,22 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
textContent.append(((TextContent) content).text()).append("\n");
|
textContent.append(((TextContent) content).text()).append("\n");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
//update-begin---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||||
|
if (oConvertUtils.isNotEmpty(saveContent)) {
|
||||||
|
historyMessage.setContent(saveContent);
|
||||||
|
} else {
|
||||||
historyMessage.setContent(textContent.toString());
|
historyMessage.setContent(textContent.toString());
|
||||||
|
}
|
||||||
historyMessage.setImages(images);
|
historyMessage.setImages(images);
|
||||||
|
// 保存文件信息
|
||||||
|
if (oConvertUtils.isNotEmpty(files)) {
|
||||||
|
List<MessageHistory.FileHistory> fileHistories = new ArrayList<>();
|
||||||
|
for (String file : files) {
|
||||||
|
fileHistories.add(new MessageHistory.FileHistory(file));
|
||||||
|
}
|
||||||
|
historyMessage.setFiles(fileHistories);
|
||||||
|
}
|
||||||
|
//update-end---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||||
} else if (message.type().equals(ChatMessageType.AI)) {
|
} else if (message.type().equals(ChatMessageType.AI)) {
|
||||||
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
||||||
AiMessage aiMessage = (AiMessage) message;
|
AiMessage aiMessage = (AiMessage) message;
|
||||||
@ -766,9 +864,20 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
|
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
|
||||||
try {
|
try {
|
||||||
// 组装用户消息
|
// 组装用户消息
|
||||||
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
|
String content = sendParams.getContent();
|
||||||
|
//将文件内容给提示词
|
||||||
|
if(!CollectionUtils.isEmpty(sendParams.getFiles())){
|
||||||
|
content = buildContentWithFiles(content, sendParams.getFiles());
|
||||||
|
}
|
||||||
|
UserMessage userMessage = aiChatHandler.buildUserMessage(content, sendParams.getImages());
|
||||||
// 追加消息
|
// 追加消息
|
||||||
appendMessage(messages, userMessage, chatConversation, topicId);
|
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||||
|
appendMessage(messages, userMessage, chatConversation, topicId, sendParams.getFiles(), sendParams.getContent());
|
||||||
|
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||||
|
// 绘画AI逻辑:当开启生成绘画时调用
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableDraw()) && sendParams.getEnableDraw()) {
|
||||||
|
return genImageChat(emitter,sendParams,requestId,messages,chatConversation,topicId);
|
||||||
|
}
|
||||||
/* 这里应该是有几种情况:
|
/* 这里应该是有几种情况:
|
||||||
* 1. 非ai应用:获取默认模型->开始聊天
|
* 1. 非ai应用:获取默认模型->开始聊天
|
||||||
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
||||||
@ -781,7 +890,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
||||||
} else {
|
} else {
|
||||||
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
||||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
|
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 发消息
|
// 发消息
|
||||||
@ -789,7 +898,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||||
}
|
}
|
||||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
|
// 设置深度思考搜索参数
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
||||||
|
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
||||||
|
}
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams, sendParams.getSessionType());
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
}
|
}
|
||||||
// 发送就绪消息
|
// 发送就绪消息
|
||||||
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
||||||
@ -804,6 +919,59 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成图片
|
||||||
|
*
|
||||||
|
* @param emitter
|
||||||
|
* @param sendParams
|
||||||
|
* @param requestId
|
||||||
|
* @param messages
|
||||||
|
* @param chatConversation
|
||||||
|
* @param topicId
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private SseEmitter genImageChat(SseEmitter emitter, ChatSendParams sendParams, String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择绘画模型", sendParams.getDrawModelId());
|
||||||
|
AIChatParams aiChatParams = new AIChatParams();
|
||||||
|
try {
|
||||||
|
List<String> images = sendParams.getImages();
|
||||||
|
List<Map<String, Object>> imageList = new ArrayList<>();
|
||||||
|
if(CollectionUtils.isEmpty(images)) {
|
||||||
|
//生成图片
|
||||||
|
imageList = aiChatHandler.imageGenerate(sendParams.getDrawModelId(), sendParams.getContent(), aiChatParams);
|
||||||
|
} else {
|
||||||
|
//图生图
|
||||||
|
imageList = aiChatHandler.imageEdit(sendParams.getDrawModelId(), sendParams.getContent(), images, aiChatParams);
|
||||||
|
}
|
||||||
|
// 记录历史消息
|
||||||
|
String imageMarkdown = imageList.stream().map(map -> {
|
||||||
|
String newUrl = this.uploadImage(map);
|
||||||
|
return "";
|
||||||
|
}).collect(Collectors.joining("\n"));
|
||||||
|
AiMessage aiMessage = new AiMessage(imageMarkdown);
|
||||||
|
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||||
|
// 处理绘画结果并通过SSE返回给客户端
|
||||||
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||||
|
EventMessageData messageEventData = EventMessageData.builder().message(imageMarkdown).build();
|
||||||
|
eventData.setData(messageEventData);
|
||||||
|
eventData.setRequestId(requestId);
|
||||||
|
sendMessage2Client(emitter, eventData);
|
||||||
|
// 保存会话
|
||||||
|
saveChatConversation(chatConversation, false, SpringContextUtils.getHttpServletRequest(), sendParams.getSessionType());
|
||||||
|
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||||
|
eventData.setRequestId(requestId);
|
||||||
|
sendMessage2Client(emitter, eventData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("绘画AI调用异常", e);
|
||||||
|
EventData errorEventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
||||||
|
EventMessageData messageEventData = EventMessageData.builder().message("绘画AI调用失败:" + e.getMessage()).build();
|
||||||
|
errorEventData.setData(messageEventData);
|
||||||
|
errorEventData.setRequestId(requestId);
|
||||||
|
closeSSE(emitter, errorEventData);
|
||||||
|
}
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运行流程
|
* 运行流程
|
||||||
*
|
*
|
||||||
@ -875,7 +1043,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
sendMessage2Client(emitter, msgEventData);
|
sendMessage2Client(emitter, msgEventData);
|
||||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||||
// 保存会话
|
// 保存会话
|
||||||
saveChatConversation(chatConversation, false, httpRequest);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
saveChatConversation(chatConversation, false, httpRequest, sendParams.getSessionType());
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||||
@ -908,16 +1078,31 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @param chatConversation
|
* @param chatConversation
|
||||||
* @param topicId
|
* @param topicId
|
||||||
* @param sendParams
|
* @param sendParams
|
||||||
|
* @param flowId
|
||||||
|
* @param memoryId
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/28 10:41
|
* @date 2025/2/28 10:41
|
||||||
*/
|
*/
|
||||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
|
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams, String flowId, String memoryId) {
|
||||||
AiragApp aiApp = chatConversation.getApp();
|
AiragApp aiApp = chatConversation.getApp();
|
||||||
String modelId = aiApp.getModelId();
|
String modelId = aiApp.getModelId();
|
||||||
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
||||||
// AI应用提示词
|
// AI应用提示词
|
||||||
String prompt = aiApp.getPrompt();
|
String prompt = aiApp.getPrompt();
|
||||||
|
|
||||||
|
String username = "jeecg";
|
||||||
|
try {
|
||||||
|
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||||
|
username = JwtUtil.getUserNameByToken(req);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
}
|
||||||
|
//将变量中的题试题替换并追加
|
||||||
|
if(oConvertUtils.isObjectNotEmpty(aiApp.getVariables())) {
|
||||||
|
prompt = airagVariableService.additionalPrompt(username, aiApp);
|
||||||
|
}
|
||||||
|
|
||||||
if (oConvertUtils.isNotEmpty(prompt)) {
|
if (oConvertUtils.isNotEmpty(prompt)) {
|
||||||
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
|
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
|
||||||
}
|
}
|
||||||
@ -943,6 +1128,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (metadata.containsKey("maxTokens")) {
|
if (metadata.containsKey("maxTokens")) {
|
||||||
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
|
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
|
||||||
}
|
}
|
||||||
|
if (metadata.containsKey(FlowConsts.FLOW_NODE_OPTION_TIME_OUT)) {
|
||||||
|
aiChatParams.setTimeout(oConvertUtils.getInt(metadata.getInteger(FlowConsts.FLOW_NODE_OPTION_TIME_OUT), 300));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -965,15 +1153,69 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//流程不为空,构建插件
|
||||||
|
if(oConvertUtils.isNotEmpty(flowId)){
|
||||||
|
Map<String, Object> result = airagFlowPluginService.getFlowsToPlugin(flowId);
|
||||||
|
this.addPluginToParams(aiChatParams, result);
|
||||||
|
}
|
||||||
|
|
||||||
// 设置网络搜索参数(如果前端传递了)
|
// 设置网络搜索参数(如果前端传递了)
|
||||||
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置深度思考参数(如果前端传递了)
|
||||||
|
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
||||||
|
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置记忆库的插件
|
||||||
|
if(sendParams != null && oConvertUtils.isNotEmpty(memoryId)){
|
||||||
|
//开启记忆
|
||||||
|
if(null == aiApp.getIzOpenMemory() || AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())){
|
||||||
|
Map<String, Object> pluginMemory = airagKnowledgeService.getPluginMemory(memoryId);
|
||||||
|
this.addPluginToParams(aiChatParams, pluginMemory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//设置变量的插件
|
||||||
|
// 添加系统级工具:变量更新
|
||||||
|
if (oConvertUtils.isNotEmpty(aiApp.getId())) {
|
||||||
|
airagVariableService.addUpdateVariableTool(aiApp,username,aiChatParams);
|
||||||
|
}
|
||||||
|
|
||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "构造应用自定义参数完成");
|
printChatDuration(requestId, "构造应用自定义参数完成");
|
||||||
// 发消息
|
// 发消息
|
||||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams, sendParams.getSessionType());
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加插件到参数中
|
||||||
|
*
|
||||||
|
* @param aiChatParams
|
||||||
|
* @param result
|
||||||
|
*/
|
||||||
|
private void addPluginToParams(AIChatParams aiChatParams, Map<String, Object> result) {
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<ToolSpecification, ToolExecutor> flowsToPlugin = (Map<ToolSpecification, ToolExecutor>) result.get("pluginTool");
|
||||||
|
String pluginId = (String) result.get("pluginId");
|
||||||
|
if (aiChatParams.getTools() == null) {
|
||||||
|
aiChatParams.setTools(new HashMap<>());
|
||||||
|
}
|
||||||
|
if (flowsToPlugin != null) {
|
||||||
|
aiChatParams.getTools().putAll(flowsToPlugin);
|
||||||
|
}
|
||||||
|
if (aiChatParams.getPluginIds() == null) {
|
||||||
|
aiChatParams.setPluginIds(new ArrayList<>());
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isNotEmpty(pluginId)) {
|
||||||
|
aiChatParams.getPluginIds().add(pluginId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -984,11 +1226,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
* @param topicId
|
* @param topicId
|
||||||
* @param modelId
|
* @param modelId
|
||||||
* @param messages
|
* @param messages
|
||||||
|
* @param sessionType
|
||||||
* @return
|
* @return
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/2/25 19:24
|
* @date 2025/2/25 19:24
|
||||||
*/
|
*/
|
||||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
|
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams, String sessionType) {
|
||||||
// 调用ai聊天
|
// 调用ai聊天
|
||||||
if (null == aiChatParams) {
|
if (null == aiChatParams) {
|
||||||
aiChatParams = new AIChatParams();
|
aiChatParams = new AIChatParams();
|
||||||
@ -997,11 +1240,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
|
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
|
||||||
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
|
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
|
||||||
}
|
}
|
||||||
|
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){
|
||||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||||
|
} else {
|
||||||
|
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
|
||||||
|
}
|
||||||
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
||||||
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
|
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
|
||||||
aiChatParams.setReturnThinking(true);
|
|
||||||
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
|
// for [QQYUN-9234] MCP服务连接关闭 - 保存参数引用用于在回调中关闭MCP连接
|
||||||
|
final AIChatParams finalAiChatParams = aiChatParams;
|
||||||
TokenStream chatStream;
|
TokenStream chatStream;
|
||||||
try {
|
try {
|
||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
@ -1013,6 +1261,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(e.getMessage(), e);
|
log.error(e.getMessage(), e);
|
||||||
|
// for [QQYUN-9234] MCP服务连接关闭 - 异常时关闭MCP连接
|
||||||
|
finalAiChatParams.closeMcpConnections();
|
||||||
// sse
|
// sse
|
||||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
if (null == emitter) {
|
if (null == emitter) {
|
||||||
@ -1098,6 +1348,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "LLM输出消息完成");
|
printChatDuration(requestId, "LLM输出消息完成");
|
||||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||||
|
// for [QQYUN-9234] MCP服务连接关闭 - 聊天完成时关闭MCP连接
|
||||||
|
finalAiChatParams.closeMcpConnections();
|
||||||
// 记录ai的回复
|
// 记录ai的回复
|
||||||
AiMessage aiMessage = responseMessage.aiMessage();
|
AiMessage aiMessage = responseMessage.aiMessage();
|
||||||
FinishReason finishReason = responseMessage.finishReason();
|
FinishReason finishReason = responseMessage.finishReason();
|
||||||
@ -1113,7 +1365,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||||
// 保存会话
|
// 保存会话
|
||||||
saveChatConversation(chatConversation, false, httpRequest);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
saveChatConversation(chatConversation, false, httpRequest, sessionType);
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
closeSSE(emitter, eventData);
|
closeSSE(emitter, eventData);
|
||||||
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
||||||
// 上下文长度超过限制
|
// 上下文长度超过限制
|
||||||
@ -1137,6 +1391,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
// 打印流程耗时日志
|
// 打印流程耗时日志
|
||||||
printChatDuration(requestId, "LLM输出消息异常");
|
printChatDuration(requestId, "LLM输出消息异常");
|
||||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||||
|
// for [QQYUN-9234] MCP服务连接关闭 - 聊天异常时关闭MCP连接
|
||||||
|
finalAiChatParams.closeMcpConnections();
|
||||||
// sse
|
// sse
|
||||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||||
if (null == emitter) {
|
if (null == emitter) {
|
||||||
@ -1201,7 +1457,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
*/
|
*/
|
||||||
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
||||||
try {
|
try {
|
||||||
log.info("发送消息:{}", eventData.getRequestId());
|
log.debug("发送消息:{}", eventData.getRequestId());
|
||||||
String eventStr = JSONObject.toJSONString(eventData);
|
String eventStr = JSONObject.toJSONString(eventData);
|
||||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||||
emitter.send(SseEmitter.event().data(eventStr));
|
emitter.send(SseEmitter.event().data(eventStr));
|
||||||
@ -1251,7 +1507,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String key = getConversationCacheKey(chatConversation.getId(), null);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
String key = getConversationCacheKey(chatConversation.getId(), null,"");
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
if (oConvertUtils.isEmpty(key)) {
|
if (oConvertUtils.isEmpty(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1281,10 +1539,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
||||||
cachedConversation.setTitle(summaryTitle);
|
cachedConversation.setTitle(summaryTitle);
|
||||||
} else {
|
} else {
|
||||||
cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
|
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||||
|
cachedConversation.setTitle(question.length() > maxLength ? question.substring(0, maxLength) : question);
|
||||||
}
|
}
|
||||||
//保存会话
|
//保存会话
|
||||||
saveChatConversation(cachedConversation);
|
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
|
saveChatConversation(cachedConversation,"");
|
||||||
|
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1329,4 +1590,296 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
|||||||
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
|
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据会话类型获取会话信息
|
||||||
|
*
|
||||||
|
* @param sessionType
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Result<?> getConversationsByType(String sessionType) {
|
||||||
|
String key = getConversationDirCacheKey(null);
|
||||||
|
key = key + ":" + sessionType + ":*";
|
||||||
|
List<String> keys = redisUtil.scan(key);
|
||||||
|
// 如果键集合为空,返回空列表
|
||||||
|
if (keys.isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历键集合,获取对应的 ChatConversation 对象
|
||||||
|
List<ChatConversation> conversations = new ArrayList<>();
|
||||||
|
for (Object k : keys) {
|
||||||
|
ChatConversation conversation = (ChatConversation) redisTemplate.boundValueOps(k).get();
|
||||||
|
|
||||||
|
if (conversation != null) {
|
||||||
|
AiragApp app = conversation.getApp();
|
||||||
|
if (null == app) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
conversation.setApp(null);
|
||||||
|
conversation.setMessages(null);
|
||||||
|
conversations.add(conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对会话列表按创建时间降序排序
|
||||||
|
conversations.sort((o1, o2) -> {
|
||||||
|
Date date1 = o1.getCreateTime();
|
||||||
|
Date date2 = o2.getCreateTime();
|
||||||
|
if (date1 == null && date2 == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (date1 == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (date2 == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return date2.compareTo(date1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
return Result.ok(conversations);
|
||||||
|
}
|
||||||
|
|
||||||
|
//================================================= begin 【QQYUN-14269】【AI】支持变量 ========================================
|
||||||
|
/**
|
||||||
|
* 初始化变量(仅不存在时设置)
|
||||||
|
*/
|
||||||
|
private void saveVariables(AiragApp app) {
|
||||||
|
if(null == app){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!AiAppConsts.IZ_OPEN_MEMORY.equals(app.getIzOpenMemory())){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isObjectNotEmpty(app.getVariables())) {
|
||||||
|
// 变量替换
|
||||||
|
String username = "jeecg";
|
||||||
|
try {
|
||||||
|
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||||
|
username = JwtUtil.getUserNameByToken(req);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isNotEmpty(username) && oConvertUtils.isNotEmpty(app.getId())) {
|
||||||
|
String variables = app.getVariables();
|
||||||
|
JSONArray objects = JSONArray.parseArray(variables);
|
||||||
|
for (int i = 0; i < objects.size(); i++) {
|
||||||
|
JSONObject jsonObject = objects.getJSONObject(i);
|
||||||
|
String name = jsonObject.getString("name");
|
||||||
|
String defaultValue = jsonObject.getString("defaultValue");
|
||||||
|
if (oConvertUtils.isNotEmpty(name)) {
|
||||||
|
airagVariableService.initVariable(username, app.getId(), name, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//================================================= end 【QQYUN-14269】【AI】支持变量 ========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai海报生成
|
||||||
|
*
|
||||||
|
* @param chatSendParams
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String genAiPoster(ChatSendParams chatSendParams) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择绘画模型", chatSendParams.getDrawModelId());
|
||||||
|
AssertUtils.assertNotEmpty("请填写提示词", chatSendParams.getContent());
|
||||||
|
AIChatParams aiChatParams = new AIChatParams();
|
||||||
|
if(oConvertUtils.isNotEmpty(chatSendParams.getImageSize())){
|
||||||
|
aiChatParams.setImageSize(chatSendParams.getImageSize());
|
||||||
|
}
|
||||||
|
String image= chatSendParams.getImageUrl();
|
||||||
|
List<Map<String, Object>> imageList = new ArrayList<>();
|
||||||
|
if(oConvertUtils.isEmpty(image)) {
|
||||||
|
//生成图片
|
||||||
|
imageList = aiChatHandler.imageGenerate(chatSendParams.getDrawModelId(), chatSendParams.getContent(), aiChatParams);
|
||||||
|
} else {
|
||||||
|
//图生图
|
||||||
|
imageList = aiChatHandler.imageEdit(chatSendParams.getDrawModelId(), chatSendParams.getContent(), Arrays.asList(image.split(SymbolConstant.COMMA)), aiChatParams);
|
||||||
|
}
|
||||||
|
return imageList.stream().map(this::uploadImage).collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片
|
||||||
|
*
|
||||||
|
* @param map
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String uploadImage(Map<String, Object> map) {
|
||||||
|
if (null == map || map.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String type = String.valueOf(map.get("type"));
|
||||||
|
String value = String.valueOf(map.get("value"));
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
// 判断是否是base64
|
||||||
|
if ("base64".equals(type)) {
|
||||||
|
if(value.startsWith("data:image")){
|
||||||
|
value = value.substring(value.indexOf(",") + 1);
|
||||||
|
}
|
||||||
|
data = Base64.getDecoder().decode(value);
|
||||||
|
} else {
|
||||||
|
//下载网络图片
|
||||||
|
InputStream inputStream = FileDownloadUtils.getDownInputStream(value, "");
|
||||||
|
if (inputStream != null) {
|
||||||
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||||
|
byte[] inpByte = new byte[1024]; // 1KB缓冲区
|
||||||
|
int nRead;
|
||||||
|
while ((nRead = inputStream.read(inpByte, 0, data.length)) != -1) {
|
||||||
|
buffer.write(inpByte, 0, nRead);
|
||||||
|
}
|
||||||
|
data = buffer.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data != null) {
|
||||||
|
Path path = jeecgBaseConfig.getPath();
|
||||||
|
String bizPath = "chat";
|
||||||
|
String url = CommonUtils.uploadOnlineImage(data, path.getUpload(), bizPath, jeecgBaseConfig.getUploadType());
|
||||||
|
if("local".equals(jeecgBaseConfig.getUploadType())){
|
||||||
|
url = "#{domainURL}/" + url;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("上传图片失败", e);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
//================================================= begin【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
||||||
|
/**
|
||||||
|
* 构建文件内容
|
||||||
|
*
|
||||||
|
* @param content
|
||||||
|
* @param files
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String buildContentWithFiles(String content, List<String> files) {
|
||||||
|
String filesText = parseFilesToText(files);
|
||||||
|
if (oConvertUtils.isEmpty(content)) {
|
||||||
|
content = "请基于我提供的附件内容回答问题。";
|
||||||
|
}else{
|
||||||
|
content = content + "\n\n请基于我提供的附件内容回答问题。";
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isNotEmpty(filesText)) {
|
||||||
|
if (oConvertUtils.isNotEmpty(content)) {
|
||||||
|
content = content + "\n\n" + filesText;
|
||||||
|
} else {
|
||||||
|
content = filesText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将文件转换成text
|
||||||
|
*
|
||||||
|
* @param files
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String parseFilesToText(List<String> files) {
|
||||||
|
if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(files)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
TikaDocumentParser parser = new TikaDocumentParser(AutoDetectParser::new, null, null, null);
|
||||||
|
int parsedCount = 0;
|
||||||
|
for (String fileRef : files) {
|
||||||
|
if (parsedCount >= LLMConsts.CHAT_FILE_MAX_COUNT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isEmpty(fileRef)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileRefWithoutQuery = fileRef;
|
||||||
|
if (fileRefWithoutQuery.contains("?")) {
|
||||||
|
fileRefWithoutQuery = fileRefWithoutQuery.substring(0, fileRefWithoutQuery.indexOf("?"));
|
||||||
|
}
|
||||||
|
String fileName = FilenameUtils.getName(fileRefWithoutQuery);
|
||||||
|
String ext = FilenameUtils.getExtension(fileName);
|
||||||
|
if (oConvertUtils.isEmpty(ext) || !LLMConsts.CHAT_FILE_EXT_WHITELIST.contains(ext.toLowerCase())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
File file = ensureLocalFile(fileRef, fileName);
|
||||||
|
if (file == null || !file.exists() || !file.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Document document = parser.parse(file);
|
||||||
|
if (document == null || oConvertUtils.isEmpty(document.text())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String text = document.text().trim();
|
||||||
|
if (text.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
||||||
|
text = text.substring(0, LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH);
|
||||||
|
}
|
||||||
|
sb.append("附件[").append(fileName).append("]内容:\n").append(text).append("\n\n");
|
||||||
|
parsedCount++;
|
||||||
|
if (sb.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("附件解析失败: {}, {}", fileRef, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本地文件
|
||||||
|
*
|
||||||
|
* @param fileRef
|
||||||
|
* @param fileName
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private File ensureLocalFile(String fileRef, String fileName) {
|
||||||
|
String uploadpath = jeecgBaseConfig.getPath().getUpload();
|
||||||
|
if (LLMConsts.WEB_PATTERN.matcher(fileRef).matches()) {
|
||||||
|
String tempDir = uploadpath + File.separator + "chat" + File.separator + UUID.randomUUID() + File.separator;
|
||||||
|
File dir = new File(tempDir);
|
||||||
|
if (!dir.exists() && !dir.mkdirs()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String tempFilePath = tempDir + fileName;
|
||||||
|
FileDownloadUtils.download2DiskFromNet(fileRef, tempFilePath);
|
||||||
|
return new File(tempFilePath);
|
||||||
|
}
|
||||||
|
return new File(uploadpath + File.separator + fileRef);
|
||||||
|
}
|
||||||
|
//================================================= end【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ai创作
|
||||||
|
*
|
||||||
|
* @param aiWriteGenerateVo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public SseEmitter genAiWriter(AiWriteGenerateVo aiWriteGenerateVo) {
|
||||||
|
String activeMode = "compose";
|
||||||
|
ChatSendParams sendParams = new ChatSendParams();
|
||||||
|
sendParams.setAppId(AiAppConsts.WRITER_APP_ID);
|
||||||
|
String content = "";
|
||||||
|
//写作
|
||||||
|
if (activeMode.equals(aiWriteGenerateVo.getActiveMode())) {
|
||||||
|
content = StrUtil.format(Prompts.AI_WRITER_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
||||||
|
} else {
|
||||||
|
//回复
|
||||||
|
content = StrUtil.format(Prompts.AI_REPLY_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getOriginalContent(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
||||||
|
}
|
||||||
|
sendParams.setContent(content);
|
||||||
|
sendParams.setIzSaveSession(false);
|
||||||
|
return this.send(sendParams);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
package org.jeecg.modules.airag.app.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||||
|
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||||
|
import dev.langchain4j.service.tool.ToolExecutor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||||
|
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||||
|
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||||
|
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AI应用变量服务实现
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AiragVariableServiceImpl implements IAiragVariableService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
private static final String CACHE_PREFIX = "airag:app:var:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化变量(仅不存在时设置)
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param defaultValue
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void initVariable(String username, String appId, String name, String defaultValue) {
|
||||||
|
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = CACHE_PREFIX + appId + ":" + username;
|
||||||
|
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加提示词
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param app
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String additionalPrompt(String username, AiragApp app) {
|
||||||
|
String memoryPrompt = app.getMemoryPrompt();
|
||||||
|
String prompt = app.getPrompt();
|
||||||
|
|
||||||
|
if (oConvertUtils.isEmpty(memoryPrompt)) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
String variablesStr = app.getVariables();
|
||||||
|
if (oConvertUtils.isEmpty(variablesStr)) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||||
|
if (variableList == null || variableList.isEmpty()) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = CACHE_PREFIX + app.getId() + ":" + username;
|
||||||
|
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
|
||||||
|
|
||||||
|
for (AppVariableVo variable : variableList) {
|
||||||
|
if (variable.getEnable() != null && !variable.getEnable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = variable.getName();
|
||||||
|
String value = variable.getDefaultValue();
|
||||||
|
|
||||||
|
// 优先使用Redis中的值
|
||||||
|
if (savedValues.containsKey(name)) {
|
||||||
|
Object savedVal = savedValues.get(name);
|
||||||
|
if (savedVal != null) {
|
||||||
|
value = String.valueOf(savedVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换 {{name}}
|
||||||
|
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
|
||||||
|
}
|
||||||
|
return prompt + "\n" + memoryPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新变量值
|
||||||
|
*
|
||||||
|
* @param userId
|
||||||
|
* @param appId
|
||||||
|
* @param name
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateVariable(String userId, String appId, String name, String value) {
|
||||||
|
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = CACHE_PREFIX + appId + ":" + userId;
|
||||||
|
redisTemplate.opsForHash().put(key, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加变量更新工具
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @param aiApp
|
||||||
|
* @param username
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
|
||||||
|
if (params.getTools() == null) {
|
||||||
|
params.setTools(new HashMap<>());
|
||||||
|
}
|
||||||
|
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 构建变量描述信息
|
||||||
|
String variablesStr = aiApp.getVariables();
|
||||||
|
List<AppVariableVo> variableList = null;
|
||||||
|
if (oConvertUtils.isNotEmpty(variablesStr)) {
|
||||||
|
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
//工具描述
|
||||||
|
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
|
||||||
|
if (variableList != null && !variableList.isEmpty()) {
|
||||||
|
descriptionBuilder.append("\n\n可用变量列表:");
|
||||||
|
for (AppVariableVo var : variableList) {
|
||||||
|
if (var.getEnable() != null && !var.getEnable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
descriptionBuilder.append("\n- ").append(var.getName());
|
||||||
|
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||||
|
descriptionBuilder.append(": ").append(var.getDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descriptionBuilder.append("\n\n注意:variableName必须是上述列表中的名称之一。");
|
||||||
|
}
|
||||||
|
|
||||||
|
//构建更新变量的工具
|
||||||
|
ToolSpecification spec = ToolSpecification.builder()
|
||||||
|
.name("update_variable")
|
||||||
|
.description(descriptionBuilder.toString())
|
||||||
|
.parameters(JsonObjectSchema.builder()
|
||||||
|
.addStringProperty("variableName", "变量名称")
|
||||||
|
.addStringProperty("value", "变量值")
|
||||||
|
.required("variableName", "value")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
//监听工具的调用
|
||||||
|
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
|
||||||
|
try {
|
||||||
|
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
||||||
|
String name = args.getString("variableName");
|
||||||
|
String value = args.getString("value");
|
||||||
|
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
|
||||||
|
//更新变量值
|
||||||
|
variableService.updateVariable(username, aiApp.getId(), name, value);
|
||||||
|
return "变量 " + name + " 已更新为: " + value;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新变量失败", e);
|
||||||
|
return "更新变量失败: " + e.getMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
params.getTools().put(spec, executor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package org.jeecg.modules.airag.app.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: ai写作生成实体类
|
||||||
|
*
|
||||||
|
* @author: wangshuai
|
||||||
|
* @date: 2026/1/12 15:59
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AiWriteGenerateVo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写作类型
|
||||||
|
*/
|
||||||
|
private String activeMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写作内容提示
|
||||||
|
*/
|
||||||
|
private String prompt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原文
|
||||||
|
*/
|
||||||
|
private String originalContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 长度
|
||||||
|
*/
|
||||||
|
private String length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式
|
||||||
|
*/
|
||||||
|
private String format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语气
|
||||||
|
*/
|
||||||
|
private String tone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语言
|
||||||
|
*/
|
||||||
|
private String language;
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package org.jeecg.modules.airag.app.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 应用变量配置
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-02-26
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AppVariableVo implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量名
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认值
|
||||||
|
*/
|
||||||
|
private String defaultValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
private Boolean enable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动作
|
||||||
|
*/
|
||||||
|
private String action;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序
|
||||||
|
*/
|
||||||
|
private Integer orderNum;
|
||||||
|
}
|
||||||
@ -47,4 +47,14 @@ public class ChatConversation {
|
|||||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> flowInputs;
|
private Map<String, Object> flowInputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* portal 应用门户
|
||||||
|
*/
|
||||||
|
private String sessionType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否保存会话
|
||||||
|
*/
|
||||||
|
private Boolean izSaveSession;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,11 @@ public class ChatSendParams {
|
|||||||
*/
|
*/
|
||||||
private List<String> images;
|
private List<String> images;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件列表
|
||||||
|
*/
|
||||||
|
private List<String> files;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流额外入参配置
|
* 工作流额外入参配置
|
||||||
* key: 参数field, value: 参数值
|
* key: 参数field, value: 参数值
|
||||||
@ -59,4 +64,39 @@ public class ChatSendParams {
|
|||||||
*/
|
*/
|
||||||
private Boolean enableSearch;
|
private Boolean enableSearch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启深度思考
|
||||||
|
*/
|
||||||
|
private Boolean enableThink;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话类型: portal 应用门户
|
||||||
|
*/
|
||||||
|
private String sessionType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启生成绘画
|
||||||
|
*/
|
||||||
|
private Boolean enableDraw;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘画模型的id
|
||||||
|
*/
|
||||||
|
private String drawModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片尺寸
|
||||||
|
*/
|
||||||
|
private String imageSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一张图片
|
||||||
|
*/
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否保存会话
|
||||||
|
*/
|
||||||
|
private Boolean izSaveSession;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.jeecg.modules.airag.demo;
|
package org.jeecg.modules.airag.demo;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -11,12 +12,15 @@ import java.util.Map;
|
|||||||
* @Author: chenrui
|
* @Author: chenrui
|
||||||
* @Date: 2025/3/6 11:42
|
* @Date: 2025/3/6 11:42
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Component("testAiragEnhance")
|
@Component("testAiragEnhance")
|
||||||
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||||
Object arg1 = inputParams.get("arg1");
|
Object arg1 = inputParams.get("arg1");
|
||||||
Object arg2 = inputParams.get("arg2");
|
Object arg2 = inputParams.get("arg2");
|
||||||
|
Object index = inputParams.get("index");
|
||||||
|
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
|
||||||
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,209 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.consts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 流程插件常量
|
||||||
|
*
|
||||||
|
* @author: wangshuai
|
||||||
|
* @date: 2025/12/23 19:37
|
||||||
|
*/
|
||||||
|
public interface FlowPluginContent {
|
||||||
|
/**
|
||||||
|
* 名称
|
||||||
|
*/
|
||||||
|
String NAME = "name";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
String DESCRIPTION = "description";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应
|
||||||
|
*/
|
||||||
|
String RESPONSES = "responses";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
String TYPE = "type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数
|
||||||
|
*/
|
||||||
|
String PARAMETERS = "parameters";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否必须
|
||||||
|
*/
|
||||||
|
String REQUIRED = "required";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认值
|
||||||
|
*/
|
||||||
|
String DEFAULT_VALUE = "defaultValue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径
|
||||||
|
*/
|
||||||
|
String PATH = "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 方法
|
||||||
|
*/
|
||||||
|
String METHOD = "method";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 位置
|
||||||
|
*/
|
||||||
|
String LOCATION = "location";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证类型
|
||||||
|
*/
|
||||||
|
String AUTH_TYPE = "authType";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token参数名称
|
||||||
|
*/
|
||||||
|
String TOKEN_PARAM_NAME = "tokenParamName";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token参数值
|
||||||
|
*/
|
||||||
|
String TOKEN_PARAM_VALUE = "tokenParamValue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token
|
||||||
|
*/
|
||||||
|
String TOKEN = "token";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path位置
|
||||||
|
*/
|
||||||
|
String LOCATION_PATH = "Path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header位置
|
||||||
|
*/
|
||||||
|
String LOCATION_HEADER = "Header";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query位置
|
||||||
|
*/
|
||||||
|
String LOCATION_QUERY = "Query";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body位置
|
||||||
|
*/
|
||||||
|
String LOCATION_BODY = "Body";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form-Data位置
|
||||||
|
*/
|
||||||
|
String LOCATION_FORM_DATA = "Form-Data";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String类型
|
||||||
|
*/
|
||||||
|
String TYPE_STRING = "String";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* string类型
|
||||||
|
*/
|
||||||
|
String TYPE_STRING_LOWER = "string";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number类型
|
||||||
|
*/
|
||||||
|
String TYPE_NUMBER = "Number";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* number类型
|
||||||
|
*/
|
||||||
|
String TYPE_NUMBER_LOWER = "number";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integer类型
|
||||||
|
*/
|
||||||
|
String TYPE_INTEGER = "Integer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* integer类型
|
||||||
|
*/
|
||||||
|
String TYPE_INTEGER_LOWER = "integer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean类型
|
||||||
|
*/
|
||||||
|
String TYPE_BOOLEAN = "Boolean";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* boolean类型
|
||||||
|
*/
|
||||||
|
String TYPE_BOOLEAN_LOWER = "boolean";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具数量
|
||||||
|
*/
|
||||||
|
String TOOL_COUNT = "tool_count";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
String ENABLED = "enabled";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入
|
||||||
|
*/
|
||||||
|
String INPUTS = "inputs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出
|
||||||
|
*/
|
||||||
|
String OUTPUTS = "outputs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST请求
|
||||||
|
*/
|
||||||
|
String POST = "POST";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token名称
|
||||||
|
*/
|
||||||
|
String X_ACCESS_TOKEN = "X-Access-Token";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件名称
|
||||||
|
*/
|
||||||
|
String PLUGIN_NAME = "流程调用";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件描述
|
||||||
|
*/
|
||||||
|
String PLUGIN_DESC = "调用工作流";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件请求地址
|
||||||
|
*/
|
||||||
|
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆库插件名称
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_NAME = "记忆库";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆库插件描述
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加记忆路径
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询记忆路径
|
||||||
|
*/
|
||||||
|
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
package org.jeecg.modules.airag.llm.consts;
|
package org.jeecg.modules.airag.llm.consts;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +38,11 @@ public class LLMConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String MODEL_TYPE_LLM = "LLM";
|
public static final String MODEL_TYPE_LLM = "LLM";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型类型: 图像生成
|
||||||
|
*/
|
||||||
|
public static final String MODEL_TYPE_IMAGE = "IMAGE";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向量模型:默认维度
|
* 向量模型:默认维度
|
||||||
*/
|
*/
|
||||||
@ -85,4 +93,29 @@ public class LLMConsts {
|
|||||||
*/
|
*/
|
||||||
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库类型:知识库
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库类型:记忆库
|
||||||
|
*/
|
||||||
|
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持文件的后缀
|
||||||
|
*/
|
||||||
|
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件内容最大长度
|
||||||
|
*/
|
||||||
|
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件对打数量
|
||||||
|
*/
|
||||||
|
public static final int CHAT_FILE_MAX_COUNT = 3;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.controller;
|
||||||
|
|
||||||
|
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||||
|
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* airag baseAPI Controller
|
||||||
|
*
|
||||||
|
* @author sjlei
|
||||||
|
* @date 2025-12-30
|
||||||
|
*/
|
||||||
|
@RestController("airagBaseApiController")
|
||||||
|
public class AiragBaseApiController implements IAiragBaseApi {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
AiragBaseApiImpl airagBaseApi;
|
||||||
|
|
||||||
|
@PostMapping("/airag/api/knowledgeWriteTextDocument")
|
||||||
|
public String knowledgeWriteTextDocument(
|
||||||
|
@RequestParam("knowledgeId") String knowledgeId,
|
||||||
|
@RequestParam("title") String title,
|
||||||
|
@RequestParam("content") String content
|
||||||
|
) {
|
||||||
|
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,14 +1,18 @@
|
|||||||
package org.jeecg.modules.airag.llm.controller;
|
package org.jeecg.modules.airag.llm.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||||
@ -22,7 +26,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -80,6 +83,9 @@ public class AiragKnowledgeController {
|
|||||||
@RequiresPermissions("airag:knowledge:add")
|
@RequiresPermissions("airag:knowledge:add")
|
||||||
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
||||||
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
||||||
|
if(oConvertUtils.isEmpty(airagKnowledge.getType())) {
|
||||||
|
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
|
||||||
|
}
|
||||||
airagKnowledgeService.save(airagKnowledge);
|
airagKnowledgeService.save(airagKnowledge);
|
||||||
return Result.OK("添加成功!");
|
return Result.OK("添加成功!");
|
||||||
}
|
}
|
||||||
@ -101,6 +107,9 @@ public class AiragKnowledgeController {
|
|||||||
return Result.error("未找到对应数据");
|
return Result.error("未找到对应数据");
|
||||||
}
|
}
|
||||||
String oldEmbedId = airagKnowledgeEntity.getEmbedId();
|
String oldEmbedId = airagKnowledgeEntity.getEmbedId();
|
||||||
|
if(oConvertUtils.isEmpty(airagKnowledgeEntity.getType())) {
|
||||||
|
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
|
||||||
|
}
|
||||||
airagKnowledgeService.updateById(airagKnowledge);
|
airagKnowledgeService.updateById(airagKnowledge);
|
||||||
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
|
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
|
||||||
// 更新了模型,重建文档
|
// 更新了模型,重建文档
|
||||||
@ -358,4 +367,61 @@ public class AiragKnowledgeController {
|
|||||||
return Result.OK(airagKnowledges);
|
return Result.OK(airagKnowledges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加记忆
|
||||||
|
*
|
||||||
|
* @param airagKnowledgeDoc
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Operation(summary = "添加记忆")
|
||||||
|
@PostMapping(value = "/plugin/add")
|
||||||
|
public Result<?> add(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc, HttpServletRequest request) {
|
||||||
|
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getKnowledgeId())) {
|
||||||
|
return Result.error("知识库ID不能为空");
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getContent())) {
|
||||||
|
return Result.error("内容不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getTitle())) {
|
||||||
|
// 取内容前20个字作为标题
|
||||||
|
String content = airagKnowledgeDoc.getContent();
|
||||||
|
String title = content.length() > 20 ? content.substring(0, 20) : content;
|
||||||
|
airagKnowledgeDoc.setTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
airagKnowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||||
|
// 保存并构建向量
|
||||||
|
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询记忆
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Operation(summary = "查询记忆")
|
||||||
|
@PostMapping(value = "/plugin/query")
|
||||||
|
public Result<?> pluginQuery(@RequestBody Map<String, Object> params, HttpServletRequest request) {
|
||||||
|
String knowId = (String) params.get("knowledgeId");
|
||||||
|
String queryText = (String) params.get("queryText");
|
||||||
|
if (oConvertUtils.isEmpty(knowId)) {
|
||||||
|
return Result.error("知识库ID不能为空");
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isEmpty(queryText)) {
|
||||||
|
return Result.error("查询内容不能为空");
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<AiragKnowledgeDoc> queryWrapper = new LambdaQueryWrapper<AiragKnowledgeDoc>();
|
||||||
|
queryWrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
|
||||||
|
long count = airagKnowledgeDocService.count(queryWrapper);
|
||||||
|
if(count == 0){
|
||||||
|
return Result.ok("");
|
||||||
|
}
|
||||||
|
// 默认查询前5条
|
||||||
|
KnowledgeSearchResult searchResp = embeddingHandler.embeddingSearch(Collections.singletonList(knowId), queryText, (int) count, null);
|
||||||
|
return Result.ok(searchResp);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import org.jeecg.common.util.AssertUtils;
|
|||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||||
@ -172,11 +173,16 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
|||||||
try {
|
try {
|
||||||
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
|
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
|
||||||
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
|
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
|
||||||
}else{
|
}else if(LLMConsts.MODEL_TYPE_EMBED.equals(airagModel.getModelType())){
|
||||||
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
|
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
|
||||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
|
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
|
||||||
embeddingModel.embed("test text");
|
embeddingModel.embed("test text");
|
||||||
|
//update-begin---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---=
|
||||||
|
}else if(LLMConsts.MODEL_TYPE_IMAGE.equals(airagModel.getModelType())){
|
||||||
|
AIChatParams aiChatParams = new AIChatParams();
|
||||||
|
aiChatHandler.imageGenerate(airagModel, "To test whether it can be successfully called, simply return success", aiChatParams);
|
||||||
}
|
}
|
||||||
|
//update-end---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
log.error("测试模型连接失败", e);
|
log.error("测试模型连接失败", e);
|
||||||
return Result.error("测试模型连接失败,请检查模型配置是否正确!");
|
return Result.error("测试模型连接失败,请检查模型配置是否正确!");
|
||||||
|
|||||||
@ -7,6 +7,7 @@ package org.jeecg.modules.airag.llm.document;
|
|||||||
|
|
||||||
import dev.langchain4j.data.document.BlankDocumentException;
|
import dev.langchain4j.data.document.BlankDocumentException;
|
||||||
import dev.langchain4j.data.document.Document;
|
import dev.langchain4j.data.document.Document;
|
||||||
|
import dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser;
|
||||||
import dev.langchain4j.internal.Utils;
|
import dev.langchain4j.internal.Utils;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.apache.poi.hslf.usermodel.HSLFTextParagraph;
|
import org.apache.poi.hslf.usermodel.HSLFTextParagraph;
|
||||||
@ -30,7 +31,10 @@ import org.xml.sax.ContentHandler;
|
|||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -51,6 +55,8 @@ public class TikaDocumentParser {
|
|||||||
private final Supplier<ContentHandler> contentHandlerSupplier;
|
private final Supplier<ContentHandler> contentHandlerSupplier;
|
||||||
private final Supplier<Metadata> metadataSupplier;
|
private final Supplier<Metadata> metadataSupplier;
|
||||||
private final Supplier<ParseContext> parseContextSupplier;
|
private final Supplier<ParseContext> parseContextSupplier;
|
||||||
|
//文件前缀
|
||||||
|
private static final Set<String> FILE_SUFFIX = new HashSet<>(Arrays.asList("docx", "doc", "pptx", "ppt", "xlsx", "xls"));
|
||||||
|
|
||||||
public TikaDocumentParser() {
|
public TikaDocumentParser() {
|
||||||
this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null);
|
this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null);
|
||||||
@ -71,22 +77,16 @@ public class TikaDocumentParser {
|
|||||||
InputStream isForParsing = Files.newInputStream(file.toPath());
|
InputStream isForParsing = Files.newInputStream(file.toPath());
|
||||||
// 使用 Tika 自动检测 MIME 类型
|
// 使用 Tika 自动检测 MIME 类型
|
||||||
String fileName = file.getName().toLowerCase();
|
String fileName = file.getName().toLowerCase();
|
||||||
|
//后缀
|
||||||
|
String ext = FilenameUtils.getExtension(fileName);
|
||||||
if (fileName.endsWith(".txt")
|
if (fileName.endsWith(".txt")
|
||||||
|| fileName.endsWith(".md")
|
|| fileName.endsWith(".md")
|
||||||
|| fileName.endsWith(".pdf")) {
|
|| fileName.endsWith(".pdf")) {
|
||||||
return extractByTika(isForParsing);
|
return extractByTika(isForParsing);
|
||||||
} else if (fileName.endsWith(".docx")) {
|
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||||
return extractTextFromDocx(isForParsing);
|
} else if (FILE_SUFFIX.contains(ext.toLowerCase())) {
|
||||||
} else if (fileName.endsWith(".doc")) {
|
return parseDocExcelPdfUsingApachePoi(file);
|
||||||
return extractTextFromDoc(isForParsing);
|
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||||
} else if (fileName.endsWith(".xlsx")) {
|
|
||||||
return extractTextFromExcel(isForParsing);
|
|
||||||
} else if (fileName.endsWith(".xls")) {
|
|
||||||
return extractTextFromExcel(isForParsing);
|
|
||||||
} else if (fileName.endsWith(".pptx")) {
|
|
||||||
return extractTextFromPptx(isForParsing);
|
|
||||||
} else if (fileName.endsWith(".ppt")) {
|
|
||||||
return extractTextFromPpt(isForParsing);
|
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName));
|
throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName));
|
||||||
}
|
}
|
||||||
@ -95,6 +95,27 @@ public class TikaDocumentParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* langchain4j 内部解析器
|
||||||
|
* @param file
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Document parseDocExcelPdfUsingApachePoi(File file) {
|
||||||
|
AssertUtils.assertNotEmpty("请选择文件", file);
|
||||||
|
try (InputStream inputStream = Files.newInputStream(file.toPath())) {
|
||||||
|
ApachePoiDocumentParser parser = new ApachePoiDocumentParser();
|
||||||
|
Document document = parser.parse(inputStream);
|
||||||
|
if (document == null || Utils.isNullOrBlank(document.text())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return document;
|
||||||
|
} catch (BlankDocumentException e) {
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Document tryExtractDocOrDocx(InputStream inputStream) throws IOException {
|
private static Document tryExtractDocOrDocx(InputStream inputStream) throws IOException {
|
||||||
try {
|
try {
|
||||||
// 先尝试 DOCX(基于 OPC XML 格式)
|
// 先尝试 DOCX(基于 OPC XML 格式)
|
||||||
|
|||||||
@ -102,4 +102,11 @@ public class AiragKnowledge implements Serializable {
|
|||||||
@Excel(name = "状态", width = 15)
|
@Excel(name = "状态", width = 15)
|
||||||
@Schema(description = "状态")
|
@Schema(description = "状态")
|
||||||
private java.lang.String status;
|
private java.lang.String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型(knowledge知识 memory 记忆)
|
||||||
|
*/
|
||||||
|
@Excel(name="类型(knowledge知识 memory 记忆)", width = 15)
|
||||||
|
@Schema(description = "类型(knowledge知识 memory 记忆)")
|
||||||
|
private java.lang.String type;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,13 +12,16 @@ import org.jeecg.ai.handler.LLMHandler;
|
|||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.util.AssertUtils;
|
import org.jeecg.common.util.AssertUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
|
import org.jeecg.modules.airag.common.handler.McpToolProviderWrapper;
|
||||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||||
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
||||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||||
|
import org.jeecg.config.AiRagConfigBean;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@ -54,6 +57,8 @@ public class AIChatHandler implements IAIChatHandler {
|
|||||||
@Autowired
|
@Autowired
|
||||||
LLMHandler llmHandler;
|
LLMHandler llmHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
AiRagConfigBean aiRagConfigBean;
|
||||||
|
|
||||||
@Value(value = "${jeecg.path.upload:}")
|
@Value(value = "${jeecg.path.upload:}")
|
||||||
private String uploadpath;
|
private String uploadpath;
|
||||||
@ -285,7 +290,7 @@ public class AIChatHandler implements IAIChatHandler {
|
|||||||
|
|
||||||
// 默认超时时间
|
// 默认超时时间
|
||||||
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
|
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
|
||||||
params.setTimeout(60);
|
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
//deepseek-reasoner 推理模型不支持插件tool
|
//deepseek-reasoner 推理模型不支持插件tool
|
||||||
@ -301,6 +306,7 @@ public class AIChatHandler implements IAIChatHandler {
|
|||||||
/**
|
/**
|
||||||
* 构造插件和MCP工具
|
* 构造插件和MCP工具
|
||||||
* for [QQYUN-12453]【AI】支持插件
|
* for [QQYUN-12453]【AI】支持插件
|
||||||
|
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
|
||||||
* @param params
|
* @param params
|
||||||
* @author chenrui
|
* @author chenrui
|
||||||
* @date 2025/10/31 14:04
|
* @date 2025/10/31 14:04
|
||||||
@ -310,6 +316,7 @@ public class AIChatHandler implements IAIChatHandler {
|
|||||||
|
|
||||||
if(oConvertUtils.isObjectNotEmpty(pluginIds)){
|
if(oConvertUtils.isObjectNotEmpty(pluginIds)){
|
||||||
List<McpToolProvider> mcpToolProviders = new ArrayList<>();
|
List<McpToolProvider> mcpToolProviders = new ArrayList<>();
|
||||||
|
List<McpToolProviderWrapper> mcpToolProviderWrappers = new ArrayList<>();
|
||||||
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
|
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
|
||||||
|
|
||||||
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
|
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
|
||||||
@ -325,15 +332,18 @@ public class AIChatHandler implements IAIChatHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("mcp".equalsIgnoreCase(category)) {
|
if ("mcp".equalsIgnoreCase(category)) {
|
||||||
// MCP类型:构建McpToolProvider
|
// MCP类型:构建McpToolProviderWrapper(包含连接引用用于后续关闭)
|
||||||
McpToolProvider mcpToolProvider = buildMcpToolProvider(
|
// for [QQYUN-9234] MCP服务连接关闭
|
||||||
|
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
|
||||||
airagMcp.getName(),
|
airagMcp.getName(),
|
||||||
airagMcp.getType(),
|
airagMcp.getType(),
|
||||||
airagMcp.getEndpoint(),
|
airagMcp.getEndpoint(),
|
||||||
airagMcp.getHeaders()
|
airagMcp.getHeaders(),
|
||||||
|
aiRagConfigBean.getAllowSensitiveNodes()
|
||||||
);
|
);
|
||||||
if (mcpToolProvider != null) {
|
if (wrapper != null) {
|
||||||
mcpToolProviders.add(mcpToolProvider);
|
mcpToolProviders.add(wrapper.getMcpToolProvider());
|
||||||
|
mcpToolProviderWrappers.add(wrapper);
|
||||||
}
|
}
|
||||||
} else if ("plugin".equalsIgnoreCase(category)) {
|
} else if ("plugin".equalsIgnoreCase(category)) {
|
||||||
// 插件类型:构建ToolSpecification和ToolExecutor
|
// 插件类型:构建ToolSpecification和ToolExecutor
|
||||||
@ -349,6 +359,12 @@ public class AIChatHandler implements IAIChatHandler {
|
|||||||
params.setMcpToolProviders(mcpToolProviders);
|
params.setMcpToolProviders(mcpToolProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存MCP连接包装器,用于后续关闭
|
||||||
|
// for [QQYUN-9234] MCP服务连接关闭
|
||||||
|
if (!mcpToolProviderWrappers.isEmpty()) {
|
||||||
|
params.setMcpToolProviderWrappers(mcpToolProviderWrappers);
|
||||||
|
}
|
||||||
|
|
||||||
// 设置插件工具
|
// 设置插件工具
|
||||||
if (!pluginTools.isEmpty()) {
|
if (!pluginTools.isEmpty()) {
|
||||||
if (params.getTools() == null) {
|
if (params.getTools() == null) {
|
||||||
@ -401,5 +417,129 @@ public class AIChatHandler implements IAIChatHandler {
|
|||||||
return imageContents;
|
return imageContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//================================================= begin【QQYUN-12145】【AI】AI 绘画创作 ========================================
|
||||||
|
/**
|
||||||
|
* 文本生成图片
|
||||||
|
* @param modelId
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> imageGenerate(String modelId, String messages, AIChatParams params) {
|
||||||
|
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||||
|
AssertUtils.assertNotEmpty("请选择图片大模型", modelId);
|
||||||
|
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||||
|
return this.imageGenerate(airagModel, messages, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本生成图片
|
||||||
|
*
|
||||||
|
* @param airagModel
|
||||||
|
* @param messages
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<Map<String, Object>> imageGenerate(AiragModel airagModel, String messages, AIChatParams params) {
|
||||||
|
params = mergeParams(airagModel, params);
|
||||||
|
try {
|
||||||
|
return llmHandler.imageGenerate(messages, params);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String errMsg = "调用绘画AI接口失败,详情请查看后台日志。";
|
||||||
|
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||||
|
// 根据常见异常关键字做细致翻译
|
||||||
|
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String value = entry.getValue();
|
||||||
|
if (errMsg.contains(key)) {
|
||||||
|
errMsg = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.error("AI模型调用异常: {}", errMsg, e);
|
||||||
|
throw new JeecgBootException(errMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图生图
|
||||||
|
*
|
||||||
|
* @param modelId
|
||||||
|
* @param messages
|
||||||
|
* @param images
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<Map<String, Object>> imageEdit(String modelId, String messages, List<String> images, AIChatParams params) {
|
||||||
|
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||||
|
params = mergeParams(airagModel, params);
|
||||||
|
List<String> originalImageBase64List = getFirstImageBase64(images);
|
||||||
|
try {
|
||||||
|
return llmHandler.imageEdit(messages, originalImageBase64List, params);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String errMsg = "调用绘画AI接口失败,详情请查看后台日志。";
|
||||||
|
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||||
|
// 根据常见异常关键字做细致翻译
|
||||||
|
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String value = entry.getValue();
|
||||||
|
if (errMsg.contains(key)) {
|
||||||
|
errMsg = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.error("AI模型调用异常: {}", errMsg, e);
|
||||||
|
throw new JeecgBootException(errMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要将图片转换成Base64编码
|
||||||
|
* @param images 图片路径列表
|
||||||
|
* @return Base64编码字符串
|
||||||
|
*/
|
||||||
|
private List<String> getFirstImageBase64(List<String> images) {
|
||||||
|
List<String> originalImageBase64List = new ArrayList<>();
|
||||||
|
if (images != null && !images.isEmpty()) {
|
||||||
|
for (String imageUrl : images) {
|
||||||
|
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(imageUrl);
|
||||||
|
try {
|
||||||
|
byte[] fileContent;
|
||||||
|
if (matcher.matches()) {
|
||||||
|
// 来源于网络
|
||||||
|
java.net.URL url = new java.net.URL(imageUrl);
|
||||||
|
java.net.URLConnection conn = url.openConnection();
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
try (java.io.InputStream in = conn.getInputStream()) {
|
||||||
|
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
||||||
|
int nRead;
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
while ((nRead = in.read(data, 0, data.length)) != -1) {
|
||||||
|
buffer.write(data, 0, nRead);
|
||||||
|
}
|
||||||
|
buffer.flush();
|
||||||
|
fileContent = buffer.toByteArray();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 本地文件
|
||||||
|
String filePath = uploadpath + File.separator + imageUrl;
|
||||||
|
Path path = Paths.get(filePath);
|
||||||
|
fileContent = Files.readAllBytes(path);
|
||||||
|
}
|
||||||
|
originalImageBase64List.add(Base64.getEncoder().encodeToString(fileContent));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("图片读取失败: " + imageUrl, e);
|
||||||
|
throw new JeecgBootException("图片读取失败: " + imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalImageBase64List;
|
||||||
|
}
|
||||||
|
//================================================= end 【QQYUN-12145】【AI】AI 绘画创作 ========================================
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,13 +17,17 @@ import dev.langchain4j.store.embedding.EmbeddingMatch;
|
|||||||
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
|
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
|
||||||
import dev.langchain4j.store.embedding.EmbeddingStore;
|
import dev.langchain4j.store.embedding.EmbeddingStore;
|
||||||
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
|
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
|
||||||
|
import dev.langchain4j.store.embedding.filter.Filter;
|
||||||
|
import dev.langchain4j.store.embedding.filter.logical.And;
|
||||||
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
|
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.apache.tika.parser.AutoDetectParser;
|
import org.apache.tika.parser.AutoDetectParser;
|
||||||
import org.jeecg.ai.factory.AiModelFactory;
|
import org.jeecg.ai.factory.AiModelFactory;
|
||||||
import org.jeecg.ai.factory.AiModelOptions;
|
import org.jeecg.ai.factory.AiModelOptions;
|
||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
import org.jeecg.common.util.*;
|
import org.jeecg.common.util.*;
|
||||||
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
|
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
|
||||||
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||||
@ -94,11 +98,21 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
|||||||
*/
|
*/
|
||||||
private static final int DEFAULT_OVERLAP_SIZE = 50;
|
private static final int DEFAULT_OVERLAP_SIZE = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大输出长度
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_MAX_OUTPUT_CHARS = 4000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向量存储元数据:knowledgeId
|
* 向量存储元数据:knowledgeId
|
||||||
*/
|
*/
|
||||||
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
|
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量存储元数据: 用户账号
|
||||||
|
*/
|
||||||
|
public static final String EMBED_STORE_METADATA_USER_NAME = "username";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向量存储元数据:docId
|
* 向量存储元数据:docId
|
||||||
*/
|
*/
|
||||||
@ -109,6 +123,11 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
|||||||
*/
|
*/
|
||||||
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
|
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向量存储元数据:创建时间
|
||||||
|
*/
|
||||||
|
public static final String EMBED_STORE_CREATE_TIME = "createTime";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 向量存储缓存
|
* 向量存储缓存
|
||||||
*/
|
*/
|
||||||
@ -175,7 +194,26 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
|||||||
.build();
|
.build();
|
||||||
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
|
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
|
||||||
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
|
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
|
||||||
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()));
|
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()))
|
||||||
|
//初始化记忆库的时候添加创建时间选项
|
||||||
|
.put(EMBED_STORE_CREATE_TIME, String.valueOf(doc.getCreateTime() != null ? doc.getCreateTime().getTime() : System.currentTimeMillis()));
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||||
|
//添加用户名字到元数据里面,用于记忆库中数据隔离
|
||||||
|
String username = doc.getCreateBy();
|
||||||
|
if (oConvertUtils.isEmpty(username)) {
|
||||||
|
try {
|
||||||
|
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||||
|
String token = TokenUtils.getTokenByRequest(request);
|
||||||
|
username = JwtUtil.getUsername(token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore:token获取不到默认为admin
|
||||||
|
username = "admin";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oConvertUtils.isNotEmpty(username)) {
|
||||||
|
metadata.put(EMBED_STORE_METADATA_USER_NAME, username);
|
||||||
|
}
|
||||||
|
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||||
Document from = Document.from(content, metadata);
|
Document from = Document.from(content, metadata);
|
||||||
ingestor.ingest(from);
|
ingestor.ingest(from);
|
||||||
return metadata.toMap();
|
return metadata.toMap();
|
||||||
@ -208,16 +246,47 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//命中的文档内容
|
|
||||||
StringBuilder data = new StringBuilder();
|
StringBuilder data = new StringBuilder();
|
||||||
// 对documents按score降序排序并取前topNumber个
|
//update-begin---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
|
||||||
List<Map<String, Object>> sortedDocuments = documents.stream()
|
//是否为记忆库
|
||||||
.sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
|
boolean memoryMode = false;
|
||||||
.limit(topNumber)
|
//记忆库只有一个
|
||||||
.peek(doc -> data.append(doc.get("content")).append("\n"))
|
if (knowIds.size() == 1) {
|
||||||
|
String firstId = knowIds.get(0);
|
||||||
|
if (oConvertUtils.isNotEmpty(firstId)) {
|
||||||
|
AiragKnowledge k = airagKnowledgeMapper.getByIdIgnoreTenant(firstId);
|
||||||
|
memoryMode = (k != null && LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(k.getType()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//如果是记忆库按照创建时间排序,如果不是按照score分值进行排序
|
||||||
|
List<Map<String, Object>> prepared = documents.stream()
|
||||||
|
.sorted(memoryMode
|
||||||
|
? Comparator.comparingLong((Map<String, Object> doc) -> oConvertUtils.getLong(doc.get(EMBED_STORE_CREATE_TIME), 0L)).reversed()
|
||||||
|
: Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
List<Map<String, Object>> limited = new ArrayList<>();
|
||||||
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
|
//将返回的结果按照最大的token进行长度限制
|
||||||
|
for (Map<String, Object> doc : prepared) {
|
||||||
|
if (limited.size() >= topNumber) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
String content = oConvertUtils.getString(doc.get("content"), "");
|
||||||
|
int remain = DEFAULT_MAX_OUTPUT_CHARS - data.length();
|
||||||
|
if (remain <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//数据库中文本的长度和已经拼接的长度
|
||||||
|
if (content.length() <= remain) {
|
||||||
|
data.append(content).append("\n");
|
||||||
|
limited.add(doc);
|
||||||
|
} else {
|
||||||
|
data.append(content, 0, remain);
|
||||||
|
limited.add(doc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new KnowledgeSearchResult(data.toString(), limited);
|
||||||
|
//update-end---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,11 +313,31 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
|||||||
|
|
||||||
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
|
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
|
||||||
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
|
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
|
||||||
|
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||||
|
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
|
||||||
|
|
||||||
|
// 记忆库的时候需要根据用户隔离
|
||||||
|
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
|
||||||
|
try {
|
||||||
|
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||||
|
String token = TokenUtils.getTokenByRequest(request);
|
||||||
|
String username = JwtUtil.getUsername(token);
|
||||||
|
if (oConvertUtils.isNotEmpty(username)) {
|
||||||
|
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
log.info("构建过滤器异常,{}",e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||||
|
|
||||||
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
||||||
.queryEmbedding(queryEmbedding)
|
.queryEmbedding(queryEmbedding)
|
||||||
.maxResults(topNumber)
|
.maxResults(topNumber)
|
||||||
.minScore(similarity)
|
.minScore(similarity)
|
||||||
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
.filter(filter)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||||
@ -262,6 +351,9 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
|||||||
Metadata metadata = matchRes.embedded().metadata();
|
Metadata metadata = matchRes.embedded().metadata();
|
||||||
data.put("chunk", metadata.getInteger("index"));
|
data.put("chunk", metadata.getInteger("index"));
|
||||||
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
|
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
|
||||||
|
//查询返回的时候增加创建时间,用于排序
|
||||||
|
String ct = metadata.getString(EMBED_STORE_CREATE_TIME);
|
||||||
|
data.put(EMBED_STORE_CREATE_TIME, ct);
|
||||||
return data;
|
return data;
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@ -295,13 +387,32 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
|||||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||||
topNumber = oConvertUtils.getInteger(topNumber, 5);
|
topNumber = oConvertUtils.getInteger(topNumber, 5);
|
||||||
similarity = oConvertUtils.getDou(similarity, 0.75);
|
similarity = oConvertUtils.getDou(similarity, 0.75);
|
||||||
|
|
||||||
|
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||||
|
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
|
||||||
|
// 记忆库的时候需要根据用户隔离
|
||||||
|
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
|
||||||
|
try {
|
||||||
|
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||||
|
String token = TokenUtils.getTokenByRequest(request);
|
||||||
|
String username = JwtUtil.getUsername(token);
|
||||||
|
if (oConvertUtils.isNotEmpty(username)) {
|
||||||
|
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// ignore
|
||||||
|
log.info("构建过滤器异常,{}",e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||||
|
|
||||||
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
|
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
|
||||||
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
|
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
|
||||||
.embeddingStore(embeddingStore)
|
.embeddingStore(embeddingStore)
|
||||||
.embeddingModel(embeddingModel)
|
.embeddingModel(embeddingModel)
|
||||||
.maxResults(topNumber)
|
.maxResults(topNumber)
|
||||||
.minScore(similarity)
|
.minScore(similarity)
|
||||||
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
.filter(filter)
|
||||||
.build();
|
.build();
|
||||||
retrievers.add(contentRetriever);
|
retrievers.add(contentRetriever);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import org.jeecg.common.util.CommonUtils;
|
|||||||
import org.jeecg.common.util.RestUtil;
|
import org.jeecg.common.util.RestUtil;
|
||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||||
|
import org.jeecg.modules.airag.flow.component.ToolsNode;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
@ -51,8 +53,9 @@ public class PluginToolBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String baseUrl = airagMcp.getEndpoint();
|
String baseUrl = airagMcp.getEndpoint();
|
||||||
|
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
|
||||||
// 如果baseUrl为空,使用当前系统地址
|
// 如果baseUrl为空,使用当前系统地址
|
||||||
if (oConvertUtils.isEmpty(baseUrl)) {
|
if (isEmptyBaseUrl) {
|
||||||
if (currentHttpRequest != null) {
|
if (currentHttpRequest != null) {
|
||||||
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
|
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
|
||||||
log.info("插件[{}]的BaseURL为空,使用系统地址: {}", airagMcp.getName(), baseUrl);
|
log.info("插件[{}]的BaseURL为空,使用系统地址: {}", airagMcp.getName(), baseUrl);
|
||||||
@ -65,6 +68,9 @@ public class PluginToolBuilder {
|
|||||||
// 解析headers
|
// 解析headers
|
||||||
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
|
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
|
||||||
|
|
||||||
|
// 判断是否需要加签
|
||||||
|
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
|
||||||
|
|
||||||
// 解析并应用授权配置(从metadata中读取)
|
// 解析并应用授权配置(从metadata中读取)
|
||||||
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
|
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
|
||||||
|
|
||||||
@ -76,7 +82,7 @@ public class PluginToolBuilder {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
ToolSpecification spec = buildToolSpecification(toolConfig);
|
ToolSpecification spec = buildToolSpecification(toolConfig);
|
||||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
|
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign);
|
||||||
if (spec != null && executor != null) {
|
if (spec != null && executor != null) {
|
||||||
tools.put(spec, executor);
|
tools.put(spec, executor);
|
||||||
}
|
}
|
||||||
@ -187,7 +193,7 @@ public class PluginToolBuilder {
|
|||||||
/**
|
/**
|
||||||
* 构建ToolExecutor
|
* 构建ToolExecutor
|
||||||
*/
|
*/
|
||||||
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders) {
|
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders, boolean isNeedSign) {
|
||||||
String path = toolConfig.getString("path");
|
String path = toolConfig.getString("path");
|
||||||
String method = toolConfig.getString("method");
|
String method = toolConfig.getString("method");
|
||||||
JSONArray parameters = toolConfig.getJSONArray("parameters");
|
JSONArray parameters = toolConfig.getJSONArray("parameters");
|
||||||
@ -215,8 +221,13 @@ public class PluginToolBuilder {
|
|||||||
JSONObject urlVariables = buildUrlVariables(parameters, args);
|
JSONObject urlVariables = buildUrlVariables(parameters, args);
|
||||||
Object body = buildRequestBody(parameters, args, httpHeaders);
|
Object body = buildRequestBody(parameters, args, httpHeaders);
|
||||||
|
|
||||||
// 发送HTTP请求
|
if (isNeedSign) {
|
||||||
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
|
// 发送请求前加签
|
||||||
|
ToolsNode.Helper.applySignature(url, httpHeaders, urlVariables, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送HTTP请求,增加超时时间
|
||||||
|
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class, AiragConsts.DEFAULT_TIMEOUT * 1000);
|
||||||
|
|
||||||
// 直接返回原始响应字符串,不进行解析
|
// 直接返回原始响应字符串,不进行解析
|
||||||
return response.getBody() != null ? response.getBody() : "";
|
return response.getBody() != null ? response.getBody() : "";
|
||||||
@ -335,10 +346,16 @@ public class PluginToolBuilder {
|
|||||||
if (isQueryParam || !isOtherType) {
|
if (isQueryParam || !isOtherType) {
|
||||||
Object value = args.get(paramName);
|
Object value = args.get(paramName);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
//如果是知识库的id赋值默认值
|
||||||
|
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||||
|
String defaultValue = param.getString("defaultValue");
|
||||||
|
urlVariables.put(paramName, defaultValue);
|
||||||
|
} else {
|
||||||
urlVariables.put(paramName, value);
|
urlVariables.put(paramName, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return urlVariables.isEmpty() ? null : urlVariables;
|
return urlVariables.isEmpty() ? null : urlVariables;
|
||||||
}
|
}
|
||||||
@ -392,7 +409,13 @@ public class PluginToolBuilder {
|
|||||||
|
|
||||||
Object value = args.get(paramName);
|
Object value = args.get(paramName);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
//如果是知识库的id赋值默认值
|
||||||
|
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||||
|
String defaultValue = param.getString("defaultValue");
|
||||||
|
body.put(paramName, defaultValue);
|
||||||
|
} else {
|
||||||
body.put(paramName, value);
|
body.put(paramName, value);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 检查是否有默认值
|
// 检查是否有默认值
|
||||||
String defaultValue = param.getString("defaultValue");
|
String defaultValue = param.getString("defaultValue");
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 获取流程mcp服务
|
||||||
|
* @Author: wangshuai
|
||||||
|
* @Date: 2025-12-22 15:34:20
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface IAiragFlowPluginService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步所有启用的流程到MCP插件配置
|
||||||
|
*
|
||||||
|
* @param flowIds 多个流程id
|
||||||
|
*/
|
||||||
|
Map<String, Object> getFlowsToPlugin(String flowIds);
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ package org.jeecg.modules.airag.llm.service;
|
|||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AIRag知识库
|
* AIRag知识库
|
||||||
*
|
*
|
||||||
@ -11,4 +13,12 @@ import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
|||||||
* @Version: V1.0
|
* @Version: V1.0
|
||||||
*/
|
*/
|
||||||
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
|
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建知识库的工具
|
||||||
|
*
|
||||||
|
* @param memoryId
|
||||||
|
* @return Map<String, Object>
|
||||||
|
*/
|
||||||
|
Map<String, Object> getPluginMemory(String memoryId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.service.impl;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* airag baseAPI 实现类
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Primary
|
||||||
|
@Service("airagBaseApiImpl")
|
||||||
|
public class AiragBaseApiImpl implements IAiragBaseApi {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAiragKnowledgeDocService airagKnowledgeDocService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
|
||||||
|
AssertUtils.assertNotEmpty("知识库ID不能为空", knowledgeId);
|
||||||
|
AssertUtils.assertNotEmpty("写入内容不能为空", content);
|
||||||
|
AiragKnowledgeDoc knowledgeDoc = new AiragKnowledgeDoc();
|
||||||
|
knowledgeDoc.setKnowledgeId(knowledgeId);
|
||||||
|
knowledgeDoc.setTitle(title);
|
||||||
|
knowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||||
|
knowledgeDoc.setContent(content);
|
||||||
|
Result<?> result = airagKnowledgeDocService.editDocument(knowledgeDoc);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new JeecgBootBizTipException(result.getMessage());
|
||||||
|
}
|
||||||
|
if (knowledgeDoc.getId() == null) {
|
||||||
|
throw new JeecgBootBizTipException("知识库文档ID为空");
|
||||||
|
}
|
||||||
|
log.info("[AI-KNOWLEDGE] 文档写入完成,知识库:{}, 文档ID:{}", knowledgeId, knowledgeDoc.getId());
|
||||||
|
return knowledgeDoc.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,231 @@
|
|||||||
|
package org.jeecg.modules.airag.llm.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||||
|
import dev.langchain4j.service.tool.ToolExecutor;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.constant.SymbolConstant;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import org.jeecg.common.util.TokenUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||||
|
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||||
|
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||||
|
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
|
||||||
|
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
|
||||||
|
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||||
|
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||||
|
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: 流程同步到MCP服务实现类
|
||||||
|
* @Author: wangshuai
|
||||||
|
* @Date: 2025-12-22
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class AiragFlowPluginServiceImpl implements IAiragFlowPluginService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAiragFlowService airagFlowService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getFlowsToPlugin(String flowIds) {
|
||||||
|
log.info("开始构建流程插件");
|
||||||
|
// 1. 查询所有启用的流程
|
||||||
|
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(AiragFlow::getStatus, FlowConsts.FLOW_STATUS_ENABLE);
|
||||||
|
queryWrapper.in(AiragFlow::getId, Arrays.asList(flowIds.split(SymbolConstant.COMMA)));
|
||||||
|
List<AiragFlow> flows = airagFlowService.list(queryWrapper);
|
||||||
|
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
|
if (flows.isEmpty()) {
|
||||||
|
log.info("当前应用所选流程没有启用的流程");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//返回数据
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
//插件
|
||||||
|
//插件id
|
||||||
|
AiragMcp tool = new AiragMcp();
|
||||||
|
// 2. 构建插件
|
||||||
|
String id = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
tool.setId(id);
|
||||||
|
// 插件名称
|
||||||
|
tool.setName(FlowPluginContent.PLUGIN_NAME);
|
||||||
|
// 描述
|
||||||
|
tool.setDescr(FlowPluginContent.PLUGIN_DESC);
|
||||||
|
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||||
|
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||||
|
tool.setCategory("plugin");
|
||||||
|
tool.setEndpoint("");
|
||||||
|
int toolCount = 0;
|
||||||
|
//构建拆件工具
|
||||||
|
for (AiragFlow flow : flows) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
SubFlowResult subFlow = new SubFlowResult(flow);
|
||||||
|
// 获取入参参数
|
||||||
|
JSONArray parameter = getInputParameter(flow, subFlow);
|
||||||
|
// 获取出参参数
|
||||||
|
JSONArray outParams = getOutputParameter(flow, subFlow);
|
||||||
|
// name必须符合 ^[a-zA-Z0-9_-]+$
|
||||||
|
String validToolName = "flow_" + flow.getId();
|
||||||
|
// 将原始名称拼接到描述中
|
||||||
|
String description = flow.getName();
|
||||||
|
if (oConvertUtils.isNotEmpty(flow.getDescr())) {
|
||||||
|
description += " : " + flow.getDescr();
|
||||||
|
}
|
||||||
|
//构造工具参数
|
||||||
|
String flowTool = buildParameter(parameter, outParams, flow.getId(), tool.getTools(), validToolName, description);
|
||||||
|
tool.setTools(flowTool);
|
||||||
|
toolCount++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理流程[{}]转换插件失败: {}", flow.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||||
|
//构建元数据(请求头)
|
||||||
|
String meataData = buildMetadata(toolCount, tenantId);
|
||||||
|
tool.setMetadata(meataData);
|
||||||
|
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||||
|
result.put("pluginTool", tools);
|
||||||
|
result.put("pluginId", id);
|
||||||
|
log.info("构建流程插件结束");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建元数据
|
||||||
|
*
|
||||||
|
* @param toolCount
|
||||||
|
* @param tenantId
|
||||||
|
*/
|
||||||
|
private String buildMetadata(int toolCount, String tenantId) {
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||||
|
jsonObject.put(FlowPluginContent.TOOL_COUNT, toolCount);
|
||||||
|
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||||
|
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||||
|
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||||
|
return jsonObject.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建参数
|
||||||
|
*
|
||||||
|
* @param parameter
|
||||||
|
* @param outParams
|
||||||
|
* @param flowId
|
||||||
|
* @param tools
|
||||||
|
* @param description
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
private String buildParameter(JSONArray parameter, JSONArray outParams, String flowId, String tools, String name, String description) {
|
||||||
|
JSONArray paramArray = new JSONArray();
|
||||||
|
JSONObject parameterObject = new JSONObject();
|
||||||
|
parameterObject.put(FlowPluginContent.NAME, name);
|
||||||
|
parameterObject.put(FlowPluginContent.DESCRIPTION, description);
|
||||||
|
parameterObject.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_REQUEST_URL + flowId);
|
||||||
|
parameterObject.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||||
|
parameterObject.put(FlowPluginContent.ENABLED, true);
|
||||||
|
parameterObject.put(FlowPluginContent.PARAMETERS, parameter);
|
||||||
|
parameterObject.put(FlowPluginContent.RESPONSES, outParams);
|
||||||
|
if (oConvertUtils.isNotEmpty(tools)) {
|
||||||
|
paramArray = JSONArray.parseArray(tools);
|
||||||
|
paramArray.add(parameterObject);
|
||||||
|
} else {
|
||||||
|
paramArray.add(parameterObject);
|
||||||
|
}
|
||||||
|
return paramArray.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取参数
|
||||||
|
*
|
||||||
|
* @param flow
|
||||||
|
* @param subFlow
|
||||||
|
*/
|
||||||
|
private JSONArray getInputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||||
|
JSONArray parameters = new JSONArray();
|
||||||
|
String metadata = flow.getMetadata();
|
||||||
|
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||||
|
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||||
|
if (jsonObject.containsKey(FlowPluginContent.INPUTS)) {
|
||||||
|
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.INPUTS);
|
||||||
|
jsonArray.forEach(item -> {
|
||||||
|
if (oConvertUtils.isNotEmpty(item.toString())) {
|
||||||
|
JSONObject json = JSONObject.parseObject(item.toString());
|
||||||
|
json.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parameters.addAll(jsonArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//需要获取子流程的参数,子流程的参数是单独封装的,否则在流程执行的时候会报错缺少参数
|
||||||
|
List<FlowNodeConfig.NodeParam> inputParams = subFlow.getInputParams();
|
||||||
|
if (inputParams != null) {
|
||||||
|
for (FlowNodeConfig.NodeParam param : inputParams) {
|
||||||
|
JSONObject p = new JSONObject();
|
||||||
|
// 参数名
|
||||||
|
p.put(FlowPluginContent.NAME, param.getField());
|
||||||
|
String paramDesc = param.getName();
|
||||||
|
if (oConvertUtils.isEmpty(paramDesc)) {
|
||||||
|
paramDesc = param.getField();
|
||||||
|
}
|
||||||
|
// 参数描述
|
||||||
|
p.put(FlowPluginContent.DESCRIPTION, paramDesc);
|
||||||
|
// 类型
|
||||||
|
p.put(FlowPluginContent.TYPE, oConvertUtils.getString(param.getType(), FlowPluginContent.TYPE_STRING));
|
||||||
|
// 所有参数都在Body中
|
||||||
|
p.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||||
|
boolean required = param.getRequired() != null && param.getRequired();
|
||||||
|
p.put(FlowPluginContent.REQUIRED, required);
|
||||||
|
parameters.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建返回值
|
||||||
|
*/
|
||||||
|
private JSONArray getOutputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||||
|
JSONArray parameters = new JSONArray();
|
||||||
|
String metadata = flow.getMetadata();
|
||||||
|
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||||
|
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||||
|
if (jsonObject.containsKey(FlowPluginContent.OUTPUTS)) {
|
||||||
|
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.OUTPUTS);
|
||||||
|
parameters.addAll(jsonArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// List<FlowNodeConfig.NodeParam> outputParams = subFlow.getOutputParams();
|
||||||
|
// if (outputParams != null) {
|
||||||
|
// for (FlowNodeConfig.NodeParam param : outputParams) {
|
||||||
|
// JSONObject p = new JSONObject();
|
||||||
|
// // 参数名
|
||||||
|
// p.put("name", param.getField());
|
||||||
|
// String paramDesc = param.getName();
|
||||||
|
// if (oConvertUtils.isEmpty(paramDesc)) {
|
||||||
|
// paramDesc = param.getField();
|
||||||
|
// }
|
||||||
|
// // 参数描述
|
||||||
|
// p.put("description", paramDesc);
|
||||||
|
// // 类型
|
||||||
|
// p.put("type", oConvertUtils.getString(param.getType(), "String"));
|
||||||
|
// parameters.add(p);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -131,7 +131,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
|||||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
|
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
|
||||||
AssertUtils.assertNotEmpty("文档不存在", docList);
|
AssertUtils.assertNotEmpty("文档不存在", docList);
|
||||||
|
|
||||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
|
||||||
// 检查状态
|
// 检查状态
|
||||||
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
|
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
|
||||||
.filter(doc -> {
|
.filter(doc -> {
|
||||||
@ -330,6 +329,7 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
|||||||
if (File.separator.equals("\\")) {
|
if (File.separator.equals("\\")) {
|
||||||
// Windows path handling
|
// Windows path handling
|
||||||
String escapedPath = uploadpath.replace("//", "\\\\");
|
String escapedPath = uploadpath.replace("//", "\\\\");
|
||||||
|
escapedPath = escapedPath.replace("/", "\\\\");
|
||||||
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
|
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
|
||||||
} else {
|
} else {
|
||||||
// Unix path handling
|
// Unix path handling
|
||||||
|
|||||||
@ -1,18 +1,215 @@
|
|||||||
package org.jeecg.modules.airag.llm.service.impl;
|
package org.jeecg.modules.airag.llm.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||||
|
import dev.langchain4j.service.tool.ToolExecutor;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.util.DateUtils;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import org.jeecg.common.util.TokenUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||||
|
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||||
|
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||||
|
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||||
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
|
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
|
||||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Description: AIRag知识库
|
* @Description: AIRag知识库
|
||||||
* @Author: jeecg-boot
|
* @Author: jeecg-boot
|
||||||
* @Date: 2025-02-18
|
* @Date: 2025-02-18
|
||||||
* @Version: V1.0
|
* @Version: V1.0
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService {
|
public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getPluginMemory(String memoryId) {
|
||||||
|
//step 1获取知识库
|
||||||
|
AiragKnowledge airagKnowledge = this.baseMapper.selectById(memoryId);
|
||||||
|
if(airagKnowledge == null){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.getKnowledgeToPlugin(memoryId,airagKnowledge.getDescr());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件信息
|
||||||
|
*
|
||||||
|
* @param knowledgeId
|
||||||
|
* @param descr
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getKnowledgeToPlugin(String knowledgeId, String descr) {
|
||||||
|
//step1 构建插件
|
||||||
|
log.info("开始构建记忆库插件");
|
||||||
|
if (oConvertUtils.isEmpty(knowledgeId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||||
|
|
||||||
|
//返回数据
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
//插件
|
||||||
|
//插件id
|
||||||
|
AiragMcp tool = new AiragMcp();
|
||||||
|
// 2. 构建插件
|
||||||
|
tool.setId(knowledgeId);
|
||||||
|
// 插件名称
|
||||||
|
tool.setName(FlowPluginContent.PLUGIN_MEMORY_NAME);
|
||||||
|
// 描述
|
||||||
|
tool.setDescr(FlowPluginContent.PLUGIN_MEMORY_DESC);
|
||||||
|
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||||
|
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||||
|
tool.setCategory("plugin");
|
||||||
|
tool.setEndpoint("");
|
||||||
|
|
||||||
|
JSONArray toolsArray = new JSONArray();
|
||||||
|
// 添加记忆
|
||||||
|
toolsArray.add(buildAddMemoryTool(knowledgeId,descr));
|
||||||
|
// 查询记忆
|
||||||
|
toolsArray.add(buildQueryMemoryTool(knowledgeId,descr));
|
||||||
|
tool.setTools(toolsArray.toJSONString());
|
||||||
|
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||||
|
//构建元数据(请求头)
|
||||||
|
String meataData = buildMetadata(tenantId);
|
||||||
|
tool.setMetadata(meataData);
|
||||||
|
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||||
|
result.put("pluginTool", tools);
|
||||||
|
result.put("pluginId", knowledgeId);
|
||||||
|
log.info("构建记忆库插件结束");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建元数据
|
||||||
|
*
|
||||||
|
* @param tenantId
|
||||||
|
*/
|
||||||
|
private String buildMetadata(String tenantId) {
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||||
|
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||||
|
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||||
|
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||||
|
return jsonObject.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建添加记忆工具
|
||||||
|
*
|
||||||
|
* @param knowId
|
||||||
|
* @param descr
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private JSONObject buildAddMemoryTool(String knowId, String descr) {
|
||||||
|
JSONObject tool = new JSONObject();
|
||||||
|
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||||
|
tool.put(FlowPluginContent.NAME, "add_memory");
|
||||||
|
String addDescPrefix = "【自动触发】向记忆库添加长期信息。范围:";
|
||||||
|
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
|
||||||
|
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
|
||||||
|
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||||
|
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_ADD_PATH);
|
||||||
|
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||||
|
tool.put(FlowPluginContent.ENABLED, true);
|
||||||
|
|
||||||
|
JSONArray parameters = new JSONArray();
|
||||||
|
|
||||||
|
// 知识库ID参数
|
||||||
|
JSONObject knowIdParam = new JSONObject();
|
||||||
|
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
|
||||||
|
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
|
||||||
|
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||||
|
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||||
|
knowIdParam.put(FlowPluginContent.REQUIRED, true);
|
||||||
|
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
|
||||||
|
parameters.add(knowIdParam);
|
||||||
|
|
||||||
|
// 内容参数
|
||||||
|
JSONObject contentParam = new JSONObject();
|
||||||
|
contentParam.put(FlowPluginContent.NAME, "content");
|
||||||
|
contentParam.put(FlowPluginContent.DESCRIPTION, "记忆内容。当前时间为:" + DateUtils.now() + "。格式要求:'在yyyy年MM月dd日 HH:mm分,用户[用户的行为/问题],assistant[助手的回答/反应]。'");
|
||||||
|
contentParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||||
|
contentParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||||
|
contentParam.put(FlowPluginContent.REQUIRED, true);
|
||||||
|
parameters.add(contentParam);
|
||||||
|
|
||||||
|
// 标题参数
|
||||||
|
JSONObject titleParam = new JSONObject();
|
||||||
|
titleParam.put(FlowPluginContent.NAME, "title");
|
||||||
|
titleParam.put(FlowPluginContent.DESCRIPTION, "记忆标题");
|
||||||
|
titleParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||||
|
titleParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||||
|
titleParam.put(FlowPluginContent.REQUIRED, false);
|
||||||
|
parameters.add(titleParam);
|
||||||
|
tool.put(FlowPluginContent.PARAMETERS, parameters);
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
JSONArray responses = new JSONArray();
|
||||||
|
tool.put(FlowPluginContent.RESPONSES, responses);
|
||||||
|
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建查询记忆工具
|
||||||
|
*
|
||||||
|
* @param knowId
|
||||||
|
* @param descr
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private JSONObject buildQueryMemoryTool(String knowId, String descr) {
|
||||||
|
JSONObject tool = new JSONObject();
|
||||||
|
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||||
|
String addDescPrefix = "【自动触发】向记忆库检索信息。范围:";
|
||||||
|
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
|
||||||
|
tool.put(FlowPluginContent.NAME, "query_memory");
|
||||||
|
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
|
||||||
|
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||||
|
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_QUERY_PATH);
|
||||||
|
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||||
|
tool.put(FlowPluginContent.ENABLED, true);
|
||||||
|
|
||||||
|
JSONArray parameters = new JSONArray();
|
||||||
|
|
||||||
|
// 知识库ID参数
|
||||||
|
JSONObject knowIdParam = new JSONObject();
|
||||||
|
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
|
||||||
|
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
|
||||||
|
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||||
|
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||||
|
knowIdParam.put(FlowPluginContent.REQUIRED, true);
|
||||||
|
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
|
||||||
|
parameters.add(knowIdParam);
|
||||||
|
|
||||||
|
// 查询内容参数
|
||||||
|
JSONObject queryTextParam = new JSONObject();
|
||||||
|
queryTextParam.put(FlowPluginContent.NAME, "queryText");
|
||||||
|
queryTextParam.put(FlowPluginContent.DESCRIPTION, "查询内容");
|
||||||
|
queryTextParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||||
|
queryTextParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||||
|
queryTextParam.put(FlowPluginContent.REQUIRED, true);
|
||||||
|
parameters.add(queryTextParam);
|
||||||
|
|
||||||
|
tool.put(FlowPluginContent.PARAMETERS, parameters);
|
||||||
|
// 响应
|
||||||
|
JSONArray responses = new JSONArray();
|
||||||
|
tool.put(FlowPluginContent.RESPONSES, responses);
|
||||||
|
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,11 +10,14 @@ import dev.langchain4j.agent.tool.ToolSpecification;
|
|||||||
import dev.langchain4j.mcp.client.DefaultMcpClient;
|
import dev.langchain4j.mcp.client.DefaultMcpClient;
|
||||||
import dev.langchain4j.mcp.client.McpClient;
|
import dev.langchain4j.mcp.client.McpClient;
|
||||||
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
|
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
|
||||||
|
import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
|
||||||
|
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
|
||||||
import dev.langchain4j.model.chat.request.json.*;
|
import dev.langchain4j.model.chat.request.json.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jeecg.common.api.vo.Result;
|
import org.jeecg.common.api.vo.Result;
|
||||||
import org.jeecg.common.constant.CommonConstant;
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.AiRagConfigBean;
|
||||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||||
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
||||||
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
|
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
|
||||||
@ -22,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -31,12 +35,15 @@ import java.util.stream.Collectors;
|
|||||||
* @Date: 2025-10-20
|
* @Date: 2025-10-20
|
||||||
* @Version: V1.0
|
* @Version: V1.0
|
||||||
*/
|
*/
|
||||||
@Service("airagMcpServiceImpl")
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@SuppressWarnings("removal")
|
||||||
|
@Service("airagMcpServiceImpl")
|
||||||
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
|
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
|
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
|
||||||
|
@Autowired
|
||||||
|
private AiRagConfigBean aiRagConfigBean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增或编辑Mcpserver
|
* 新增或编辑Mcpserver
|
||||||
@ -125,7 +132,7 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
|||||||
Map<String, String> headers = null;
|
Map<String, String> headers = null;
|
||||||
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
|
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
|
||||||
try {
|
try {
|
||||||
headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
|
headers = JSONObject.parseObject(mcp.getHeaders(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {});
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
headers = null;
|
headers = null;
|
||||||
}
|
}
|
||||||
@ -136,19 +143,53 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
|||||||
McpClient mcpClient = null;
|
McpClient mcpClient = null;
|
||||||
try {
|
try {
|
||||||
if ("sse".equalsIgnoreCase(type)) {
|
if ("sse".equalsIgnoreCase(type)) {
|
||||||
HttpMcpTransport.Builder builder = new HttpMcpTransport.Builder()
|
//TODO 1.4.0-beta10被弃用,推荐使用http
|
||||||
|
log.info("[MCP]使用SSE协议(HttpMcpTransport), endpoint:{}", endpoint);
|
||||||
|
HttpMcpTransport.Builder builder = HttpMcpTransport.builder()
|
||||||
.sseUrl(endpoint)
|
.sseUrl(endpoint)
|
||||||
.logRequests(true)
|
.logRequests(true)
|
||||||
.logResponses(true);
|
.logResponses(true);
|
||||||
|
if (headers != null && !headers.isEmpty()) {
|
||||||
|
builder.customHeaders(headers);
|
||||||
|
}
|
||||||
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||||
} else if ("stdio".equalsIgnoreCase(type)) {
|
} else if ("stdio".equalsIgnoreCase(type)) {
|
||||||
// stdio 类型:endpoint 可能是一个命令行,需要拆分为命令列表
|
//update-begin---author:wangshuai---date:2025-12-18---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
|
||||||
// List<String> cmdParts = Arrays.asList(endpoint.trim().split("\\s+"));
|
String openSafe = aiRagConfigBean.getAllowSensitiveNodes();
|
||||||
// StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
|
if(oConvertUtils.isNotEmpty(openSafe) && openSafe.toLowerCase().contains("stdio")) {
|
||||||
// .command(cmdParts)
|
log.info("[MCP]使用STDIO协议(StdioMcpTransport), endpoint:{}", endpoint);
|
||||||
// .environment(headers);
|
// stdio 类型:endpoint 可能是一个命令行
|
||||||
// mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
// Windows 下需要通过 cmd.exe /c 来执行命令,否则找不到 npx 等程序
|
||||||
return Result.error("不支持的MCP类型:" + type);
|
List<String> cmdParts;
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
if (os.contains("win")) {
|
||||||
|
// Windows: 使用 cmd.exe /c 执行
|
||||||
|
cmdParts = new ArrayList<>();
|
||||||
|
cmdParts.add("cmd.exe");
|
||||||
|
cmdParts.add("/c");
|
||||||
|
cmdParts.add(endpoint.trim());
|
||||||
|
} else {
|
||||||
|
// Linux/Mac: 使用 sh -c 执行
|
||||||
|
cmdParts = new ArrayList<>();
|
||||||
|
cmdParts.add("sh");
|
||||||
|
cmdParts.add("-c");
|
||||||
|
cmdParts.add(endpoint.trim());
|
||||||
|
}
|
||||||
|
log.info("[MCP]执行stdio命令: {}", cmdParts);
|
||||||
|
StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
|
||||||
|
.command(cmdParts)
|
||||||
|
.environment(headers);
|
||||||
|
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||||
|
} else {
|
||||||
|
String disabledMsg = "stdio 功能已禁用。若需启用,请在 yml 的 jeecg.airag.allow-sensitive-nodes 中加入 stdio。";
|
||||||
|
log.warn("[MCP]{}", disabledMsg);
|
||||||
|
return Result.error(disabledMsg);
|
||||||
|
}
|
||||||
|
//update-end---author:wangshuai---date:2025-12-19---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
|
||||||
|
}else if("http".equalsIgnoreCase(type)){
|
||||||
|
log.info("[MCP]使用HTTP协议(StreamableHttpMcpTransport), endpoint:{}", endpoint);
|
||||||
|
//增加http选项
|
||||||
|
mcpClient = mcpHttpCreate(endpoint,headers);
|
||||||
} else {
|
} else {
|
||||||
return Result.error("不支持的MCP类型:" + type);
|
return Result.error("不支持的MCP类型:" + type);
|
||||||
}
|
}
|
||||||
@ -204,6 +245,29 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mcp插件http创建
|
||||||
|
*
|
||||||
|
* @param endpoint
|
||||||
|
* @param headers
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private McpClient mcpHttpCreate(String endpoint, Map<String, String> headers) {
|
||||||
|
StreamableHttpMcpTransport.Builder builder = new StreamableHttpMcpTransport.Builder()
|
||||||
|
.url(endpoint)
|
||||||
|
.timeout(Duration.ofMinutes(60))
|
||||||
|
.logRequests(true)
|
||||||
|
.logResponses(true);
|
||||||
|
|
||||||
|
if (headers != null && !headers.isEmpty()) {
|
||||||
|
builder.customHeaders(headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultMcpClient.Builder()
|
||||||
|
.transport(builder.build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
// 安全序列化单个对象为 JSON 字符串
|
// 安全序列化单个对象为 JSON 字符串
|
||||||
private String safeWriteJson(Object obj) {
|
private String safeWriteJson(Object obj) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.consts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI提示词常量类
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class AiPromptsConsts {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:进行中
|
||||||
|
*/
|
||||||
|
public static final String STATUS_RUNNING = "run";
|
||||||
|
/**
|
||||||
|
* 状态:完成
|
||||||
|
*/
|
||||||
|
public static final String STATUS_COMPLETED = "completed";
|
||||||
|
/**
|
||||||
|
* 状态:失败
|
||||||
|
*/
|
||||||
|
public static final String STATUS_FAILED = "failed";
|
||||||
|
/**
|
||||||
|
* 业务类型:评估器
|
||||||
|
*/
|
||||||
|
public static final String BIZ_TYPE_EVALUATOR = "evaluator";
|
||||||
|
/**
|
||||||
|
* 业务类型:轨迹
|
||||||
|
*/
|
||||||
|
public static final String BIZ_TYPE_TRACK = "track";
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,213 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||||
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||||
|
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||||
|
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_ext_data
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-24
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Tag(name="airag_ext_data")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/airag/extData")
|
||||||
|
@Slf4j
|
||||||
|
public class AiragExtDataController extends JeecgController<AiragExtData, IAiragExtDataService> {
|
||||||
|
@Autowired
|
||||||
|
private IAiragExtDataService airagExtDataService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页列表查询
|
||||||
|
*
|
||||||
|
* @param airagExtData
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
//@AutoLog(value = "airag_ext_data-分页列表查询")
|
||||||
|
@Operation(summary="airag_ext_data-分页列表查询")
|
||||||
|
@GetMapping(value = "/list")
|
||||||
|
public Result<IPage<AiragExtData>> queryPageList(AiragExtData airagExtData,
|
||||||
|
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||||
|
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||||
|
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||||
|
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||||
|
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 调用轨迹列表查询
|
||||||
|
*
|
||||||
|
* @param airagExtData
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Operation(summary="airag_ext_data-分页列表查询")
|
||||||
|
@GetMapping(value = "/getTrackList")
|
||||||
|
public Result<IPage<AiragExtData>> getTrackList(AiragExtData airagExtData,
|
||||||
|
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||||
|
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||||
|
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||||
|
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||||
|
String metadata = airagExtData.getMetadata();
|
||||||
|
if(oConvertUtils.isEmpty(metadata)){
|
||||||
|
return Result.OK();
|
||||||
|
}
|
||||||
|
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*
|
||||||
|
* @param airagExtData
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_ext_data-添加")
|
||||||
|
@Operation(summary="airag_ext_data-添加")
|
||||||
|
@PostMapping(value = "/add")
|
||||||
|
public Result<String> add(@RequestBody AiragExtData airagExtData) {
|
||||||
|
airagExtData.setBizType(AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||||
|
airagExtDataService.save(airagExtData);
|
||||||
|
return Result.OK("添加成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*
|
||||||
|
* @param airagExtData
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_ext_data-编辑")
|
||||||
|
@Operation(summary="airag_ext_data-编辑")
|
||||||
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||||
|
public Result<String> edit(@RequestBody AiragExtData airagExtData) {
|
||||||
|
airagExtDataService.updateById(airagExtData);
|
||||||
|
return Result.OK("编辑成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id删除
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_ext_data-通过id删除")
|
||||||
|
@Operation(summary="airag_ext_data-通过id删除")
|
||||||
|
@DeleteMapping(value = "/delete")
|
||||||
|
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||||
|
airagExtDataService.removeById(id);
|
||||||
|
return Result.OK("删除成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除
|
||||||
|
*
|
||||||
|
* @param ids
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_ext_data-批量删除")
|
||||||
|
@Operation(summary="airag_ext_data-批量删除")
|
||||||
|
@DeleteMapping(value = "/deleteBatch")
|
||||||
|
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||||
|
this.airagExtDataService.removeByIds(Arrays.asList(ids.split(",")));
|
||||||
|
return Result.OK("批量删除成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id查询
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||||
|
@Operation(summary="airag_ext_data-通过id查询")
|
||||||
|
@GetMapping(value = "/queryById")
|
||||||
|
public Result<AiragExtData> queryById(@RequestParam(name="id",required=true) String id) {
|
||||||
|
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||||
|
if(airagExtData==null) {
|
||||||
|
return Result.error("未找到对应数据");
|
||||||
|
}
|
||||||
|
return Result.OK(airagExtData);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 通过id查询
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||||
|
@Operation(summary="airag_ext_data-通过id查询")
|
||||||
|
@GetMapping(value = "/queryTrackById")
|
||||||
|
public Result<List<AiragExtData>> queryTrackById(@RequestParam(name="id",required=true) String id) {
|
||||||
|
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||||
|
String status = airagExtData.getStatus();
|
||||||
|
if(AiPromptsConsts.STATUS_RUNNING.equals(status)) {
|
||||||
|
return Result.error("处理中,请稍后刷新");
|
||||||
|
}
|
||||||
|
List<AiragExtData> trackList = airagExtDataService.queryTrackById(id);
|
||||||
|
return Result.OK(trackList);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 构造器调试
|
||||||
|
*
|
||||||
|
* @param debugVo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/evaluator/debug")
|
||||||
|
public Result<?> debugEvaluator(@RequestBody AiragDebugVo debugVo) {
|
||||||
|
return airagExtDataService.debugEvaluator(debugVo);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 导出excel
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param airagExtData
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/exportXls")
|
||||||
|
public ModelAndView exportXls(HttpServletRequest request, AiragExtData airagExtData) {
|
||||||
|
return super.exportXls(request, airagExtData, AiragExtData.class, "airag_ext_data");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过excel导入数据
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param response
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||||
|
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
return super.importExcel(request, response, AiragExtData.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||||
|
import org.jeecg.common.constant.CommonConstant;
|
||||||
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||||
|
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||||
|
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
/**
|
||||||
|
* @Description: airag_prompts
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-24
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Tag(name="airag_prompts")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/airag/prompts")
|
||||||
|
@Slf4j
|
||||||
|
public class AiragPromptsController extends JeecgController<AiragPrompts, IAiragPromptsService> {
|
||||||
|
@Autowired
|
||||||
|
private IAiragPromptsService airagPromptsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页列表查询
|
||||||
|
*
|
||||||
|
* @param airagPrompts
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
//@AutoLog(value = "airag_prompts-分页列表查询")
|
||||||
|
@Operation(summary="airag_prompts-分页列表查询")
|
||||||
|
@GetMapping(value = "/list")
|
||||||
|
public Result<IPage<AiragPrompts>> queryPageList(AiragPrompts airagPrompts,
|
||||||
|
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||||
|
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
QueryWrapper<AiragPrompts> queryWrapper = QueryGenerator.initQueryWrapper(airagPrompts, req.getParameterMap());
|
||||||
|
Page<AiragPrompts> page = new Page<AiragPrompts>(pageNo, pageSize);
|
||||||
|
IPage<AiragPrompts> pageList = airagPromptsService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*
|
||||||
|
* @param airagPrompts
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_prompts-添加")
|
||||||
|
@Operation(summary="airag_prompts-添加")
|
||||||
|
@PostMapping(value = "/add")
|
||||||
|
public Result<String> add(@RequestBody AiragPrompts airagPrompts) {
|
||||||
|
airagPrompts.setDelFlag(CommonConstant.DEL_FLAG_0);
|
||||||
|
airagPrompts.setStatus("0");
|
||||||
|
airagPromptsService.save(airagPrompts);
|
||||||
|
return Result.OK("添加成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*
|
||||||
|
* @param airagPrompts
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_prompts-编辑")
|
||||||
|
@Operation(summary="airag_prompts-编辑")
|
||||||
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||||
|
public Result<String> edit(@RequestBody AiragPrompts airagPrompts) {
|
||||||
|
airagPromptsService.updateById(airagPrompts);
|
||||||
|
return Result.OK("编辑成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id删除
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_prompts-通过id删除")
|
||||||
|
@Operation(summary="airag_prompts-通过id删除")
|
||||||
|
@DeleteMapping(value = "/delete")
|
||||||
|
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||||
|
airagPromptsService.removeById(id);
|
||||||
|
return Result.OK("删除成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除
|
||||||
|
*
|
||||||
|
* @param ids
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "airag_prompts-批量删除")
|
||||||
|
@Operation(summary="airag_prompts-批量删除")
|
||||||
|
@DeleteMapping(value = "/deleteBatch")
|
||||||
|
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||||
|
this.airagPromptsService.removeByIds(Arrays.asList(ids.split(",")));
|
||||||
|
return Result.OK("批量删除成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id查询
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
//@AutoLog(value = "airag_prompts-通过id查询")
|
||||||
|
@Operation(summary="airag_prompts-通过id查询")
|
||||||
|
@GetMapping(value = "/queryById")
|
||||||
|
public Result<AiragPrompts> queryById(@RequestParam(name="id",required=true) String id) {
|
||||||
|
AiragPrompts airagPrompts = airagPromptsService.getById(id);
|
||||||
|
if(airagPrompts==null) {
|
||||||
|
return Result.error("未找到对应数据");
|
||||||
|
}
|
||||||
|
return Result.OK(airagPrompts);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 构造器调试
|
||||||
|
*
|
||||||
|
* @param experimentVo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/experiment")
|
||||||
|
public Result<?> promptExperiment(@RequestBody AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||||
|
return airagPromptsService.promptExperiment(experimentVo,request);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 导出excel
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param airagPrompts
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/exportXls")
|
||||||
|
public ModelAndView exportXls(HttpServletRequest request, AiragPrompts airagPrompts) {
|
||||||
|
return super.exportXls(request, airagPrompts, AiragPrompts.class, "airag_prompts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过excel导入数据
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param response
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||||
|
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
return super.importExcel(request, response, AiragPrompts.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import org.jeecg.common.constant.ProvinceCityArea;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import lombok.Data;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.jeecg.common.aspect.annotation.Dict;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_ext_data
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("airag_ext_data")
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(description="airag_ext_data")
|
||||||
|
public class AiragExtData implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**主键ID*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
private java.lang.String id;
|
||||||
|
/**业务类型标识(evaluator:评估器;track:测试追踪)*/
|
||||||
|
@Excel(name = "业务类型标识(evaluator:评估器;track:测试追踪)", width = 15)
|
||||||
|
@Schema(description = "业务类型标识(evaluator:评估器;track:测试追踪)")
|
||||||
|
private java.lang.String bizType;
|
||||||
|
/**名称*/
|
||||||
|
@Excel(name = "名称", width = 15)
|
||||||
|
@Schema(description = "名称")
|
||||||
|
private java.lang.String name;
|
||||||
|
/**描述信息*/
|
||||||
|
@Excel(name = "描述信息", width = 15)
|
||||||
|
@Schema(description = "描述信息")
|
||||||
|
private java.lang.String descr;
|
||||||
|
/**标签,多个用逗号分隔*/
|
||||||
|
@Excel(name = "标签,多个用逗号分隔", width = 15)
|
||||||
|
@Schema(description = "标签,多个用逗号分隔")
|
||||||
|
private java.lang.String tags;
|
||||||
|
/**实际存储内容,json*/
|
||||||
|
@Excel(name = "实际存储内容,json", width = 15)
|
||||||
|
@Schema(description = "实际存储内容,json")
|
||||||
|
private java.lang.String dataValue;
|
||||||
|
/**元数据,用于存储补充业务数据信息*/
|
||||||
|
@Excel(name = "元数据,用于存储补充业务数据信息", width = 15)
|
||||||
|
@Schema(description = "元数据,用于存储补充业务数据信息")
|
||||||
|
private java.lang.String metadata;
|
||||||
|
/**评测集数据*/
|
||||||
|
@Excel(name = "评测集数据", width = 15)
|
||||||
|
@Schema(description = "评测集数据")
|
||||||
|
private java.lang.String datasetValue;
|
||||||
|
/**创建人*/
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
private java.lang.String createBy;
|
||||||
|
/**创建时间*/
|
||||||
|
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private java.util.Date createTime;
|
||||||
|
/**修改人*/
|
||||||
|
@Schema(description = "修改人")
|
||||||
|
private java.lang.String updateBy;
|
||||||
|
/**修改时间*/
|
||||||
|
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "修改时间")
|
||||||
|
private java.util.Date updateTime;
|
||||||
|
/**所属部门*/
|
||||||
|
@Schema(description = "所属部门")
|
||||||
|
private java.lang.String sysOrgCode;
|
||||||
|
/**租户id*/
|
||||||
|
@Excel(name = "租户id", width = 15)
|
||||||
|
@Schema(description = "租户id")
|
||||||
|
private java.lang.String tenantId;
|
||||||
|
/**状态*/
|
||||||
|
@Excel(name = "状态(run:进行中 completed:已完成)", width = 15)
|
||||||
|
@Schema(description = "状态(run:进行中 completed:已完成)")
|
||||||
|
private java.lang.String status;
|
||||||
|
/**版本*/
|
||||||
|
@Excel(name = "版本", width = 15)
|
||||||
|
@Schema(description = "版本")
|
||||||
|
private java.lang.Integer version;
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.entity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import org.jeecg.common.constant.ProvinceCityArea;
|
||||||
|
import org.jeecg.common.util.SpringContextUtils;
|
||||||
|
import lombok.Data;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.jeecg.common.aspect.annotation.Dict;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_prompts
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("airag_prompts")
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(description="airag_prompts")
|
||||||
|
public class AiragPrompts implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**主键ID*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
private java.lang.String id;
|
||||||
|
/**提示词名称*/
|
||||||
|
@Excel(name = "提示词名称", width = 15)
|
||||||
|
@Schema(description = "提示词名称")
|
||||||
|
private java.lang.String name;
|
||||||
|
/**提示词名称*/
|
||||||
|
@Excel(name = "提示key", width = 15)
|
||||||
|
@Schema(description = "提示key")
|
||||||
|
private java.lang.String promptKey;
|
||||||
|
/**提示词功能描述*/
|
||||||
|
@Excel(name = "提示词功能描述", width = 15)
|
||||||
|
@Schema(description = "提示词功能描述")
|
||||||
|
private java.lang.String description;
|
||||||
|
/**提示词模板内容,支持变量占位符如 {{variable}}*/
|
||||||
|
@Excel(name = "提示词模板内容,支持变量占位符如 {{variable}}", width = 15)
|
||||||
|
@Schema(description = "提示词模板内容,支持变量占位符如 {{variable}}")
|
||||||
|
private java.lang.String content;
|
||||||
|
/**提示词分类*/
|
||||||
|
@Excel(name = "提示词分类", width = 15)
|
||||||
|
@Schema(description = "提示词分类")
|
||||||
|
private java.lang.String category;
|
||||||
|
/**标签,多个逗号分割*/
|
||||||
|
@Excel(name = "标签,多个逗号分割", width = 15)
|
||||||
|
@Schema(description = "标签,多个逗号分割")
|
||||||
|
private java.lang.String tags;
|
||||||
|
/**适配的大模型ID*/
|
||||||
|
@Excel(name = "适配的大模型ID", width = 15)
|
||||||
|
@Schema(description = "适配的大模型ID")
|
||||||
|
private java.lang.String modelId;
|
||||||
|
/**大模型的参数配置*/
|
||||||
|
@Excel(name = "大模型的参数配置", width = 15)
|
||||||
|
@Schema(description = "大模型的参数配置")
|
||||||
|
private java.lang.String modelParam;
|
||||||
|
/**状态(0:未发布 1:已发布)*/
|
||||||
|
@Excel(name = "状态(0:未发布 1:已发布)", width = 15)
|
||||||
|
@Schema(description = "状态(0:未发布 1:已发布)")
|
||||||
|
private java.lang.String status;
|
||||||
|
/**版本号(格式 0.0.1)*/
|
||||||
|
@Excel(name = "版本号(格式 0.0.1)", width = 15)
|
||||||
|
@Schema(description = "版本号(格式 0.0.1)")
|
||||||
|
private java.lang.String version;
|
||||||
|
/**删除状态(0未删除 1已删除)*/
|
||||||
|
@Excel(name = "删除状态(0未删除 1已删除)", width = 15)
|
||||||
|
@Schema(description = "删除状态(0未删除 1已删除)")
|
||||||
|
@TableLogic
|
||||||
|
private java.lang.Integer delFlag;
|
||||||
|
/**创建人*/
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
private java.lang.String createBy;
|
||||||
|
/**创建日期*/
|
||||||
|
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "创建日期")
|
||||||
|
private java.util.Date createTime;
|
||||||
|
/**更新人*/
|
||||||
|
@Schema(description = "更新人")
|
||||||
|
private java.lang.String updateBy;
|
||||||
|
/**更新日期*/
|
||||||
|
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "更新日期")
|
||||||
|
private java.util.Date updateTime;
|
||||||
|
/**所属部门*/
|
||||||
|
@Schema(description = "所属部门")
|
||||||
|
private java.lang.String sysOrgCode;
|
||||||
|
/**租户id*/
|
||||||
|
@Excel(name = "租户id", width = 15)
|
||||||
|
@Schema(description = "租户id")
|
||||||
|
private java.lang.String tenantId;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_ext_data
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface AiragExtDataMapper extends BaseMapper<AiragExtData> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_prompts
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface AiragPromptsMapper extends BaseMapper<AiragPrompts> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?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.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?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.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.service;
|
||||||
|
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_ext_data
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface IAiragExtDataService extends IService<AiragExtData> {
|
||||||
|
|
||||||
|
Result debugEvaluator(AiragDebugVo debugVo);
|
||||||
|
|
||||||
|
List<AiragExtData> queryTrackById(String id);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.service;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_prompts
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface IAiragPromptsService extends IService<AiragPrompts> {
|
||||||
|
|
||||||
|
Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request);
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
|
import dev.langchain4j.data.message.SystemMessage;
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||||
|
import org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper;
|
||||||
|
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||||
|
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_ext_data
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Service("airagExtDataServiceImpl")
|
||||||
|
public class AiragExtDataServiceImpl extends ServiceImpl<AiragExtDataMapper, AiragExtData> implements IAiragExtDataService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAIChatHandler aiChatHandler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result debugEvaluator(AiragDebugVo debugVo) {
|
||||||
|
//1.提示词
|
||||||
|
String prompt = debugVo.getPrompts();
|
||||||
|
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||||
|
|
||||||
|
//2.测试内容
|
||||||
|
String content = debugVo.getContent();
|
||||||
|
AssertUtils.assertNotEmpty("请输入测试内容", content);
|
||||||
|
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(content));
|
||||||
|
|
||||||
|
//3.模型数据
|
||||||
|
String modelId = debugVo.getModelId();
|
||||||
|
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||||
|
|
||||||
|
//4.模型参数
|
||||||
|
String modelParam = debugVo.getModelParam();
|
||||||
|
// 默认大模型参数
|
||||||
|
AIChatParams params = new AIChatParams();
|
||||||
|
params.setTemperature(0.8);
|
||||||
|
params.setTopP(0.9);
|
||||||
|
params.setPresencePenalty(0.1);
|
||||||
|
params.setFrequencyPenalty(0.1);
|
||||||
|
|
||||||
|
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||||
|
JSONObject param = JSON.parseObject(modelParam);
|
||||||
|
if(param.containsKey("temperature")){
|
||||||
|
params.setTemperature(param.getDoubleValue("temperature"));
|
||||||
|
}
|
||||||
|
if(param.containsKey("topP")){
|
||||||
|
params.setTemperature(param.getDoubleValue("topP"));
|
||||||
|
}
|
||||||
|
if(param.containsKey("presencePenalty")){
|
||||||
|
params.setTemperature(param.getDoubleValue("presencePenalty"));
|
||||||
|
}
|
||||||
|
if(param.containsKey("frequencyPenalty")){
|
||||||
|
params.setTemperature(param.getDoubleValue("frequencyPenalty"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//5.AI问答
|
||||||
|
String promptValue = aiChatHandler.completions(modelId,messages, params);
|
||||||
|
if (promptValue == null || promptValue.isEmpty()) {
|
||||||
|
return Result.error("生成失败");
|
||||||
|
}
|
||||||
|
return Result.OK("success", promptValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询AI问答记录
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<AiragExtData> queryTrackById(String id) {
|
||||||
|
LambdaQueryWrapper<AiragExtData> lqw = new LambdaQueryWrapper<AiragExtData>()
|
||||||
|
.eq(AiragExtData::getMetadata, id)
|
||||||
|
.orderByDesc(AiragExtData::getVersion)
|
||||||
|
.orderByDesc(AiragExtData::getCreateTime);
|
||||||
|
List<AiragExtData> list = this.baseMapper.selectList(lqw);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,394 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
|
import dev.langchain4j.data.message.SystemMessage;
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.jeecg.common.util.CommonUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.config.JeecgBaseConfig;
|
||||||
|
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||||
|
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||||
|
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||||
|
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||||
|
import org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper;
|
||||||
|
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||||
|
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||||
|
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: airag_prompts
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service("airagPromptsServiceImpl")
|
||||||
|
public class AiragPromptsServiceImpl extends ServiceImpl<AiragPromptsMapper, AiragPrompts> implements IAiragPromptsService {
|
||||||
|
@Autowired
|
||||||
|
IAIChatHandler aiChatHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
IAiragExtDataService airagExtDataService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JeecgBaseConfig jeecgBaseConfig;
|
||||||
|
// 创建静态线程池,确保整个应用生命周期中只有一个实例
|
||||||
|
private static final ExecutorService executor = new ThreadPoolExecutor(
|
||||||
|
4, // 核心线程数
|
||||||
|
8, // 最大线程数
|
||||||
|
60L, TimeUnit.SECONDS,
|
||||||
|
new ArrayBlockingQueue<>(100), // 防止内存溢出
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提示词实验
|
||||||
|
* @param experimentVo
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||||
|
log.info("开始执行提示词实验,参数:{}", JSON.toJSONString(experimentVo));
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
String promptKey = experimentVo.getPromptKey();
|
||||||
|
AssertUtils.assertNotEmpty("请选择提示词", promptKey);
|
||||||
|
String dataId = experimentVo.getExtDataId();
|
||||||
|
AssertUtils.assertNotEmpty("请选择数据集", dataId);
|
||||||
|
|
||||||
|
Map<String, String> fieldMappings = experimentVo.getMappings();
|
||||||
|
AssertUtils.assertNotEmpty("请配置字段映射", fieldMappings);
|
||||||
|
|
||||||
|
try {
|
||||||
|
//1.查询提示词
|
||||||
|
AiragPrompts airagPrompts = this.baseMapper.selectOne(new LambdaQueryWrapper<AiragPrompts>().eq(AiragPrompts::getPromptKey, promptKey));
|
||||||
|
AssertUtils.assertNotEmpty("未找到指定的提示词", airagPrompts);
|
||||||
|
String modelParam = airagPrompts.getModelParam();
|
||||||
|
// 过滤提示词变量
|
||||||
|
JSONArray promptVariables;
|
||||||
|
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||||
|
JSONObject airagPromptsParams = JSON.parseObject(modelParam);
|
||||||
|
if(airagPromptsParams.containsKey("promptVariables")){
|
||||||
|
promptVariables = airagPromptsParams.getJSONArray("promptVariables");
|
||||||
|
} else {
|
||||||
|
promptVariables = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
promptVariables = null;
|
||||||
|
}
|
||||||
|
//2.查询数据集
|
||||||
|
AiragExtData airagExtData = airagExtDataService.getById(dataId);
|
||||||
|
AssertUtils.assertNotEmpty("未找到指定的数据集", airagExtData);
|
||||||
|
String datasetValue = airagExtData.getDatasetValue();
|
||||||
|
if(oConvertUtils.isEmpty(datasetValue)){
|
||||||
|
return Result.error("评测集不能为空!");
|
||||||
|
}
|
||||||
|
|
||||||
|
//3.异步调用 根据映射字段,调用评估器测评
|
||||||
|
JSONObject datasetObj = JSONObject.parseObject(datasetValue);
|
||||||
|
//评测列配置
|
||||||
|
JSONArray columns = datasetObj.getJSONArray("columns");
|
||||||
|
//评测题库
|
||||||
|
JSONArray datasetArray = datasetObj.getJSONArray("dataSource");
|
||||||
|
AssertUtils.assertNotEmpty("数据集中没有找到数据源", datasetArray);
|
||||||
|
AssertUtils.assertTrue("数据源为空", datasetArray.size() > 0);
|
||||||
|
|
||||||
|
//测评结果集 - 使用线程安全的CopyOnWriteArrayList
|
||||||
|
List<JSONObject> scoreResult = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
// 批量提交任务
|
||||||
|
List<CompletableFuture<Void>> futures = IntStream.range(0, datasetArray.size())
|
||||||
|
.mapToObj(i -> CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
log.info("开始处理第{}条数据", i + 1);
|
||||||
|
//定义返回结果
|
||||||
|
JSONObject result = new JSONObject();
|
||||||
|
//评测数据
|
||||||
|
JSONObject dataset = datasetArray.getJSONObject(i);
|
||||||
|
result.putAll(dataset);
|
||||||
|
//用户问题
|
||||||
|
String userQuery = dataset.getString(fieldMappings.get("user_query"));
|
||||||
|
result.put("userQuery", userQuery);
|
||||||
|
//变量处理
|
||||||
|
if(!CollectionUtils.isEmpty(promptVariables)){
|
||||||
|
String content = airagPrompts.getContent();
|
||||||
|
for (Object var : promptVariables){
|
||||||
|
JSONObject variable = JSONObject.parseObject(var.toString());
|
||||||
|
String name = dataset.getString(fieldMappings.get(variable.getString("name")));
|
||||||
|
//提示词默认变量值
|
||||||
|
String defaultValue = variable.getString("value");
|
||||||
|
// 获取目标类型
|
||||||
|
String dataType = findDataType(columns, variable);
|
||||||
|
if("FILE".equals(dataType)){
|
||||||
|
defaultValue = getFileAccessHttpUrl(request, defaultValue);
|
||||||
|
name = getFileAccessHttpUrl(request, name);
|
||||||
|
}
|
||||||
|
if(oConvertUtils.isNotEmpty(name)){
|
||||||
|
//提示词 评估集变量值替换
|
||||||
|
content = content.replaceAll(variable.getString("name"), name);
|
||||||
|
}else if(oConvertUtils.isNotEmpty(defaultValue)){
|
||||||
|
content = content.replaceAll(variable.getString("name"), defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
airagPrompts.setContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
//提示词答案
|
||||||
|
String promptAnswer = getPromptAnswer(airagPrompts, dataset, fieldMappings);
|
||||||
|
result.put("promptAnswer", promptAnswer);
|
||||||
|
|
||||||
|
//评估器答案
|
||||||
|
String answerScore = getAnswerScore(promptAnswer, dataset, fieldMappings, airagExtData);
|
||||||
|
result.put("answerScore", answerScore);
|
||||||
|
|
||||||
|
scoreResult.add(result);
|
||||||
|
log.info("第{}条数据处理完成", i + 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理第{}条数据时发生异常", i + 1, e);
|
||||||
|
// 重新抛出异常,让CompletableFuture捕获
|
||||||
|
throw new CompletionException(e);
|
||||||
|
}
|
||||||
|
}, executor))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 非阻塞方式处理完成
|
||||||
|
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||||
|
.whenComplete((result, ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
log.error("批量处理失败", ex);
|
||||||
|
// 更新状态为失败
|
||||||
|
airagExtData.setStatus(AiPromptsConsts.STATUS_FAILED);
|
||||||
|
} else {
|
||||||
|
log.info("所有数据处理完成,共处理{}条数据", scoreResult.size());
|
||||||
|
// 查询已存在的评测记录
|
||||||
|
List<AiragExtData> existingTracks = airagExtDataService.queryTrackById(dataId);
|
||||||
|
Integer version = 1;
|
||||||
|
if(!CollectionUtils.isEmpty(existingTracks)) {
|
||||||
|
version = existingTracks.stream()
|
||||||
|
.map(AiragExtData::getVersion)
|
||||||
|
.max(Integer::compareTo)
|
||||||
|
.orElse(0) + 1;
|
||||||
|
}
|
||||||
|
for (JSONObject item : scoreResult) {
|
||||||
|
// 保存结果
|
||||||
|
AiragExtData track = new AiragExtData();
|
||||||
|
//关联评估器ID
|
||||||
|
track.setMetadata(dataId);
|
||||||
|
//定义类型
|
||||||
|
track.setBizType(AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||||
|
//定义版本
|
||||||
|
track.setVersion(version);
|
||||||
|
//定义状态
|
||||||
|
track.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||||
|
//定义评测结果
|
||||||
|
track.setDataValue(item.toJSONString());
|
||||||
|
airagExtDataService.save(track);
|
||||||
|
}
|
||||||
|
// 更新状态为完成
|
||||||
|
airagExtData.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||||
|
}
|
||||||
|
airagExtDataService.updateById(airagExtData);
|
||||||
|
});
|
||||||
|
|
||||||
|
//4.修改状态进行中
|
||||||
|
airagExtData.setStatus(AiPromptsConsts.STATUS_RUNNING);
|
||||||
|
airagExtDataService.updateById(airagExtData);
|
||||||
|
|
||||||
|
log.info("提示词实验已提交,共{}条数据待处理", datasetArray.size());
|
||||||
|
return Result.OK("实验已开始,正在处理数据");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("提示词实验执行失败", e);
|
||||||
|
return Result.error("实验执行失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提示词回答的结果
|
||||||
|
* @param airagPrompts
|
||||||
|
* @param questions
|
||||||
|
* @param fieldMappings
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getPromptAnswer(AiragPrompts airagPrompts, JSONObject questions, Map<String, String> fieldMappings) {
|
||||||
|
try {
|
||||||
|
//0.判断是否配置了判断fieldMappings的value值 是否包含actual_output
|
||||||
|
if (!fieldMappings.containsValue("actual_output")) {
|
||||||
|
log.warn("字段映射中没有配置actual_output");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//1.提示词
|
||||||
|
String prompt = airagPrompts.getContent();
|
||||||
|
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||||
|
|
||||||
|
String userQuery = questions.getString(fieldMappings.get("user_query"));
|
||||||
|
AssertUtils.assertNotEmpty("请输入测试内容", userQuery);
|
||||||
|
|
||||||
|
//2.ai问题组装
|
||||||
|
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||||
|
|
||||||
|
//3.模型数据
|
||||||
|
String modelId = airagPrompts.getModelId();
|
||||||
|
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||||
|
|
||||||
|
//4.模型参数
|
||||||
|
String modelParam = airagPrompts.getModelParam();
|
||||||
|
// 默认大模型参数
|
||||||
|
AIChatParams params = new AIChatParams();
|
||||||
|
params.setTemperature(0.8);
|
||||||
|
params.setTopP(0.9);
|
||||||
|
params.setPresencePenalty(0.1);
|
||||||
|
params.setFrequencyPenalty(0.1);
|
||||||
|
|
||||||
|
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||||
|
JSONObject param = JSON.parseObject(modelParam);
|
||||||
|
if(param.containsKey("temperature")){
|
||||||
|
params.setTemperature(param.getDoubleValue("temperature"));
|
||||||
|
}
|
||||||
|
if(param.containsKey("topP")){
|
||||||
|
params.setTopP(param.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||||
|
}
|
||||||
|
if(param.containsKey("presencePenalty")){
|
||||||
|
params.setPresencePenalty(param.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||||
|
}
|
||||||
|
if(param.containsKey("frequencyPenalty")){
|
||||||
|
params.setFrequencyPenalty(param.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("调用AI模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||||
|
//5.AI问答
|
||||||
|
String promptAnswer = aiChatHandler.completions(modelId, messages, params);
|
||||||
|
log.debug("AI模型返回结果:{}", promptAnswer);
|
||||||
|
|
||||||
|
return promptAnswer;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取提示词回答失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评测答案分数
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getAnswerScore(String promptAnswer, JSONObject questions, Map<String, String> fieldMappings, AiragExtData airagExtData) {
|
||||||
|
try {
|
||||||
|
//1.提示词
|
||||||
|
String prompt = airagExtData.getDataValue();
|
||||||
|
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||||
|
prompt += "定义返回格式: 得分:最终的得分,必须输出一个数字,表示满足Prompt中评分标准的程度。得分范围从 0.0 到 1.0,1.0 表示完全满足评分标准,0.0 表示完全不满足评分标准。\n" +
|
||||||
|
"原因:{对得分的可读性的解释,说明打分原因}。最后,必须用一句话结束理由,该句话为:因此,应该给出的分数是<你评测的的得分>。请勿返回提问的问题、添加分析过程、解释说明等内容,只返回要求的格式内容";
|
||||||
|
|
||||||
|
String userQuery = "输入的内容:";
|
||||||
|
//2.拼接测试内容
|
||||||
|
for (Map.Entry<String, String> entry : fieldMappings.entrySet()) {
|
||||||
|
// 评估器中的key
|
||||||
|
String key = entry.getKey();
|
||||||
|
// 评估器中的映射的key
|
||||||
|
String value = entry.getValue();
|
||||||
|
String valueData;
|
||||||
|
if("actual_output".equalsIgnoreCase(value)){
|
||||||
|
valueData = promptAnswer;
|
||||||
|
}else{
|
||||||
|
valueData = questions.getString(value);
|
||||||
|
}
|
||||||
|
userQuery += (key + ":" + valueData + " ");
|
||||||
|
}
|
||||||
|
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||||
|
|
||||||
|
//3.模型数据
|
||||||
|
String metadata = airagExtData.getMetadata();
|
||||||
|
if(oConvertUtils.isNotEmpty(metadata)){
|
||||||
|
JSONObject modelParam = JSONObject.parseObject(metadata);
|
||||||
|
String modelId = modelParam.getString("modelId");
|
||||||
|
AssertUtils.assertNotEmpty("评估器模型ID不能为空", modelId);
|
||||||
|
|
||||||
|
// 默认大模型参数
|
||||||
|
AIChatParams params = new AIChatParams();
|
||||||
|
params.setTemperature(0.8);
|
||||||
|
params.setTopP(0.9);
|
||||||
|
params.setPresencePenalty(0.1);
|
||||||
|
params.setFrequencyPenalty(0.1);
|
||||||
|
|
||||||
|
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||||
|
if(modelParam.containsKey("temperature")){
|
||||||
|
params.setTemperature(modelParam.getDoubleValue("temperature"));
|
||||||
|
}
|
||||||
|
if(modelParam.containsKey("topP")){
|
||||||
|
params.setTopP(modelParam.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||||
|
}
|
||||||
|
if(modelParam.containsKey("presencePenalty")){
|
||||||
|
params.setPresencePenalty(modelParam.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||||
|
}
|
||||||
|
if(modelParam.containsKey("frequencyPenalty")){
|
||||||
|
params.setFrequencyPenalty(modelParam.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("调用评估器模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||||
|
//5.AI问答
|
||||||
|
String answerScore = aiChatHandler.completions(modelId, messages, params);
|
||||||
|
log.debug("评估器模型返回结果:{}", answerScore);
|
||||||
|
|
||||||
|
return answerScore;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取答案评分失败", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param columns
|
||||||
|
* @param variable
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String findDataType(JSONArray columns, JSONObject variable) {
|
||||||
|
// 获取目标字段值
|
||||||
|
String targetName = variable.getString("name");
|
||||||
|
|
||||||
|
// 使用 Stream API 查找并获取 dataType
|
||||||
|
return columns.stream()
|
||||||
|
.map(obj -> JSONObject.parseObject(obj.toString()))
|
||||||
|
.filter(column -> targetName.equals(column.getString("name")))
|
||||||
|
.findFirst()
|
||||||
|
.map(column -> column.getString("dataType"))
|
||||||
|
.orElse(null); // 如果没有找到,返回 null
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取图片地址
|
||||||
|
* @param request
|
||||||
|
* @param url
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String getFileAccessHttpUrl(HttpServletRequest request,String url){
|
||||||
|
if(oConvertUtils.isNotEmpty(url) && url.startsWith("http")){
|
||||||
|
return url;
|
||||||
|
}else{
|
||||||
|
return CommonUtils.getBaseUrl(request) + "/sys/common/static/" + url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.vo;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AiragDebugVo
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AiragDebugVo implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
/**
|
||||||
|
* 提示词
|
||||||
|
*/
|
||||||
|
private String prompts;
|
||||||
|
/**
|
||||||
|
* 输入内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
/**适配的大模型ID*/
|
||||||
|
private String modelId;
|
||||||
|
/**大模型的参数配置*/
|
||||||
|
private String modelParam;
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.jeecg.modules.airag.prompts.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: AiragExperimentVo
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-12-12
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AiragExperimentVo implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
/**
|
||||||
|
* 提示词
|
||||||
|
*/
|
||||||
|
private String promptKey;
|
||||||
|
/**
|
||||||
|
* 输入内容
|
||||||
|
*/
|
||||||
|
private String extDataId;
|
||||||
|
/**
|
||||||
|
* 映射关系
|
||||||
|
*/
|
||||||
|
private Map<String,String> mappings;
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.consts;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author chenrui
|
||||||
|
* @ClassName: TitleLevelEnum
|
||||||
|
* @Description: 标题级别
|
||||||
|
* @date 2024年5月4日07:38:30
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum WordTitleEnum {
|
||||||
|
|
||||||
|
FIRST("first", "标题1"),
|
||||||
|
SECOND("second", "标题2"),
|
||||||
|
THIRD("third", "标题3"),
|
||||||
|
FOURTH("fourth", "标题4"),
|
||||||
|
FIFTH("fifth", "标题5"),
|
||||||
|
SIXTH("sixth", "标题6");
|
||||||
|
|
||||||
|
WordTitleEnum(String code, String name) {
|
||||||
|
this.code = code;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,244 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: word模版管理
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-07-04
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Tag(name = "word模版管理")
|
||||||
|
@RestController("eoaWordTemplateController")
|
||||||
|
@RequestMapping("/airag/word")
|
||||||
|
@Slf4j
|
||||||
|
public class EoaWordTemplateController extends JeecgController<EoaWordTemplate, IEoaWordTemplateService> {
|
||||||
|
@Autowired
|
||||||
|
private IEoaWordTemplateService eoaWordTemplateService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WordTplUtils wordTplUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页列表查询
|
||||||
|
*
|
||||||
|
* @param eoaWordTemplate
|
||||||
|
* @param pageNo
|
||||||
|
* @param pageSize
|
||||||
|
* @param req
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Operation(summary = "word模版管理-分页列表查询")
|
||||||
|
@GetMapping(value = "/list")
|
||||||
|
public Result<IPage<EoaWordTemplate>> queryPageList(EoaWordTemplate eoaWordTemplate,
|
||||||
|
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||||
|
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||||
|
HttpServletRequest req) {
|
||||||
|
QueryWrapper<EoaWordTemplate> queryWrapper = QueryGenerator.initQueryWrapper(eoaWordTemplate, req.getParameterMap());
|
||||||
|
Page<EoaWordTemplate> page = new Page<EoaWordTemplate>(pageNo, pageSize);
|
||||||
|
IPage<EoaWordTemplate> pageList = eoaWordTemplateService.page(page, queryWrapper);
|
||||||
|
return Result.OK(pageList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加
|
||||||
|
*
|
||||||
|
* @param eoaWordTemplate
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "word模版管理-添加")
|
||||||
|
@Operation(summary = "word模版管理-添加")
|
||||||
|
// @RequiresPermissions("wordtpl:template:add")
|
||||||
|
@PostMapping(value = "/add")
|
||||||
|
public Result<String> add(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||||
|
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||||
|
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||||
|
boolean isCodeExists = eoaWordTemplateService.exists(Wrappers.lambdaQuery(EoaWordTemplate.class).eq(EoaWordTemplate::getCode, eoaWordTemplate.getCode()));
|
||||||
|
AssertUtils.assertFalse("模版编码已存在", isCodeExists);
|
||||||
|
eoaWordTemplateService.save(eoaWordTemplate);
|
||||||
|
return Result.OK("添加成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑
|
||||||
|
*
|
||||||
|
* @param eoaWordTemplate
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "word模版管理-编辑")
|
||||||
|
@Operation(summary = "word模版管理-编辑")
|
||||||
|
// @RequiresPermissions("wordtpl:template:edit")
|
||||||
|
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||||
|
public Result<String> edit(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||||
|
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||||
|
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||||
|
// 避免编辑时修改编码
|
||||||
|
eoaWordTemplate.setCode(null);
|
||||||
|
eoaWordTemplateService.updateById(eoaWordTemplate);
|
||||||
|
return Result.OK("编辑成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id删除
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "word模版管理-通过id删除")
|
||||||
|
@Operation(summary = "word模版管理-通过id删除")
|
||||||
|
// @RequiresPermissions("wordtpl:template:delete")
|
||||||
|
@DeleteMapping(value = "/delete")
|
||||||
|
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
eoaWordTemplateService.removeById(id);
|
||||||
|
return Result.OK("删除成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除
|
||||||
|
*
|
||||||
|
* @param ids
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@AutoLog(value = "word模版管理-批量删除")
|
||||||
|
@Operation(summary = "word模版管理-批量删除")
|
||||||
|
// @RequiresPermissions("wordtpl:template:deleteBatch")
|
||||||
|
@DeleteMapping(value = "/deleteBatch")
|
||||||
|
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||||
|
this.eoaWordTemplateService.removeByIds(Arrays.asList(ids.split(",")));
|
||||||
|
return Result.OK("批量删除成功!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过id查询
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
//@AutoLog(value = "word模版管理-通过id查询")
|
||||||
|
@Operation(summary = "word模版管理-通过id查询")
|
||||||
|
@GetMapping(value = "/queryById")
|
||||||
|
public Result<EoaWordTemplate> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||||
|
EoaWordTemplate eoaWordTemplate = eoaWordTemplateService.getById(id);
|
||||||
|
if (eoaWordTemplate == null) {
|
||||||
|
return Result.error("未找到对应数据");
|
||||||
|
}
|
||||||
|
return Result.OK(eoaWordTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载word模版
|
||||||
|
* @param id
|
||||||
|
* @param response
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/7/9 14:38
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/download")
|
||||||
|
public void downloadTemplate(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
|
||||||
|
AssertUtils.assertNotEmpty("请先选择模版", id);
|
||||||
|
EoaWordTemplate template = eoaWordTemplateService.getById(id);
|
||||||
|
try (ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||||
|
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||||
|
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||||
|
String fileName = template.getName();
|
||||||
|
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
|
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||||
|
response.addHeader("filename", encodedFileName + ".docx");
|
||||||
|
byte[] bytes = wordTemplateOut.toByteArray();
|
||||||
|
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||||
|
bos.write(bytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
throw new JeecgBootException("下载word模版失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析word模版文件
|
||||||
|
* @param file
|
||||||
|
* @param id
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/7/9 14:38
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/parse/file")
|
||||||
|
public Result<?> parseWOrdFile(@RequestParam("file") MultipartFile file) {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = file.getInputStream();
|
||||||
|
EoaWordTemplate eoaWordTemplate = wordTplUtils.parseWordFile(inputStream);
|
||||||
|
log.info("解析的模版信息: {}", eoaWordTemplate);
|
||||||
|
return Result.OK("解析成功", eoaWordTemplate);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("解析word模版失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成word文档
|
||||||
|
*
|
||||||
|
* @param wordTplGenDTO
|
||||||
|
* @param response
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/7/10 15:39
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "/generate/word")
|
||||||
|
public void generateWord(@RequestBody WordTplGenDTO wordTplGenDTO, HttpServletResponse response) {
|
||||||
|
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||||
|
EoaWordTemplate template ;
|
||||||
|
if (oConvertUtils.isNotEmpty(wordTplGenDTO.getTemplateId())) {
|
||||||
|
template = eoaWordTemplateService.getById(wordTplGenDTO.getTemplateId());
|
||||||
|
}else{
|
||||||
|
AssertUtils.assertNotEmpty("请先选择模版", wordTplGenDTO.getTemplateCode());
|
||||||
|
template = eoaWordTemplateService.getOne(Wrappers.lambdaQuery(EoaWordTemplate.class)
|
||||||
|
.eq(EoaWordTemplate::getCode, wordTplGenDTO.getTemplateCode()));
|
||||||
|
}
|
||||||
|
AssertUtils.assertNotEmpty("未找到对应的模版", template);
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||||
|
eoaWordTemplateService.generateWordFromTpl(wordTplGenDTO, outputStream);
|
||||||
|
String fileName = template.getName();
|
||||||
|
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
|
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||||
|
response.addHeader("filename", encodedFileName + ".docx");
|
||||||
|
byte[] bytes = outputStream.toByteArray();
|
||||||
|
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||||
|
bos.write(bytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
throw new JeecgBootException("生成word文档失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并列DTO
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/7/4 18:36
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MergeColDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并列的行号
|
||||||
|
*/
|
||||||
|
private int row;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并列的起始列号
|
||||||
|
*/
|
||||||
|
private int from;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并列的结束列号
|
||||||
|
*/
|
||||||
|
private int to;
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ClassName: DocImageDto
|
||||||
|
* @Description: word文档图片用实体类
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-10-02 09:17:59
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WordImageDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields type : 类型
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-29 08:53:27
|
||||||
|
*/
|
||||||
|
private String type = "image";
|
||||||
|
/**
|
||||||
|
* @Fields value : 内容
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:20:12
|
||||||
|
*/
|
||||||
|
private String value = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields width : 图片宽度
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-10-02 09:22:33
|
||||||
|
*/
|
||||||
|
private double width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields height : 图片高度
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-10-02 09:22:40
|
||||||
|
*/
|
||||||
|
private double height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields rowFlex : 水平对齐方式,默认left
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-27 09:12:18
|
||||||
|
*/
|
||||||
|
private String rowFlex = "left";
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.dto;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WordTableCellDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields colspan : 合并列数
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-26 09:37:27
|
||||||
|
*/
|
||||||
|
private int colspan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields rowspan : 合并行数
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-26 09:38:22
|
||||||
|
*/
|
||||||
|
private int rowspan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields value : 单元格数据
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-26 09:42:14
|
||||||
|
*/
|
||||||
|
private List<Object> value = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields verticalAlign : 垂直对齐方式,默认top
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-27 09:16:56
|
||||||
|
*/
|
||||||
|
private String verticalAlign = "top";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields backgroundColor : 背景颜色
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-11-18 09:56:28
|
||||||
|
*/
|
||||||
|
private String backgroundColor;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.dto;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WordTableDTO {
|
||||||
|
|
||||||
|
private String value = "";
|
||||||
|
|
||||||
|
private String type = "table";
|
||||||
|
|
||||||
|
private List<WordTableRowDTO> trList;
|
||||||
|
|
||||||
|
private int width;
|
||||||
|
|
||||||
|
private int height;
|
||||||
|
|
||||||
|
private List<JSONObject> colgroup = new ArrayList<>();
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WordTableRowDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields height : 行高
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-26 09:45:30
|
||||||
|
*/
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields minHeight : 行最小高度
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-26 09:47:28
|
||||||
|
*/
|
||||||
|
private int minHeight = 42;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields tdList : 行数据
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-26 09:46:02
|
||||||
|
*/
|
||||||
|
private List<WordTableCellDTO> tdList;
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.dto;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author chenrui
|
||||||
|
* @ClassName: DocTextDto
|
||||||
|
* @Description: word文本实体类
|
||||||
|
* @date 2024-09-24 10:19:57
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WordTextDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields type : 类型
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-29 08:53:27
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
/**
|
||||||
|
* @Fields value : 内容
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:20:12
|
||||||
|
*/
|
||||||
|
private String value = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields bold : 是否加粗 默认false
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:20:33
|
||||||
|
*/
|
||||||
|
private boolean bold = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields color : 字体颜色
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:21:08
|
||||||
|
*/
|
||||||
|
private String color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields italic : 是否斜体 默认false
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:21:25
|
||||||
|
*/
|
||||||
|
private boolean italic = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields underline : 是否下划线 默认false
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:21:47
|
||||||
|
*/
|
||||||
|
private boolean underline = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields strikeout : 删除线 默认false
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:22:06
|
||||||
|
*/
|
||||||
|
private boolean strikeout = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields size : 字号大小
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:44:42
|
||||||
|
*/
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields font : 字体,默认微软雅黑
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-24 10:45:31
|
||||||
|
*/
|
||||||
|
private String font = "微软雅黑";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields highlight : 高亮颜色
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-25 11:20:23
|
||||||
|
*/
|
||||||
|
private String highlight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Fields rowFlex : 水平对齐方式,默认left
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2024-09-27 09:12:18
|
||||||
|
*/
|
||||||
|
private String rowFlex = "left";
|
||||||
|
|
||||||
|
private List<Object> dashArray = new ArrayList<>();
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* word模版生成入参
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/7/10 14:38
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WordTplGenDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模版id
|
||||||
|
*/
|
||||||
|
String templateId;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模版code
|
||||||
|
*/
|
||||||
|
String templateCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据
|
||||||
|
*/
|
||||||
|
Map<String,Object> data;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: word模版管理
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-07-04
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("aigc_word_template")
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(description = "word模版管理")
|
||||||
|
public class EoaWordTemplate implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@Schema(description = "主键")
|
||||||
|
private String id;
|
||||||
|
/**
|
||||||
|
* 创建人
|
||||||
|
*/
|
||||||
|
@Schema(description = "创建人")
|
||||||
|
private String createBy;
|
||||||
|
/**
|
||||||
|
* 创建日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "创建日期")
|
||||||
|
private Date createTime;
|
||||||
|
/**
|
||||||
|
* 更新人
|
||||||
|
*/
|
||||||
|
@Schema(description = "更新人")
|
||||||
|
private String updateBy;
|
||||||
|
/**
|
||||||
|
* 更新日期
|
||||||
|
*/
|
||||||
|
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@Schema(description = "更新日期")
|
||||||
|
private Date updateTime;
|
||||||
|
/**
|
||||||
|
* 所属部门
|
||||||
|
*/
|
||||||
|
@Schema(description = "所属部门")
|
||||||
|
private String sysOrgCode;
|
||||||
|
/**
|
||||||
|
* 模版名称
|
||||||
|
*/
|
||||||
|
@Excel(name = "模版名称", width = 15)
|
||||||
|
@Schema(description = "模版名称")
|
||||||
|
private String name;
|
||||||
|
/**
|
||||||
|
* 模版编码
|
||||||
|
*/
|
||||||
|
@Excel(name = "模版编码", width = 15)
|
||||||
|
@Schema(description = "模版编码")
|
||||||
|
private String code;
|
||||||
|
/**
|
||||||
|
* 页眉
|
||||||
|
*/
|
||||||
|
@Excel(name = "页眉", width = 15)
|
||||||
|
@Schema(description = "页眉")
|
||||||
|
private String header;
|
||||||
|
/**
|
||||||
|
* 页脚
|
||||||
|
*/
|
||||||
|
@Excel(name = "页脚", width = 15)
|
||||||
|
@Schema(description = "页脚")
|
||||||
|
private String footer;
|
||||||
|
/**
|
||||||
|
* 主体内容
|
||||||
|
*/
|
||||||
|
@Excel(name = "主体内容", width = 15)
|
||||||
|
@Schema(description = "主体内容")
|
||||||
|
private String main;
|
||||||
|
/**
|
||||||
|
* 页边距
|
||||||
|
*/
|
||||||
|
@Excel(name = "页边距", width = 15)
|
||||||
|
@Schema(description = "页边距")
|
||||||
|
private String margins;
|
||||||
|
/**
|
||||||
|
* 宽度
|
||||||
|
*/
|
||||||
|
@Excel(name = "宽度", width = 15)
|
||||||
|
@Schema(description = "宽度")
|
||||||
|
private Integer width;
|
||||||
|
/**
|
||||||
|
* 高度
|
||||||
|
*/
|
||||||
|
@Excel(name = "高度", width = 15)
|
||||||
|
@Schema(description = "高度")
|
||||||
|
private Integer height;
|
||||||
|
/**
|
||||||
|
* 纸张方向 vertical纵向 horizontal横向
|
||||||
|
*/
|
||||||
|
@Excel(name = "纸张方向 vertical纵向 horizontal横向", width = 15)
|
||||||
|
@Schema(description = "纸张方向 vertical纵向 horizontal横向")
|
||||||
|
private String paperDirection;
|
||||||
|
/**
|
||||||
|
* 水印
|
||||||
|
*/
|
||||||
|
@Excel(name = "水印", width = 15)
|
||||||
|
@Schema(description = "水印")
|
||||||
|
private String watermark;
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: word模版管理
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-07-04
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface EoaWordTemplateMapper extends BaseMapper<EoaWordTemplate> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?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.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: word模版管理
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-07-04
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
public interface IEoaWordTemplateService extends IService<EoaWordTemplate> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过模版生成word文档
|
||||||
|
*
|
||||||
|
* @param wordTplGenDTO
|
||||||
|
* @return
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/7/10 14:40
|
||||||
|
*/
|
||||||
|
void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream);
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
package org.jeecg.modules.airag.wordtpl.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.deepoove.poi.XWPFTemplate;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.constant.DataBaseConstant;
|
||||||
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
|
import org.jeecg.common.system.util.JwtUtil;
|
||||||
|
import org.jeecg.common.util.AssertUtils;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||||
|
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description: word模版管理
|
||||||
|
* @Author: jeecg-boot
|
||||||
|
* @Date: 2025-07-04
|
||||||
|
* @Version: V1.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service("eoaWordTemplateService")
|
||||||
|
public class EoaWordTemplateServiceImpl extends ServiceImpl<EoaWordTemplateMapper, EoaWordTemplate> implements IEoaWordTemplateService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置的系统变量键列表
|
||||||
|
*/
|
||||||
|
private static final String[] SYSTEM_KEYS = {
|
||||||
|
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
|
||||||
|
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
|
||||||
|
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
|
||||||
|
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
|
||||||
|
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
|
||||||
|
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
|
||||||
|
DataBaseConstant.SYS_BASE_PATH
|
||||||
|
};
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WordTplUtils wordTplUtils;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream) {
|
||||||
|
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||||
|
AssertUtils.assertNotEmpty("模版ID不能为空", wordTplGenDTO.getTemplateId());
|
||||||
|
String templateId = wordTplGenDTO.getTemplateId();
|
||||||
|
// 生成word模版 date:2025/7/10
|
||||||
|
EoaWordTemplate template = getById(templateId);
|
||||||
|
ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||||
|
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||||
|
//根据word模版和数据生成word文件
|
||||||
|
Map<String, Object> data = wordTplGenDTO.getData();
|
||||||
|
mergeSystemVarsToData(data);
|
||||||
|
try {
|
||||||
|
XWPFTemplate.compile(new ByteArrayInputStream(wordTemplateOut.toByteArray())).render(data).write(wordOutputStream);
|
||||||
|
}catch (Exception e){
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
throw new JeecgBootException("生成word文档失败,请检查模版和数据是否正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将系统变量合并到数据中
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* @author chenrui
|
||||||
|
* @date 2025/7/3 17:43
|
||||||
|
*/
|
||||||
|
private static void mergeSystemVarsToData(Map<String, Object> data) {
|
||||||
|
for (String key : SYSTEM_KEYS) {
|
||||||
|
if (!data.containsKey(key)) {
|
||||||
|
String value = JwtUtil.getUserSystemData(key, null);
|
||||||
|
if (value != null) {
|
||||||
|
data.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>jeecg-boot-module</artifactId>
|
<artifactId>jeecg-boot-module</artifactId>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<version>3.9.0</version>
|
<version>3.9.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,326 @@
|
|||||||
|
package org.jeecg.modules.demo.mcp;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.config.shiro.IgnoreAuth;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Server 示例 (Model Context Protocol)
|
||||||
|
*
|
||||||
|
* 这是一个符合 MCP 协议的服务端实现,支持 SSE 传输。
|
||||||
|
*
|
||||||
|
* 连接地址: http://你的服务器:8080/jeecg-boot/demo/mcp/sse
|
||||||
|
*
|
||||||
|
* 提供的工具:
|
||||||
|
* - hello: 打招呼工具
|
||||||
|
* - get_time: 获取当前时间
|
||||||
|
* - calculate: 简单计算器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/demo/mcp")
|
||||||
|
@Tag(name = "MCP Server 示例")
|
||||||
|
public class McpDemoController {
|
||||||
|
|
||||||
|
// 存储 SSE 连接
|
||||||
|
private final Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 定义工具列表
|
||||||
|
private final List<Map<String, Object>> TOOLS = List.of(
|
||||||
|
Map.of(
|
||||||
|
"name", "hello",
|
||||||
|
"description", "打招呼工具,返回问候语",
|
||||||
|
"inputSchema", Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of(
|
||||||
|
"name", Map.of("type", "string", "description", "你的名字")
|
||||||
|
),
|
||||||
|
"required", List.of("name")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Map.of(
|
||||||
|
"name", "get_time",
|
||||||
|
"description", "获取当前服务器时间",
|
||||||
|
"inputSchema", Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Map.of(
|
||||||
|
"name", "calculate",
|
||||||
|
"description", "简单计算器,支持加减乘除",
|
||||||
|
"inputSchema", Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of(
|
||||||
|
"a", Map.of("type", "number", "description", "第一个数"),
|
||||||
|
"b", Map.of("type", "number", "description", "第二个数"),
|
||||||
|
"operator", Map.of("type", "string", "description", "运算符: +, -, *, /")
|
||||||
|
),
|
||||||
|
"required", List.of("a", "b", "operator")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP SSE 端点 - 客户端通过此接口建立 SSE 连接
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
@Operation(summary = "MCP SSE 连接端点")
|
||||||
|
public SseEmitter sse(HttpServletRequest request) {
|
||||||
|
String clientId = UUID.randomUUID().toString();
|
||||||
|
log.info("[MCP Server] 新客户端 SSE 连接: {}", clientId);
|
||||||
|
|
||||||
|
SseEmitter emitter = new SseEmitter(0L); // 不超时
|
||||||
|
sseEmitters.put(clientId, emitter);
|
||||||
|
|
||||||
|
emitter.onCompletion(() -> {
|
||||||
|
log.info("[MCP Server] 客户端断开: {}", clientId);
|
||||||
|
sseEmitters.remove(clientId);
|
||||||
|
});
|
||||||
|
emitter.onTimeout(() -> {
|
||||||
|
log.info("[MCP Server] 客户端超时: {}", clientId);
|
||||||
|
sseEmitters.remove(clientId);
|
||||||
|
});
|
||||||
|
emitter.onError(e -> {
|
||||||
|
log.error("[MCP Server] SSE 错误: {}", e.getMessage());
|
||||||
|
sseEmitters.remove(clientId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送 endpoint 事件,告诉客户端消息端点地址
|
||||||
|
try {
|
||||||
|
String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
|
||||||
|
String messageEndpoint = baseUrl + request.getContextPath() + "/demo/mcp/message?sessionId=" + clientId;
|
||||||
|
emitter.send(SseEmitter.event()
|
||||||
|
.name("endpoint")
|
||||||
|
.data(messageEndpoint));
|
||||||
|
log.info("[MCP Server] 发送 endpoint 事件: {}", messageEndpoint);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("[MCP Server] 发送 endpoint 事件失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streamable HTTP 端点 - 同时支持 POST 到 /sse 的 JSON-RPC 请求
|
||||||
|
* Cursor 客户端会先尝试这种方式
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@PostMapping(value = "/sse")
|
||||||
|
@Operation(summary = "MCP Streamable HTTP 端点")
|
||||||
|
public void ssePost(@RequestBody String body, HttpServletResponse response) throws IOException {
|
||||||
|
log.info("[MCP Server] Streamable HTTP 请求: {}", body);
|
||||||
|
handleJsonRpcRequest(body, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 消息处理端点 - 处理 JSON-RPC 请求
|
||||||
|
* 直接写入原始 JSON-RPC 响应,避免框架包装
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@PostMapping(value = "/message")
|
||||||
|
@Operation(summary = "MCP 消息处理")
|
||||||
|
public void handleMessage(@RequestParam(required = false) String sessionId,
|
||||||
|
@RequestBody String body,
|
||||||
|
HttpServletResponse response) throws IOException {
|
||||||
|
log.info("[MCP Server] 收到消息, sessionId: {}, body: {}", sessionId, body);
|
||||||
|
handleJsonRpcRequest(body, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 JSON-RPC 请求的公共方法
|
||||||
|
*/
|
||||||
|
private void handleJsonRpcRequest(String body, HttpServletResponse response) throws IOException {
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
PrintWriter writer = response.getWriter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject request = JSON.parseObject(body);
|
||||||
|
String method = request.getString("method");
|
||||||
|
Object id = request.get("id");
|
||||||
|
JSONObject params = request.getJSONObject("params");
|
||||||
|
|
||||||
|
// 通知类消息(没有id)不需要响应
|
||||||
|
if (id == null) {
|
||||||
|
log.info("[MCP Server] 收到通知: {}", method);
|
||||||
|
writer.write("{}");
|
||||||
|
writer.flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 JSON-RPC 2.0 响应
|
||||||
|
Map<String, Object> jsonRpcResponse = new LinkedHashMap<>();
|
||||||
|
jsonRpcResponse.put("jsonrpc", "2.0");
|
||||||
|
jsonRpcResponse.put("id", id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object result = switch (method) {
|
||||||
|
case "initialize" -> handleInitialize(params);
|
||||||
|
case "initialized", "notifications/initialized" -> handleInitialized();
|
||||||
|
case "tools/list" -> handleToolsList();
|
||||||
|
case "tools/call" -> handleToolsCall(params);
|
||||||
|
case "ping" -> handlePing();
|
||||||
|
case "notifications/cancelled" -> handleCancelled(params);
|
||||||
|
default -> {
|
||||||
|
if (method != null && method.startsWith("notifications/")) {
|
||||||
|
log.info("[MCP Server] 忽略未知通知: {}", method);
|
||||||
|
yield Map.of();
|
||||||
|
}
|
||||||
|
throw new RuntimeException("未知方法: " + method);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
jsonRpcResponse.put("result", result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MCP Server] 处理请求失败", e);
|
||||||
|
jsonRpcResponse.put("error", Map.of(
|
||||||
|
"code", -32603,
|
||||||
|
"message", e.getMessage()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseJson = JSON.toJSONString(jsonRpcResponse);
|
||||||
|
log.info("[MCP Server] 返回: {}", responseJson);
|
||||||
|
writer.write(responseJson);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MCP Server] 解析请求失败", e);
|
||||||
|
writer.write("{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32700,\"message\":\"Parse error\"}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 initialize 请求
|
||||||
|
*/
|
||||||
|
private Map<String, Object> handleInitialize(JSONObject params) {
|
||||||
|
log.info("[MCP Server] 初始化请求: {}", params);
|
||||||
|
return Map.of(
|
||||||
|
"protocolVersion", "2024-11-05",
|
||||||
|
"capabilities", Map.of(
|
||||||
|
"tools", Map.of()
|
||||||
|
),
|
||||||
|
"serverInfo", Map.of(
|
||||||
|
"name", "jeecg-mcp-demo",
|
||||||
|
"version", "1.0.0"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 initialized 通知
|
||||||
|
*/
|
||||||
|
private Map<String, Object> handleInitialized() {
|
||||||
|
log.info("[MCP Server] 客户端已初始化完成");
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 ping 请求
|
||||||
|
*/
|
||||||
|
private Map<String, Object> handlePing() {
|
||||||
|
log.info("[MCP Server] Ping");
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 notifications/cancelled 通知
|
||||||
|
*/
|
||||||
|
private Map<String, Object> handleCancelled(JSONObject params) {
|
||||||
|
log.info("[MCP Server] 请求被取消: {}", params);
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 tools/list 请求
|
||||||
|
*/
|
||||||
|
private Map<String, Object> handleToolsList() {
|
||||||
|
log.info("[MCP Server] 获取工具列表");
|
||||||
|
return Map.of("tools", TOOLS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理 tools/call 请求
|
||||||
|
*/
|
||||||
|
private Map<String, Object> handleToolsCall(JSONObject params) {
|
||||||
|
String toolName = params.getString("name");
|
||||||
|
JSONObject arguments = params.getJSONObject("arguments");
|
||||||
|
if (arguments == null) {
|
||||||
|
arguments = new JSONObject();
|
||||||
|
}
|
||||||
|
log.info("[MCP Server] 调用工具: {}, 参数: {}", toolName, arguments);
|
||||||
|
|
||||||
|
String result = switch (toolName) {
|
||||||
|
case "hello" -> {
|
||||||
|
String name = arguments.getString("name");
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
name = "World";
|
||||||
|
}
|
||||||
|
yield "你好, " + name + "! 欢迎使用 JeecgBoot MCP 服务!";
|
||||||
|
}
|
||||||
|
case "get_time" -> {
|
||||||
|
yield "当前时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
}
|
||||||
|
case "calculate" -> {
|
||||||
|
double a = arguments.getDoubleValue("a");
|
||||||
|
double b = arguments.getDoubleValue("b");
|
||||||
|
String op = arguments.getString("operator");
|
||||||
|
if (op == null) op = "+";
|
||||||
|
double res = switch (op) {
|
||||||
|
case "+" -> a + b;
|
||||||
|
case "-" -> a - b;
|
||||||
|
case "*" -> a * b;
|
||||||
|
case "/" -> b != 0 ? a / b : Double.NaN;
|
||||||
|
default -> throw new RuntimeException("不支持的运算符: " + op);
|
||||||
|
};
|
||||||
|
yield String.format("%.2f %s %.2f = %.2f", a, op, b, res);
|
||||||
|
}
|
||||||
|
default -> throw new RuntimeException("未知工具: " + toolName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Map.of(
|
||||||
|
"content", List.of(Map.of(
|
||||||
|
"type", "text",
|
||||||
|
"text", result
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用说明页面
|
||||||
|
*/
|
||||||
|
@IgnoreAuth
|
||||||
|
@GetMapping("/info")
|
||||||
|
@Operation(summary = "MCP Server 使用说明")
|
||||||
|
public Map<String, Object> info(HttpServletRequest request) {
|
||||||
|
log.info("[MCP Server] Hello 接口被访问");
|
||||||
|
String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
|
||||||
|
return Map.of(
|
||||||
|
"success", true,
|
||||||
|
"message", "JeecgBoot MCP Server 示例",
|
||||||
|
"sseUrl", baseUrl + "/demo/mcp/sse",
|
||||||
|
"tools", List.of(
|
||||||
|
Map.of("name", "hello", "description", "打招呼工具", "params", "name: 你的名字"),
|
||||||
|
Map.of("name", "get_time", "description", "获取当前时间", "params", "无"),
|
||||||
|
Map.of("name", "calculate", "description", "简单计算器", "params", "a, b, operator(+,-,*,/)")
|
||||||
|
),
|
||||||
|
"usage", "在 Cursor/Claude 等 MCP 客户端中配置 SSE URL: " + baseUrl + "/demo/mcp/sse",
|
||||||
|
"example", "请调用 hello 工具,参数 name 填 \"测试用户\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -72,10 +72,10 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
|
|||||||
Page<JeecgDemo> page = new Page<JeecgDemo>(pageNo, pageSize);
|
Page<JeecgDemo> page = new Page<JeecgDemo>(pageNo, pageSize);
|
||||||
|
|
||||||
IPage<JeecgDemo> pageList = jeecgDemoService.page(page, queryWrapper);
|
IPage<JeecgDemo> pageList = jeecgDemoService.page(page, queryWrapper);
|
||||||
log.info("查询当前页:" + pageList.getCurrent());
|
log.debug("查询当前页:" + pageList.getCurrent());
|
||||||
log.info("查询当前页数量:" + pageList.getSize());
|
log.debug("查询当前页数量:" + pageList.getSize());
|
||||||
log.info("查询结果数量:" + pageList.getRecords().size());
|
log.debug("查询结果数量:" + pageList.getRecords().size());
|
||||||
log.info("数据总数:" + pageList.getTotal());
|
log.debug("数据总数:" + pageList.getTotal());
|
||||||
return Result.OK(pageList);
|
return Result.OK(pageList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>jeecg-boot-parent</artifactId>
|
<artifactId>jeecg-boot-parent</artifactId>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<version>3.9.0</version>
|
<version>3.9.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>jeecg-system-api</artifactId>
|
<artifactId>jeecg-system-api</artifactId>
|
||||||
<groupId>org.jeecgframework.boot3</groupId>
|
<groupId>org.jeecgframework.boot3</groupId>
|
||||||
<version>3.9.0</version>
|
<version>3.9.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user