Compare commits

...

81 Commits

Author SHA1 Message Date
1d4042a0c1 v3.9.1 项目介绍 2026-01-28 10:11:25 +08:00
0e96e7395f v3.9.1 更新AI功能清单和截图 2026-01-28 09:47:14 +08:00
42844669af v3.9.1 发布 2026-01-27 22:00:14 +08:00
01c3ada3da 更新v3.9.1 aiflow 2026-01-26 16:18:48 +08:00
7493b63a5f --author:scott--date:20261026--for:优化插件工具异常,报错不影响AI后续执行 2026-01-26 15:18:13 +08:00
a20cba0adf --author:scott--date:20261026--for:优化异常处理,返回具体错误信息 2026-01-26 15:17:42 +08:00
360f5d779a 文件目录扫描漏洞 2026-01-26 15:06:33 +08:00
1936f503df 打印日志 2026-01-26 11:27:10 +08:00
fc1d28581c 通过提示词,设置 chat2bi默认数据源为空 2026-01-26 11:27:03 +08:00
faf7ea55a6 更新v3.9.1库脚本 2026-01-23 18:37:14 +08:00
981af95b44 更新v3.9.1库脚本 2026-01-23 18:33:14 +08:00
ce7415b133 更新lock 2026-01-23 18:26:37 +08:00
cc06450008 word 生成所需要java节点 2026-01-23 18:26:24 +08:00
ca7540b9cb [issues/9301] 强制预构建clipboard,解决Vite6对CommonJS模块的严格检查 2026-01-23 18:26:00 +08:00
97c76675e7 更新DESFORM_NAME_MAX_LENGTH常量值为40 2026-01-22 14:25:56 +08:00
d335ba8612 【issues/9282】下拉搜索框设置为自定义数据字典时,生成代码后台报错 #9282 2026-01-22 10:25:49 +08:00
3cd987b2e9 SysDataSourceController的queryOptions建议添加权限检查 #9288 2026-01-22 10:04:24 +08:00
03f27067d4 v3.6.1 数据库脚本 2026-01-22 10:01:42 +08:00
e94cf00ec0 默认激活微服务 profile 2026-01-22 09:51:44 +08:00
c71e6a6d25 升级版本号 v3.9.1 2026-01-22 09:31:15 +08:00
030dc503eb 升级online依赖 2026-01-22 09:23:46 +08:00
918b59120e 前端升级online依赖和打印组件 2026-01-22 09:23:32 +08:00
e2402c75b0 v3.9.1 后台代码 2026-01-21 19:02:45 +08:00
3735ca1687 前端源码 v3.9.1 2026-01-21 19:00:29 +08:00
be466f0b03 前端源码 v3.9.1 2026-01-21 19:00:21 +08:00
4092eed2a2 升级版本号 v3.9.1 2026-01-21 19:00:06 +08:00
901f05ed21 前端源码 v3.9.1 2026-01-21 18:14:55 +08:00
41877a6e8b 【issues/9275】用户组件第二次点击取消时勾选值还是回显了 2026-01-20 12:02:54 +08:00
b7a3da89ca 同步钉钉部门报错 #9228 2026-01-09 14:39:15 +08:00
de4a8ce652 --author:scott--date:20260108--for:升级minidao版本至1.10.18 2026-01-08 16:20:28 +08:00
e533af285c 【issues/9217】当配置了pagination: true时,BasicTable组件自适应高度异常 2025-12-31 16:43:12 +08:00
23dc7b3f03 字段类型是int,生成的代码,字典未匹配上,默认加上转换 2025-12-26 10:54:38 +08:00
e57aef0708 使用useListPage的导出异常 #9209 2025-12-26 10:50:11 +08:00
42087c0bf8 国际化 2025-12-24 18:27:13 +08:00
606edcc82f 国际化 2025-12-24 18:06:13 +08:00
9082e986f1 国际化 2025-12-24 18:05:16 +08:00
40cd525bba Merge branch 'main' of https://github.com/jeecgboot/jeecg-boot 2025-12-17 18:38:32 +08:00
d6b6cf079e 支持更多AI模型 2025-12-17 18:38:04 +08:00
1b688e7cd2 添加JUnit平台启动器依赖用于测试 2025-12-16 14:09:54 +08:00
58915a6410 Merge pull request #8878 from WolfCat-ICE/patch-1
Update renderUtils.ts 修复字典渲染renderTag使用tag渲染没使用字典配置颜色的问题
2025-12-15 17:42:56 +08:00
b67096dc54 Merge pull request #9004 from SunJary/patch-1
fix#9002 解决字典注解查询出现异常之后,数据源不能恢复问题
2025-12-15 17:24:33 +08:00
67795493bd 支持加签注解 @SignatureCheck,针对获取租户信息接口加签
【严重安全漏洞】用户可加入任意租户 #9196
2025-12-15 17:07:22 +08:00
e1c8f00bf2 【严重安全漏洞】用户可加入任意租户 #9196
jeecgboot模式的租户未做申请加入租户和审批逻辑,所以这俩接口注释掉
2025-12-15 17:02:16 +08:00
17a81e89a5 “用于后端字典翻译”,同一枚举dictCode,keys传多个也只add第1个DictModel #9124 2025-12-15 15:04:17 +08:00
bcbf775756 列表选字段导出异常 #9173 2025-12-15 10:56:06 +08:00
462365890e 表单添加了按钮并设置排序,代码生成报错 #9190 2025-12-15 10:49:19 +08:00
b686f9fbd1 【严重安全漏洞】未授权用户可强制任意在线用户下线,存在DOS攻击风险 #9195-- 2025-12-15 09:25:12 +08:00
872f84d006 【issues/9169】切换页码时,pageChange事件加载了两次 2025-12-10 09:43:36 +08:00
26087172df 更新文档说明 2025-12-04 14:57:41 +08:00
281c3ff3c8 措辞优化 2025-12-04 13:55:53 +08:00
38d44c2487 修改一个参数,就实现默认的四个系统主题快速切换 2025-12-03 19:55:13 +08:00
8c88f8adf5 【issues/9098】tabs标签页关闭异常 2025-12-03 19:06:21 +08:00
526734c5a5 v3.9.0 其他数据库脚本,增加aiflow调试接口权限 2025-12-02 12:19:35 +08:00
44b48ad916 AI流程调试接口加权限,存在命令执行漏洞 #9144 2025-12-01 15:18:34 +08:00
1a3ae4f61c 解决AI流程两个严重问题
1、AI流程调试接口,存在命令执行漏洞 #9144
2、AI流程导入后缀改成 *.jeecgai
2025-12-01 15:13:18 +08:00
859c509f08 解决AI流程两个严重问题
1、AI流程调试接口,存在命令执行漏洞 #9144
2、AI流程导入后缀改成 *.jeecgai
2025-12-01 15:13:11 +08:00
0704f187af 遗漏方法提交 2025-12-01 13:52:23 +08:00
199d2b439e GUI代码生成,更新文档地址 2025-11-28 15:59:10 +08:00
5f898ed034 V3.9.0 微服务启动失败 缺少配置application-liteflow.yml 2025-11-28 15:06:06 +08:00
5a9cb05c86 V3.9.0 打包太大,默认不集成积木报表的nosql支持包 2025-11-28 11:47:44 +08:00
98936680d5 V3.9.0 打包太大,删除demo模块无用代码 2025-11-28 11:42:58 +08:00
fc043fd5f3 V3.9.0 打包太大,删除非必须依赖 2025-11-28 11:35:15 +08:00
54531002a7 v3.9.0 oracle、SqlServer、postgresql初始化脚本 2025-11-28 11:04:50 +08:00
728a95c00d 描述 2025-11-28 10:09:04 +08:00
13c9951c1f 优化描述 2025-11-28 10:05:36 +08:00
8dfaa3c3e1 优化 2025-11-28 10:02:23 +08:00
050c478dce 产品描述优化 2025-11-28 10:00:44 +08:00
2688e8b6e2 解决yarn安装,启动报错问题 2025-11-27 22:32:09 +08:00
a9f30f0ca5 解决yarn安装,启动报错问题 2025-11-27 22:25:57 +08:00
1bf4a0595a V3.9.0 修复部门老数据,类型不对 2025-11-27 18:50:13 +08:00
74cd57fd99 V3.9.0 Oracle11g 数据库 登录提示 无效的列类型: 1111 #9145 2025-11-27 18:44:35 +08:00
c9ac4c9945 v3.9.0 修复历史机构的部门类型 2025-11-27 18:42:27 +08:00
3b3371ee1a v3.9.0 更新数据库 2025-11-27 18:21:31 +08:00
1d3bde9fe7 MCP示例 2025-11-27 15:46:51 +08:00
90b50a51a7 简化MCP示例 2025-11-27 15:46:40 +08:00
668ac59a5c AI聊天创建用户等功能,移除工号和邮箱的唯一性要求 2025-11-27 15:36:57 +08:00
5f01bdd29b v3.9.0 增加MCP插件示例数据 2025-11-27 13:23:56 +08:00
82bfcc7b14 启动脚本,host检查失败,不影响启动 2025-11-27 11:59:04 +08:00
ef210a2242 前端依赖包有问题,导致build报错 2025-11-27 11:53:01 +08:00
adc191f03e fix#9002 解决字典注解查询出现异常之后,数据源不能恢复问题 2025-10-20 22:21:47 +08:00
f6f2ef6316 Update renderUtils.ts 修复字典渲染renderTag使用tag渲染没使用字典配置颜色的问题
在renderDict方法中增加颜色属性传递,支持标签颜色渲染
render.renderDict(text, 'bpm_status',true)
2025-09-19 18:07:21 +08:00
436 changed files with 50440 additions and 172155 deletions

View File

@ -3,9 +3,6 @@ AIGC应用平台介绍
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
JeecgBoot平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
@ -109,6 +106,10 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| claude | √ |
| vl模型 | √ |
| 千帆大模型 | √ |
| 通义千问 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |

View File

@ -1,126 +0,0 @@
JeecgBoot低代码平台(商业版介绍)
===============
项目介绍
-----------------------------------
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot是一款集成AI应用的基于BPM流程的低代码平台旨在帮助企业快速实现低代码开发和构建个性化AI应用支持MCP和插件实现聊天式业务操作如 “一句话创建用户”)!
前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro。强大的代码生成器让前后端代码一键生成无需写任何代码 引领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://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
### 商业版功能简述
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
```
│─更多商业功能
│ ├─流程设计器
│ ├─简流设计器(类钉钉版)
│ ├─门户设计NEW
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─零代码应用管理(无需编码,在线搭建应用系统)
│ ├─积木报表企业版含jimureport、jimubi
│ ├─AI流程设计器源码
│ ├─Online全模块功能和源码
│ ├─AI写文章CMS
│ ├─AI表单字段建议表单设计器
│ ├─OA办公协同组件
│ ├─在线聊天功能
│ ├─设计表单移动适配
│ ├─设计表单支持外部填报
│ ├─设计表单AI字段建议
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
│ └─。。。
```
##### 流程设计
![](https://oscimg.oschina.net/oscnet/up-981ce174e4fbb48c8a2ce4ccfd7372e2994.png)
![](https://oscimg.oschina.net/oscnet/up-1dc0d052149ec675f3e4fad632b82b48add.png)
![](https://oscimg.oschina.net/oscnet/up-de31bc2f9d9b8332c554b0954cc73d79593.png)
![输入图片说明](https://static.oschina.net/uploads/img/201907/05165142_yyQ7.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160917_9Ftz.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160633_u59G.png "在这里输入图片标题")
##### 表单设计器
![](https://oscimg.oschina.net/oscnet/up-5f8cb657615714b02190b355e59f60c5937.png)
![](https://oscimg.oschina.net/oscnet/up-d9659b2f324e33218476ec98c9b400e6508.png)
![](https://oscimg.oschina.net/oscnet/up-4868615395272d3206dbb960ade02dbc291.png)

View File

@ -1,4 +1,4 @@
[中文](./README.md) | English
![JEECG](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/logov3.png "JeecgBoot低代码开发平台")
@ -7,12 +7,12 @@
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-28)
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -301,6 +301,41 @@ AI APP: https://help.jeecg.com/aigc
##### PC
##### AI Model and Application Management
![](https://oscimg.oschina.net/oscnet/up-0b1779e923566ccebb2d5a9cc9220c78b4a.png)
![](https://oscimg.oschina.net/oscnet/up-c8956df1d37d66b2d40136afaeca677628b.png)
![](https://oscimg.oschina.net/oscnet/up-8c348eeafd89673ca8cd1a2705014e3ac04.png)
AI Workflow Orchestration
![](https://oscimg.oschina.net/oscnet/up-2343657de2c7ac8010bc471470d084075ae.png)
MCP and Tool Management
![](https://oscimg.oschina.net/oscnet/up-8119d5dbc72e534236a3d042e11534c52ad.png)
AI Knowledge Base (Supports various document formats, with excellent markdown compatibility)
![](https://oscimg.oschina.net/oscnet/up-e2e9c118982ea366ed7f2b9827d4bb46c5d.png)
AI Toolbox
![](https://oscimg.oschina.net/oscnet/up-bf2a808d22a11fd83e577ad74741d97884b.png)
AI Chat Assistant
![](https://oscimg.oschina.net/oscnet/up-2a51accc2ff0b647e0ee058a58d291fe811.png)
![](https://oscimg.oschina.net/oscnet/up-ea1069c2a92a3ab2963d88763016cb037c2.png)
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
![](https://oscimg.oschina.net/oscnet/up-000530d95df337b43089ac77e562494f454.png)
![输入图片说明](https://static.oschina.net/uploads/img/201904/14155402_AmlV.png "在这里输入图片标题")

157
README.md
View File

@ -1,14 +1,15 @@
中文 | [English](./README.en-US.md)
JeecgBoot AI低代码平台
===============
当前最新版本: 3.9.0发布日期2025-12-01
当前最新版本: 3.9.1发布日期2026-01-28
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](https://jeecg.com)
[![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot)
@ -19,15 +20,17 @@ JeecgBoot AI低代码平台
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot 是一款基于BPM流程和代码生成AI低代码平台助力企业快速实现低代码开发和构建AI应用支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”
JeecgBoot 是一款融合代码生成AI应用的低代码开发平台助力企业快速实现低代码开发和构建AI应用。平台支持MCP和插件扩展,提供聊天式业务操作(如“一句话创建用户”),大幅提升开发效率与用户便捷性。
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus强大代码生成器实现前后端一键生成无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
`傻瓜式报表:` JimuReport是一款自主研发的强大开源企业级Web报表工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供完善成熟的AI应用平台涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合
`傻瓜式大屏:` JimuBI一款自主研发的强大的大屏和仪表盘设计工具。专注数字孪生与数据可视化支持交互式大屏、仪表盘、门户和移动端实现“一次开发多端适配”。 大屏设计类Word风格支持多屏切换自由拖拽轻松打造炫酷动态界面
`成熟AI应用功能:` 提供一套完善AI应用平台: 涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表、MCP插件配置等功能。平台兼容主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
@ -82,10 +85,9 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
- AI编程实战视频 [JEECG低代码与Cursor+GitHub Copilot实现AI高效编程实战](https://www.bilibili.com/video/BV11XyaBVEoH)
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
@ -107,8 +109,11 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
为什么选择JeecgBoot?
-----------------------------------
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案
> 界内首款AI低代码开发平台同时具备AI应用平台和低代码平台通过AI驱动低代码开发
> 开源界"小普元"超越传统商业平台。引领低代码开发模式(OnlineCoding-> 代码生成器 -> 手工MERGE),低代码开发同时又支持灵活编码, 可以帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高开发效率,节省成本,同时又不失灵活性
- 1.提供了一套完善的AI应用管理系统模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。 其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力
- 2.采用最新主流前后分离框架Spring Boot3 + MyBatisPlus + Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 )等新技术方案。便于学习容易上手,代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
@ -154,7 +159,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
技术架构
技术架构
-----------------------------------
#### 前端
@ -225,38 +230,31 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
- 15、CAS 单点登录 √
- 16、路由限流 √
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
开源版与企业版区别?
-----------------------------------
- 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 产品功能蓝图
![功能蓝图](https://jeecgos.oss-cn-beijing.aliyuncs.com/upload/test/Jeecg-Boot-lantu202005_1590912449914.jpg "在这里输入图片标题")
#### 系统功能架构图
![](https://oscimg.oschina.net/oscnet/up-1569487b95a07dbc3599fb1349a2e3aaae1.png)
### 开源版功能清单
### 功能清单
```
├─AI应用平台
│ ├─AI模型管理
│ ├─AI应用管理
│ ├─AI知识库
│ ├─AI流程编排
│ ├─AI聊天助手(支持图片、文件)
│ ├─AI聊天助手支持嵌入第三方、支持移动端
│ ├─MCP插件管理
│ ├─提示词管理
│ ├─AI应用门户汇总各种AI应用场景
│ ├─支持各种常见模型ChatGPT和DeepSeek、ollama等
├─工具箱
│ ├─OCR识别
│ ├─AI 海报
│ ├─AI 写作
│ ├─AI 简历
├─AI辅助功能
│ ├─AI建表Online表单
│ ├─AI生成报表Online报表
│ ├─AI生成大屏
├─系统管理
│ ├─用户管理
│ ├─角色管理
@ -283,18 +281,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
│ ├─系统编码规则
│ ├─系统校验规则
│ ├─APP版本管理
├─AI应用平台
│ ├─AI知识库问答系统
│ ├─AI大模型管理
│ ├─AI流程编排
│ ├─AI流程设计器
│ ├─AI对话支持图片
│ ├─AI对话助手(智能问答)
│ ├─AI建表Online表单
│ ├─AI聊天窗口支持嵌入第三方
│ ├─AI聊天窗口支持移动端
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
│ ├─AI OCR示例
├─数据可视化
│ ├─报表设计器(支持打印设计)
│ ├─大屏设和仪表盘设计
@ -405,6 +391,47 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
### 系统效果
##### AI模型与应用管理
![](https://oscimg.oschina.net/oscnet/up-0b1779e923566ccebb2d5a9cc9220c78b4a.png)
![](https://oscimg.oschina.net/oscnet/up-c8956df1d37d66b2d40136afaeca677628b.png)
![](https://oscimg.oschina.net/oscnet/up-8c348eeafd89673ca8cd1a2705014e3ac04.png)
AI流程编排
![](https://oscimg.oschina.net/oscnet/up-2343657de2c7ac8010bc471470d084075ae.png)
MCP和工具管理
![](https://oscimg.oschina.net/oscnet/up-8119d5dbc72e534236a3d042e11534c52ad.png)
AI知识库支持各种文档格式尤其markdown适配很好
![](https://oscimg.oschina.net/oscnet/up-e2e9c118982ea366ed7f2b9827d4bb46c5d.png)
AI工具箱
![](https://oscimg.oschina.net/oscnet/up-bf2a808d22a11fd83e577ad74741d97884b.png)
AI聊天助手
![](https://oscimg.oschina.net/oscnet/up-2a51accc2ff0b647e0ee058a58d291fe811.png)
![](https://oscimg.oschina.net/oscnet/up-ea1069c2a92a3ab2963d88763016cb037c2.png)
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### PC端
![](https://oscimg.oschina.net/oscnet/up-000530d95df337b43089ac77e562494f454.png)
@ -424,21 +451,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
![](https://oscimg.oschina.net/oscnet/up-16c07e000278329b69b228ae3189814b8e9.png)
##### AI功能
AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI建表
![](https://oscimg.oschina.net/oscnet/up-381423599f219a67def45dfd9a99df8ef3f.png)
![](https://oscimg.oschina.net/oscnet/up-1508c2b0708c365605f68893044ee11f20d.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### 仪表盘设计器
@ -511,6 +523,21 @@ AI写文章
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
### Jeecg Boot 产品功能蓝图
![功能蓝图](https://jeecgos.oss-cn-beijing.aliyuncs.com/upload/test/Jeecg-Boot-lantu202005_1590912449914.jpg "在这里输入图片标题")
#### 系统功能架构图
![](https://oscimg.oschina.net/oscnet/up-1569487b95a07dbc3599fb1349a2e3aaae1.png)
## 捐赠

View File

@ -1,13 +1,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.9.0发布日期2025-12-01
当前最新版本: 3.9.1(发布日期: 2026-01-22
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.9.0</version>
<version>3.9.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
@ -305,7 +305,7 @@
<optional>true</optional>
</dependency>
<!-- mini文件存储服务 -->
<!-- minio文件存储服务 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>

View File

@ -33,4 +33,10 @@ public class AiragFlowDTO implements Serializable {
* 输入参数
*/
private Map<String, Object> inputParams;
/**
* 是否流式返回
*/
private boolean isStream;
}

View File

@ -47,4 +47,8 @@ public interface TenantConstant {
*/
String APP_ADMIN = "appAdmin";
/**
* 增加SignatureCheck注解POST请求的URL
*/
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
}

View File

@ -3,9 +3,7 @@ package org.jeecg.common.system.util;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.annotation.EnumDict;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
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.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Method;
import java.util.*;
@ -183,10 +182,10 @@ public class ResourceUtil {
for (DictModel dm : dictItemList) {
String value = dm.getValue();
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()));
map.put(code, list);
break;
//break;
}
}
}

View File

@ -56,7 +56,9 @@ public class CommonUtils {
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
String dbPath = null;
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 {
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
File file = new File(basePath + File.separator + bizPath + File.separator );
@ -366,7 +368,7 @@ public class CommonUtils {
}else{
baseDomainPath = scheme + "://" + serverName + ":" + serverPort + contextPath ;
}
log.debug("-----Common getBaseUrl----- : " + baseDomainPath);
log.info("-----Common getBaseUrl----- : " + baseDomainPath);
return baseDomainPath;
}

View File

@ -46,7 +46,7 @@ public class RestUtil {
public static String getBaseUrl() {
String basepath = getDomain() + getPath();
log.info(" RestUtil.getBaseUrl: " + basepath);
log.debug(" RestUtil.getBaseUrl: " + basepath);
return basepath;
}
@ -199,7 +199,7 @@ public class RestUtil {
* @return ResponseEntity<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)) {
throw new RuntimeException("url 不能为空");
}
@ -230,7 +230,7 @@ public class RestUtil {
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(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,
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)) {
throw new RuntimeException("url 不能为空");
@ -302,7 +302,7 @@ public class RestUtil {
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(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+")":""));
}
}

View File

@ -286,5 +286,38 @@ public class SsrfFileTypeFilter {
}
}
/**
* 校验文件路径安全性,防止路径遍历攻击
* @param filePath 文件路径
*/
public static void checkPathTraversal(String filePath) {
if (StringUtils.isBlank(filePath)) {
return;
}
// 1. 防止路径遍历:不允许 ..
if (filePath.contains("..")) {
throw new JeecgBootException("文件路径包含非法字符");
}
// 2. 防止URL编码绕过%2e = .
String fileLower = filePath.toLowerCase();
if (fileLower.contains("%2e")) {
throw new JeecgBootException("文件路径包含非法字符");
}
}
/**
* 批量校验文件路径安全性(逗号分隔的多个文件路径)
* @param files 逗号分隔的文件路径
*/
public static void checkPathTraversalBatch(String files) {
if (StringUtils.isBlank(files)) {
return;
}
for (String file : files.split(",")) {
if (StringUtils.isNotBlank(file)) {
checkPathTraversal(file.trim());
}
}
}
}

View File

@ -1231,5 +1231,26 @@ public class oConvertUtils {
.filter(name -> beanWrapper.getPropertyValue(name) == null)
.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;
}
}
}

View File

@ -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命令行功能开启sqlAI流程SQL节点开启
*/
private String allowSensitiveNodes = "";
}

View File

@ -12,7 +12,6 @@ import java.util.HashMap;
import java.util.Map;
/**
* @author eightmonth@qq.com
* 启动程序修改DruidWallConfig配置
* 允许SELECT语句的WHERE子句是一个永真条件
* @author eightmonth

View File

@ -136,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.9.0")
.version("3.9.1")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description("后台API接口")
.termsOfService("NO terms of service")

View File

@ -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;
}
}

View File

@ -177,6 +177,8 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//仪表盘(按钮通信)
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//性能监控——安全隐患泄露TOEKNdurid连接池也有
//filterChainDefinitionMap.put("/actuator/**", "anon");
@ -188,6 +190,8 @@ public class ShiroConfig {
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
@ -227,6 +231,11 @@ public class ShiroConfig {
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
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.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);

View File

@ -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签名校验失败";
}

View File

@ -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);
}
}
}

View File

@ -1,6 +1,7 @@
package org.jeecg.config.sign.interceptor;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.util.PathMatcherUtil;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.filter.RequestBodyReserveFilter;
@ -64,6 +65,8 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
//------------------------------------------------------------
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
registration.addUrlPatterns(signUrlsArray);
// 增加注解签名请求
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
return registration;
}

View File

@ -33,63 +33,104 @@ public class SignAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("Sign Interceptor request URI = " + request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
if(oConvertUtils.isEmpty(xTimestamp)){
Result<?> result = Result.error("Sign签名校验失败时间戳为空");
log.error("Sign 签名校验失败Header xTimestamp 为空");
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
out.print(JSON.toJSON(result));
return false;
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("Sign 签名通过Header Sign : {}",headerSign);
try {
// 调用验证逻辑
validateSignature(request);
return true;
} else {
log.debug("sign allParams: {}", allParams);
log.error("request URI = " + request.getRequestURI());
log.error("Sign 签名校验失败Header Sign : {}",headerSign);
//校验失败返回前端
} 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("Sign签名校验失败");
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);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
log.debug("提取参数: {}", allParams);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
if(oConvertUtils.isEmpty(xTimestamp)){
log.error("Sign签名校验失败时间戳为空");
throw new IllegalArgumentException("Sign签名校验失败请求参数不完整");
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
long currentTimestamp = DateUtils.getCurrentTimestamp();
long timeDiff = currentTimestamp - clientTimestamp;
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
if (timeDiff > MAX_EXPIRE) {
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - clientTimestamp;
long maxExpireMs = MAX_EXPIRE * length1000;
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
if (timeDiff > maxExpireMs) {
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("签名验证通过");
} else {
log.error("签名验证失败, 参数: {}", allParams);
throw new IllegalArgumentException("Sign签名校验失败");
}
} catch (IllegalArgumentException e) {
// 重新抛出签名验证异常
throw e;
} catch (Exception e) {
// 包装其他异常如IOException
log.error("签名验证异常: {}", e.getMessage());
throw new IllegalArgumentException("Sign签名校验失败" + e.getMessage());
}
}
}

View File

@ -35,7 +35,7 @@ public class HttpUtils {
* @date 20210621
* @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<>();
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
@ -65,7 +65,13 @@ public class HttpUtils {
Map<String, String> allRequestParam = new HashMap<>(16);
// get请求不需要拿body参数
if (!HttpMethod.GET.name().equals(request.getMethod())) {
allRequestParam = getAllRequestParam(request);
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);
}
}
// 将URL的参数和body参数进行合并
if (allRequestParam != null) {

View File

@ -11,7 +11,12 @@ import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
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.TreeMap;
/**
* 签名工具类
@ -49,12 +54,7 @@ public class SignUtil {
String paramsJsonStr = JSONObject.toJSONString(params);
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
//设置签名秘钥
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} 缺少配置 ");
}
String signatureSecret = SignUtil.getSignatureSecret();
try {
//【issues/I484RW】2.4.6部署后下拉搜索框提示“sign签名检验失败”
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
@ -63,4 +63,129 @@ public class SignUtil {
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;
}
}

View File

@ -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()

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-module</artifactId>
<version>3.9.0</version>
<version>3.9.1</version>
</parent>
<artifactId>jeecg-boot-module-airag</artifactId>
@ -33,7 +33,7 @@
<properties>
<kotlin.version>2.2.0</kotlin.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>
<dependencyManagement>
@ -41,14 +41,14 @@
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.3.0</version>
<version>1.9.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>1.3.0-beta9</version>
<version>1.9.1-beta17</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -75,7 +75,7 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>3.9.0</version>
<version>3.9.1-beta1</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
@ -85,10 +85,14 @@
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
<exclusion>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- beigin 这两个依赖太多每个50M左右,如果你发布需要使用,请<scope>provided</scope>删掉 -->
<!-- begin 注意:这几个依赖体积较大,每个50MB。若发布需要使用,请<scope>provided</scope> 删除 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
@ -101,15 +105,15 @@
<version>${liteflow.version}</version>
<scope>provided</scope>
</dependency>
<!-- end 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<!-- end 注意这几个依赖体积较大每个约50MB。若发布时需要使用请将 <scope>provided</scope> 删除 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
@ -147,6 +151,11 @@
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<!-- langChain4j mcp support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
@ -193,7 +202,11 @@
<artifactId>langchain4j-pgvector</artifactId>
<version>1.3.0-beta9</version>
</dependency>
<!-- langChain4j Document Parser -->
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
@ -220,7 +233,12 @@
<artifactId>tika-parser-text-module</artifactId>
<version>${apache-tika.version}</version>
</dependency>
<!-- word模版引擎 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
</dependencies>
</project>

View File

@ -44,4 +44,19 @@ public class AiAppConsts {
*/
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";
}

View File

@ -104,4 +104,68 @@ public class Prompts {
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\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 = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
}

View File

@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* @Description: AI应用
@ -179,4 +178,16 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
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);
}
}

View File

@ -8,6 +8,7 @@ import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.config.shiro.IgnoreAuth;
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.ChatSendParams;
import org.springframework.beans.factory.annotation.Autowired;
@ -102,6 +103,19 @@ public class AiragChatController {
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
@DeleteMapping(value = "/conversation/{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
@GetMapping(value = "/messages")
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
return chatService.getMessages(conversationId);
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
@RequestParam(value = "sessionType", required = false) String sessionType) {
return chatService.getMessages(conversationId, sessionType);
}
/**
@ -153,7 +183,21 @@ public class AiragChatController {
@IgnoreAuth
@GetMapping(value = "/messages/clear/{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;
}
/**
* 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);
}
}

View File

@ -173,6 +173,29 @@ public class AiragApp implements Serializable {
@Schema(description = "插件")
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
*/

View File

@ -1,7 +1,6 @@
package org.jeecg.modules.airag.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.app.entity.AiragApp;
/**
@ -21,4 +20,14 @@ public interface IAiragAppService extends IService<AiragApp> {
* @date 2025/3/12 14:45
*/
Object generatePrompt(String prompt,boolean blocking);
/**
* 根据应用id生成提示词
*
* @param variables
* @param memoryId
* @param blocking
* @return
*/
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
}

View File

@ -1,6 +1,7 @@
package org.jeecg.modules.airag.app.service;
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.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
@ -59,21 +60,23 @@ public interface IAiragChatService {
* 获取对话聊天记录
*
* @param conversationId
* @param sessionType 类型
* @return
* @author chenrui
* @date 2025/2/26 15:16
*/
Result<?> getMessages(String conversationId);
Result<?> getMessages(String conversationId, String sessionType);
/**
* 删除会话
*
* @param conversationId
* @param sessionType
* @return
* @author chenrui
* @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 sessionType
* @return
* @author chenrui
* @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
*/
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);
}

View File

@ -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);
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import dev.langchain4j.data.message.AiMessage;
@ -10,12 +11,15 @@ import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.AssertUtils;
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.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
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.handler.AIChatParams;
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.EventFlowData;
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.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@ -31,6 +37,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
/**
* @Description: AI应用
@ -45,6 +52,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
@Autowired
IAIChatHandler aiChatHandler;
@Autowired
private IAiragKnowledgeService airagKnowledgeService;
@Override
public Object generatePrompt(String prompt, boolean blocking) {
AssertUtils.assertNotEmpty("请输入提示词", prompt);
@ -62,81 +72,167 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
}
return Result.OK("success", promptValue);
}else{
SseEmitter emitter = new SseEmitter(-0L);
// 异步运行(流式)
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
//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);
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
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);
// 异步运行(流式)
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onCompleteResponse((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onCompleteResponse((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try {
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
closeSSE(emitter, eventData);
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
return emitter;
}
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
return emitter;
}
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆将记忆提示词单独拆分---
private static void closeSSE(SseEmitter emitter, EventData eventData) {
try {

View File

@ -1,24 +1,36 @@
package org.jeecg.modules.airag.app.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
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.message.*;
import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest;
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.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil;
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.Prompts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
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.ChatConversation;
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.service.IAiragFlowService;
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.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
@ -85,6 +105,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Autowired
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())) {
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())) {
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流程------------
// 保存工作流入参配置(如果有)
@ -116,6 +151,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
}
//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);
}
@ -130,7 +171,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
AiragApp app = appDebugParams.getApp();
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流程------------
// 保存工作流入参配置(如果有)
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
@ -140,7 +183,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 发送消息
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;
}
@ -247,9 +292,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
@Override
public Result<?> getMessages(String conversationId) {
public Result<?> getMessages(String conversationId, String sessionType) {
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)) {
return Result.ok(Collections.emptyList());
}
@ -273,6 +320,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
.role(msg.getRole())
.content(msg.getContent())
.images(msg.getImages())
.files(msg.getFiles())
.datetime(msg.getDatetime())
.build();
// 不设置toolExecutionRequests和toolExecutionResult
@ -282,21 +330,30 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
result.put("messages", messages);
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);
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
}
@Override
public Result<?> clearMessage(String conversationId) {
public Result<?> clearMessage(String conversationId, String sessionType) {
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)) {
return Result.ok(Collections.emptyList());
}
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
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();
}
@ -443,9 +500,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
@Override
public Result<?> deleteConversation(String conversationId) {
public Result<?> deleteConversation(String conversationId, String sessionType) {
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)) {
Boolean delete = redisTemplate.delete(key);
if (delete) {
@ -463,14 +522,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
String key = getConversationCacheKey(updateTitleParams.getId(), null);
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType());
if (oConvertUtils.isEmpty(key)) {
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
return Result.ok();
}
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
chatConversation.setTitle(updateTitleParams.getTitle());
saveChatConversation(chatConversation);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (chatConversation != null) {
chatConversation.setTitle(updateTitleParams.getTitle());
}
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
return Result.ok();
}
@ -479,15 +542,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
*
* @param conversationId
* @param httpRequest
* @param sessionType 会话类型
* @return
* @author chenrui
* @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)) {
return null;
}
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;
return key;
}
@ -522,18 +591,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
*
* @param app
* @param conversationId
* @param sessionType
* @return
* @author chenrui
* @date 2025/2/25 19:19
*/
@NotNull
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId, String sessionType) {
if (oConvertUtils.isObjectEmpty(app)) {
app = new AiragApp();
app.setId(AiAppConsts.DEFAULT_APP_ID);
}
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)) {
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
}
@ -569,8 +641,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private void saveChatConversation(ChatConversation chatConversation) {
saveChatConversation(chatConversation, false, null);
private void saveChatConversation(ChatConversation chatConversation, String sessionType) {
saveChatConversation(chatConversation, false, null, sessionType);
}
/**
@ -581,11 +653,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @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) {
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)) {
return;
}
@ -680,6 +760,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @date 2025/2/25 19:05
*/
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)) {
// 系统消息,放到消息列表最前面,并且不记录历史
@ -709,8 +793,22 @@ public class AiragChatServiceImpl implements IAiragChatService {
textContent.append(((TextContent) content).text()).append("\n");
}
});
historyMessage.setContent(textContent.toString());
//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.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)) {
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
AiMessage aiMessage = (AiMessage) message;
@ -766,9 +864,20 @@ public class AiragChatServiceImpl implements IAiragChatService {
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
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应用:获取默认模型->开始聊天
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
@ -781,7 +890,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
} else {
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId());
}
} else {
// 发消息
@ -789,7 +898,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isObjectNotEmpty(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);
@ -804,6 +919,59 @@ public class AiragChatServiceImpl implements IAiragChatService {
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 "![](" + newUrl + ")";
}).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);
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{
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
@ -908,16 +1078,31 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @param chatConversation
* @param topicId
* @param sendParams
* @param flowId
* @param memoryId
* @return
* @author chenrui
* @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();
String modelId = aiApp.getModelId();
AssertUtils.assertNotEmpty("请先选择模型", modelId);
// AI应用提示词
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)) {
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
}
@ -943,6 +1128,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (metadata.containsKey("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));
}
}
}
@ -964,16 +1152,70 @@ public class AiragChatServiceImpl implements IAiragChatService {
aiChatParams.setPluginIds(pluginIds);
}
}
//流程不为空,构建插件
if(oConvertUtils.isNotEmpty(flowId)){
Map<String, Object> result = airagFlowPluginService.getFlowsToPlugin(flowId);
this.addPluginToParams(aiChatParams, result);
}
// 设置网络搜索参数(如果前端传递了)
if (sendParams != null && oConvertUtils.isObjectNotEmpty(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, "构造应用自定义参数完成");
// 发消息
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 modelId
* @param messages
* @param sessionType
* @return
* @author chenrui
* @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聊天
if (null == aiChatParams) {
aiChatParams = new AIChatParams();
@ -997,11 +1240,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
}
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
} else {
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
}
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
aiChatParams.setReturnThinking(true);
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
// for [QQYUN-9234] MCP服务连接关闭 - 保存参数引用用于在回调中关闭MCP连接
final AIChatParams finalAiChatParams = aiChatParams;
TokenStream chatStream;
try {
// 打印流程耗时日志
@ -1013,6 +1261,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
} catch (Exception e) {
log.error(e.getMessage(), e);
// for [QQYUN-9234] MCP服务连接关闭 - 异常时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
@ -1098,6 +1348,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息完成");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// for [QQYUN-9234] MCP服务连接关闭 - 聊天完成时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
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);
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);
} else if (FinishReason.LENGTH.equals(finishReason)) {
// 上下文长度超过限制
@ -1137,6 +1391,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息异常");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// for [QQYUN-9234] MCP服务连接关闭 - 聊天异常时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
@ -1201,7 +1457,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
*/
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
try {
log.info("发送消息:{}", eventData.getRequestId());
log.debug("发送消息:{}", eventData.getRequestId());
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
@ -1251,7 +1507,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isEmpty(chatConversation.getId())) {
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)) {
return;
}
@ -1281,10 +1539,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isNotEmpty(summaryTitle)) {
cachedConversation.setTitle(summaryTitle);
} 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);
}
}
}
/**
* 根据会话类型获取会话信息
*
* @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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -47,4 +47,14 @@ public class ChatConversation {
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
private Map<String, Object> flowInputs;
/**
* portal 应用门户
*/
private String sessionType;
/**
* 是否保存会话
*/
private Boolean izSaveSession;
}

View File

@ -47,6 +47,11 @@ public class ChatSendParams {
*/
private List<String> images;
/**
* 文件列表
*/
private List<String> files;
/**
* 工作流额外入参配置
* key: 参数field, value: 参数值
@ -59,4 +64,39 @@ public class ChatSendParams {
*/
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;
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.demo;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.springframework.stereotype.Component;
@ -11,12 +12,15 @@ import java.util.Map;
* @Author: chenrui
* @Date: 2025/3/6 11:42
*/
@Slf4j
@Component("testAiragEnhance")
public class TestAiragEnhance implements IAiRagEnhanceJava {
@Override
public Map<String, Object> process(Map<String, Object> inputParams) {
Object arg1 = inputParams.get("arg1");
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());
}
}

View File

@ -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";
}

View File

@ -1,5 +1,8 @@
package org.jeecg.modules.airag.llm.consts;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
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_IMAGE = "IMAGE";
/**
* 向量模型:默认维度
*/
@ -85,4 +93,29 @@ public class LLMConsts {
*/
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;
}

View File

@ -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);
}
}

View File

@ -1,14 +1,18 @@
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.metadata.IPage;
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 org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
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.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -80,6 +83,9 @@ public class AiragKnowledgeController {
@RequiresPermissions("airag:knowledge:add")
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
if(oConvertUtils.isEmpty(airagKnowledge.getType())) {
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
}
airagKnowledgeService.save(airagKnowledge);
return Result.OK("添加成功!");
}
@ -101,6 +107,9 @@ public class AiragKnowledgeController {
return Result.error("未找到对应数据");
}
String oldEmbedId = airagKnowledgeEntity.getEmbedId();
if(oConvertUtils.isEmpty(airagKnowledgeEntity.getType())) {
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
}
airagKnowledgeService.updateById(airagKnowledge);
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
// 更新了模型,重建文档
@ -357,5 +366,62 @@ public class AiragKnowledgeController {
List<AiragKnowledge> airagKnowledges = airagKnowledgeService.listByIds(idList);
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);
}
}

View File

@ -17,6 +17,7 @@ import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
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.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
@ -172,14 +173,19 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
try {
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);
}else{
}else if(LLMConsts.MODEL_TYPE_EMBED.equals(airagModel.getModelType())){
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
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){
log.error("测试模型连接失败", e);
return Result.error("测试模型连接失败,请检查模型配置是否正确!");
return Result.error(e.getMessage());
}
// 测试成功激活数据
airagModel.setActivateFlag(1);

View File

@ -7,6 +7,7 @@ package org.jeecg.modules.airag.llm.document;
import dev.langchain4j.data.document.BlankDocumentException;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser;
import dev.langchain4j.internal.Utils;
import org.apache.commons.io.FilenameUtils;
import org.apache.poi.hslf.usermodel.HSLFTextParagraph;
@ -30,7 +31,10 @@ import org.xml.sax.ContentHandler;
import java.io.*;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -51,6 +55,8 @@ public class TikaDocumentParser {
private final Supplier<ContentHandler> contentHandlerSupplier;
private final Supplier<Metadata> metadataSupplier;
private final Supplier<ParseContext> parseContextSupplier;
//文件前缀
private static final Set<String> FILE_SUFFIX = new HashSet<>(Arrays.asList("docx", "doc", "pptx", "ppt", "xlsx", "xls"));
public TikaDocumentParser() {
this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null);
@ -71,22 +77,16 @@ public class TikaDocumentParser {
InputStream isForParsing = Files.newInputStream(file.toPath());
// 使用 Tika 自动检测 MIME 类型
String fileName = file.getName().toLowerCase();
//后缀
String ext = FilenameUtils.getExtension(fileName);
if (fileName.endsWith(".txt")
|| fileName.endsWith(".md")
|| fileName.endsWith(".pdf")) {
return extractByTika(isForParsing);
} else if (fileName.endsWith(".docx")) {
return extractTextFromDocx(isForParsing);
} else if (fileName.endsWith(".doc")) {
return extractTextFromDoc(isForParsing);
} 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);
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
} else if (FILE_SUFFIX.contains(ext.toLowerCase())) {
return parseDocExcelPdfUsingApachePoi(file);
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
} else {
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 {
try {
// 先尝试 DOCX基于 OPC XML 格式)

View File

@ -102,4 +102,11 @@ public class AiragKnowledge implements Serializable {
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private java.lang.String status;
/**
* 类型(knowledge知识 memory 记忆)
*/
@Excel(name="类型(knowledge知识 memory 记忆)", width = 15)
@Schema(description = "类型(knowledge知识 memory 记忆)")
private java.lang.String type;
}

View File

@ -3,6 +3,8 @@ package org.jeecg.modules.airag.llm.handler;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.data.message.*;
import dev.langchain4j.exception.InvalidRequestException;
import dev.langchain4j.exception.ToolExecutionException;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.rag.query.router.QueryRouter;
import dev.langchain4j.service.TokenStream;
@ -11,14 +13,18 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.ai.handler.LLMHandler;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
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.IAIChatHandler;
import org.jeecg.modules.airag.common.handler.McpToolProviderWrapper;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
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.Value;
import org.springframework.stereotype.Component;
@ -54,6 +60,8 @@ public class AIChatHandler implements IAIChatHandler {
@Autowired
LLMHandler llmHandler;
@Autowired
AiRagConfigBean aiRagConfigBean;
@Value(value = "${jeecg.path.upload:}")
private String uploadpath;
@ -110,6 +118,9 @@ public class AIChatHandler implements IAIChatHandler {
String resp;
try {
resp = llmHandler.completions(messages, params);
} catch (ToolExecutionException | InvalidRequestException e) {
log.error(e.getMessage(), e);
return "";
} catch (Exception e) {
// langchain4j 异常友好提示
String errMsg = "调用大模型接口失败,详情请查看后台日志。";
@ -127,7 +138,7 @@ public class AIChatHandler implements IAIChatHandler {
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (errMsg.contains(key)) {
if (exceptionMsg.contains(key)) {
errMsg = value;
break;
}
@ -285,7 +296,7 @@ public class AIChatHandler implements IAIChatHandler {
// 默认超时时间
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
params.setTimeout(60);
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT);
}
//deepseek-reasoner 推理模型不支持插件tool
@ -301,6 +312,7 @@ public class AIChatHandler implements IAIChatHandler {
/**
* 构造插件和MCP工具
* for [QQYUN-12453]【AI】支持插件
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
* @param params
* @author chenrui
* @date 2025/10/31 14:04
@ -310,6 +322,7 @@ public class AIChatHandler implements IAIChatHandler {
if(oConvertUtils.isObjectNotEmpty(pluginIds)){
List<McpToolProvider> mcpToolProviders = new ArrayList<>();
List<McpToolProviderWrapper> mcpToolProviderWrappers = new ArrayList<>();
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
@ -325,15 +338,18 @@ public class AIChatHandler implements IAIChatHandler {
}
if ("mcp".equalsIgnoreCase(category)) {
// MCP类型构建McpToolProvider
McpToolProvider mcpToolProvider = buildMcpToolProvider(
// MCP类型构建McpToolProviderWrapper包含连接引用用于后续关闭
// for [QQYUN-9234] MCP服务连接关闭
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
airagMcp.getName(),
airagMcp.getType(),
airagMcp.getEndpoint(),
airagMcp.getHeaders()
airagMcp.getHeaders(),
aiRagConfigBean.getAllowSensitiveNodes()
);
if (mcpToolProvider != null) {
mcpToolProviders.add(mcpToolProvider);
if (wrapper != null) {
mcpToolProviders.add(wrapper.getMcpToolProvider());
mcpToolProviderWrappers.add(wrapper);
}
} else if ("plugin".equalsIgnoreCase(category)) {
// 插件类型构建ToolSpecification和ToolExecutor
@ -348,6 +364,12 @@ public class AIChatHandler implements IAIChatHandler {
if (!mcpToolProviders.isEmpty()) {
params.setMcpToolProviders(mcpToolProviders);
}
// 保存MCP连接包装器用于后续关闭
// for [QQYUN-9234] MCP服务连接关闭
if (!mcpToolProviderWrappers.isEmpty()) {
params.setMcpToolProviderWrappers(mcpToolProviderWrappers);
}
// 设置插件工具
if (!pluginTools.isEmpty()) {
@ -385,6 +407,7 @@ public class AIChatHandler implements IAIChatHandler {
String filePath = uploadpath + File.separator + imageUrl;
// 读取文件并转换为 base64 编码字符串
try {
SsrfFileTypeFilter.checkPathTraversal(filePath);
Path path = Paths.get(filePath);
byte[] fileContent = Files.readAllBytes(path);
String base64Data = Base64.getEncoder().encodeToString(fileContent);
@ -393,7 +416,7 @@ public class AIChatHandler implements IAIChatHandler {
// 构建 ImageContent 对象
imageContents.add(ImageContent.from(base64Data, mimeType));
} catch (IOException e) {
log.error("读取文件失败: " + filePath, e);
log.error("读取文件失败: {}", imageUrl, e);
throw new RuntimeException("发送消息失败,读取文件异常:" + e.getMessage(), e);
}
}
@ -401,5 +424,130 @@ public class AIChatHandler implements IAIChatHandler {
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 (e.getMessage().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;
SsrfFileTypeFilter.checkPathTraversal(filePath);
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 绘画创作 ========================================
}

View File

@ -17,13 +17,17 @@ import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
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 jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.tika.parser.AutoDetectParser;
import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*;
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
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_MAX_OUTPUT_CHARS = 4000;
/**
* 向量存储元数据:knowledgeId
*/
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
/**
* 向量存储元数据: 用户账号
*/
public static final String EMBED_STORE_METADATA_USER_NAME = "username";
/**
* 向量存储元数据: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_CREATE_TIME = "createTime";
/**
* 向量存储缓存
*/
@ -175,7 +194,26 @@ public class EmbeddingHandler implements IEmbeddingHandler {
.build();
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
.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) {
// ignoretoken获取不到默认为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);
ingestor.ingest(from);
return metadata.toMap();
@ -208,16 +246,47 @@ public class EmbeddingHandler implements IEmbeddingHandler {
}
}
//命中的文档内容
StringBuilder data = new StringBuilder();
// 对documents按score降序排序并取前topNumber个
List<Map<String, Object>> sortedDocuments = documents.stream()
.sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
.limit(topNumber)
.peek(doc -> data.append(doc.get("content")).append("\n"))
//update-begin---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
//是否为记忆库
boolean memoryMode = false;
//记忆库只有一个
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());
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
List<Map<String, Object>> limited = new ArrayList<>();
//将返回的结果按照最大的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());
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()
.queryEmbedding(queryEmbedding)
.maxResults(topNumber)
.minScore(similarity)
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.filter(filter)
.build();
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
@ -262,6 +351,9 @@ public class EmbeddingHandler implements IEmbeddingHandler {
Metadata metadata = matchRes.embedded().metadata();
data.put("chunk", metadata.getInteger("index"));
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;
}).collect(Collectors.toList());
}
@ -295,13 +387,32 @@ public class EmbeddingHandler implements IEmbeddingHandler {
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
topNumber = oConvertUtils.getInteger(topNumber, 5);
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()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(topNumber)
.minScore(similarity)
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.filter(filter)
.build();
retrievers.add(contentRetriever);
}

View File

@ -12,6 +12,8 @@ import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.TokenUtils;
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.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -51,8 +53,9 @@ public class PluginToolBuilder {
}
String baseUrl = airagMcp.getEndpoint();
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
// 如果baseUrl为空使用当前系统地址
if (oConvertUtils.isEmpty(baseUrl)) {
if (isEmptyBaseUrl) {
if (currentHttpRequest != null) {
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
log.info("插件[{}]的BaseURL为空使用系统地址: {}", airagMcp.getName(), baseUrl);
@ -64,7 +67,10 @@ public class PluginToolBuilder {
// 解析headers
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
// 判断是否需要加签
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
// 解析并应用授权配置从metadata中读取
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
@ -76,7 +82,7 @@ public class PluginToolBuilder {
try {
ToolSpecification spec = buildToolSpecification(toolConfig);
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign);
if (spec != null && executor != null) {
tools.put(spec, executor);
}
@ -187,7 +193,7 @@ public class PluginToolBuilder {
/**
* 构建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 method = toolConfig.getString("method");
JSONArray parameters = toolConfig.getJSONArray("parameters");
@ -215,17 +221,28 @@ public class PluginToolBuilder {
JSONObject urlVariables = buildUrlVariables(parameters, args);
Object body = buildRequestBody(parameters, args, httpHeaders);
// 发送HTTP请求
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
if (isNeedSign) {
// 发送请求前加签
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() : "";
} catch (HttpClientErrorException e) {
log.error("插件工具HTTP请求失败: {}", e.getMessage(), e);
return "请求失败: " + e.getStatusCode() + " - " + e.getResponseBodyAsString();
//update-begin---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
return "插件调用失败HTTP " + e.getStatusCode() + "" + e.getResponseBodyAsString()
+ "。请继续完成剩余任务。";
//update-end---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
} catch (Exception e) {
log.error("插件工具执行失败: {}", e.getMessage(), e);
return "工具执行失败: " + e.getMessage();
//update-begin---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
return "插件工具执行失败:" + e.getMessage()
+ "。请继续完成剩余任务。";
//update-end---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
}
};
}
@ -335,7 +352,13 @@ public class PluginToolBuilder {
if (isQueryParam || !isOtherType) {
Object value = args.get(paramName);
if (value != null) {
urlVariables.put(paramName, value);
//如果是知识库的id赋值默认值
if ("knowledgeId".equalsIgnoreCase(paramName)) {
String defaultValue = param.getString("defaultValue");
urlVariables.put(paramName, defaultValue);
} else {
urlVariables.put(paramName, value);
}
}
}
}
@ -392,7 +415,13 @@ public class PluginToolBuilder {
Object value = args.get(paramName);
if (value != null) {
body.put(paramName, value);
//如果是知识库的id赋值默认值
if ("knowledgeId".equalsIgnoreCase(paramName)) {
String defaultValue = param.getString("defaultValue");
body.put(paramName, defaultValue);
} else {
body.put(paramName, value);
}
} else {
// 检查是否有默认值
String defaultValue = param.getString("defaultValue");

View File

@ -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);
}

View File

@ -3,6 +3,8 @@ package org.jeecg.modules.airag.llm.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import java.util.Map;
/**
* AIRag知识库
*
@ -11,4 +13,12 @@ import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
* @Version: V1.0
*/
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
/**
* 构建知识库的工具
*
* @param memoryId
* @return Map<String, Object>
*/
Map<String, Object> getPluginMemory(String memoryId);
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -131,7 +131,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
AssertUtils.assertNotEmpty("文档不存在", docList);
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
// 检查状态
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
.filter(doc -> {
@ -330,6 +329,7 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
if (File.separator.equals("\\")) {
// Windows path handling
String escapedPath = uploadpath.replace("//", "\\\\");
escapedPath = escapedPath.replace("/", "\\\\");
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
} else {
// Unix path handling

View File

@ -1,18 +1,215 @@
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 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.AiragMcp;
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: AIRag知识库
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
@Slf4j
@Service
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;
}
}

View File

@ -10,11 +10,14 @@ import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
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 lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
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.mapper.AiragMcpMapper;
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 java.lang.reflect.Method;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
@ -31,12 +35,15 @@ import java.util.stream.Collectors;
* @Date: 2025-10-20
* @Version: V1.0
*/
@Service("airagMcpServiceImpl")
@Slf4j
@SuppressWarnings("removal")
@Service("airagMcpServiceImpl")
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
@Autowired
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
@Autowired
private AiRagConfigBean aiRagConfigBean;
/**
* 新增或编辑Mcpserver
@ -125,7 +132,7 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
Map<String, String> headers = null;
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
try {
headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
headers = JSONObject.parseObject(mcp.getHeaders(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {});
} catch (JSONException e) {
headers = null;
}
@ -136,19 +143,53 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
McpClient mcpClient = null;
try {
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)
.logRequests(true)
.logResponses(true);
if (headers != null && !headers.isEmpty()) {
builder.customHeaders(headers);
}
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
} else if ("stdio".equalsIgnoreCase(type)) {
// stdio 类型endpoint 可能是一个命令行,需要拆分为命令列表
// List<String> cmdParts = Arrays.asList(endpoint.trim().split("\\s+"));
// StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
// .command(cmdParts)
// .environment(headers);
// mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
return Result.error("不支持的MCP类型:" + type);
//update-begin---author:wangshuai---date:2025-12-18---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
String openSafe = aiRagConfigBean.getAllowSensitiveNodes();
if(oConvertUtils.isNotEmpty(openSafe) && openSafe.toLowerCase().contains("stdio")) {
log.info("[MCP]使用STDIO协议(StdioMcpTransport), endpoint:{}", endpoint);
// stdio 类型endpoint 可能是一个命令行
// Windows 下需要通过 cmd.exe /c 来执行命令,否则找不到 npx 等程序
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 {
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 字符串
private String safeWriteJson(Object obj) {
try {

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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.01.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;
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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;
}

View File

@ -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<>();
}

View File

@ -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;
}

View File

@ -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<>();
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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> {
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
}
}
}
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>jeecg-boot-module</artifactId>
<groupId>org.jeecgframework.boot3</groupId>
<version>3.9.0</version>
<version>3.9.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -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 填 \"测试用户\""
);
}
}

View File

@ -0,0 +1,333 @@
package org.jeecg.modules.demo.shop.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.demo.shop.entity.Order;
import org.jeecg.modules.demo.shop.entity.Product;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 商品管理模拟接口
* 用于AI Agent通过工具帮助用户查询商品并采购的业务演示
* @Author: chenrui
* @Date: 2025-11-06
*/
@Tag(name = "商品管理Demo")
@RestController
@RequestMapping("/demo/shop")
@Slf4j
public class ShopController {
/**
* 商品数据存储(内存)
*/
private final Map<String, Product> productStore = new ConcurrentHashMap<>();
/**
* 订单数据存储(内存)
*/
private final Map<String, Order> orderStore = new ConcurrentHashMap<>();
/**
* 订单ID生成器
*/
private final AtomicInteger orderIdGenerator = new AtomicInteger(1000);
/**
* 初始化商品数据
*
* @author chenrui
* @date 2025/11/6 14:30
*/
@PostConstruct
public void initProducts() {
// 电子产品
productStore.put("P001", new Product("P001", "iPhone 15 Pro", new BigDecimal("7999.00"), "电子产品", "Apple最新旗舰手机,6.1英寸屏幕,钛金属边框", 50));
productStore.put("P002", new Product("P002", "MacBook Pro 14", new BigDecimal("14999.00"), "电子产品", "M3 Pro芯片,16GB内存,512GB存储", 30));
productStore.put("P003", new Product("P003", "AirPods Pro 2", new BigDecimal("1899.00"), "电子产品", "主动降噪无线耳机,支持空间音频", 100));
productStore.put("P004", new Product("P004", "iPad Air", new BigDecimal("4799.00"), "电子产品", "10.9英寸液晶显示屏,M1芯片", 60));
// 图书
productStore.put("B001", new Product("B001", "Java核心技术卷I", new BigDecimal("119.00"), "图书", "Java编程经典教材,适合初学者和进阶开发者", 200));
productStore.put("B002", new Product("B002", "深入理解计算机系统", new BigDecimal("139.00"), "图书", "CSAPP经典教材,计算机系统必读书籍", 150));
productStore.put("B003", new Product("B003", "设计模式", new BigDecimal("89.00"), "图书", "软件设计经典著作,GoF四人组著作", 180));
// 生活用品
productStore.put("L001", new Product("L001", "小米电动牙刷", new BigDecimal("199.00"), "生活用品", "声波震动,IPX7防水,续航18天", 300));
productStore.put("L002", new Product("L002", "戴森吹风机", new BigDecimal("2990.00"), "生活用品", "快速干发,智能温控,保护头发", 80));
productStore.put("L003", new Product("L003", "膳魔师保温杯", new BigDecimal("259.00"), "生活用品", "真空保温,304不锈钢,保温12小时", 500));
// 食品
productStore.put("F001", new Product("F001", "三只松鼠坚果礼盒", new BigDecimal("159.00"), "食品", "混合坚果大礼包,1500g装", 400));
productStore.put("F002", new Product("F002", "茅台飞天53度", new BigDecimal("2899.00"), "食品", "贵州茅台酒,500ml", 20));
productStore.put("F003", new Product("F003", "星巴克咖啡豆", new BigDecimal("128.00"), "食品", "中度烘焙,派克市场,250g", 250));
log.info("商品数据初始化完成,共{}个商品", productStore.size());
}
/**
* 查询商品列表
*
* @param category 商品分类(可选)
* @param keyword 搜索关键词(可选)
* @return 商品列表
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "查询商品列表", description = "支持按分类和关键词搜索")
@GetMapping("/products")
public Result<List<Product>> getProducts(
@Parameter(description = "商品分类") @RequestParam(required = false) String category,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword) {
log.info("查询商品列表 - 分类: {}, 关键词: {}", category, keyword);
List<Product> products = new ArrayList<>(productStore.values());
// 按分类过滤
if (category != null && !category.trim().isEmpty()) {
products = products.stream()
.filter(p -> category.equals(p.getCategory()))
.collect(Collectors.toList());
}
// 按关键词过滤(搜索商品名称和描述)
if (keyword != null && !keyword.trim().isEmpty()) {
String searchKey = keyword.toLowerCase();
products = products.stream()
.filter(p -> p.getName().toLowerCase().contains(searchKey)
|| p.getDescription().toLowerCase().contains(searchKey))
.collect(Collectors.toList());
}
// 按价格排序
products.sort(Comparator.comparing(Product::getPrice));
log.info("查询到{}个商品", products.size());
return Result.OK(products);
}
/**
* 查询商品库存
*
* @param productId 商品ID
* @return 库存信息
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "查询商品库存", description = "根据商品ID查询库存数量")
@GetMapping("/stock")
public Result<Map<String, Object>> getStock(
@Parameter(description = "商品ID", required = true) @RequestParam String productId) {
log.info("查询商品库存 - 商品ID: {}", productId);
Product product = productStore.get(productId);
if (product == null) {
return Result.error("商品不存在: " + productId);
}
Map<String, Object> stockInfo = new HashMap<>();
stockInfo.put("productId", product.getId());
stockInfo.put("productName", product.getName());
stockInfo.put("stock", product.getStock());
stockInfo.put("available", product.getStock() > 0);
return Result.OK(stockInfo);
}
/**
* 购买商品(下单)
*
* @param productId 商品ID
* @param quantity 购买数量
* @param userId 用户ID(可选)
* @return 订单信息
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "购买商品", description = "创建订单,但不立即扣减库存")
@PostMapping("/purchase")
public Result<Order> purchase(
@Parameter(description = "商品ID", required = true) @RequestParam String productId,
@Parameter(description = "购买数量", required = true) @RequestParam Integer quantity,
@Parameter(description = "用户ID") @RequestParam(required = false) String userId) {
log.info("购买商品 - 商品ID: {}, 数量: {}, 用户: {}", productId, quantity, userId);
// 参数校验
if (quantity == null || quantity <= 0) {
return Result.error("购买数量必须大于0");
}
// 查询商品
Product product = productStore.get(productId);
if (product == null) {
return Result.error("商品不存在: " + productId);
}
// 检查库存
if (product.getStock() < quantity) {
return Result.error("库存不足,当前库存: " + product.getStock());
}
// 创建订单
String orderId = "O" + orderIdGenerator.incrementAndGet();
BigDecimal totalAmount = product.getPrice().multiply(new BigDecimal(quantity));
Order order = new Order();
order.setId(orderId);
order.setProductId(productId);
order.setProductName(product.getName());
order.setQuantity(quantity);
order.setUnitPrice(product.getPrice());
order.setTotalAmount(totalAmount);
order.setStatus("pending");
order.setCreateTime(new Date());
order.setUserId(userId);
orderStore.put(orderId, order);
log.info("订单创建成功 - 订单ID: {}, 总金额: {}", orderId, totalAmount);
return Result.OK(order);
}
/**
* 扣减商品库存
*
* @param orderId 订单ID
* @return 扣减结果
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "扣减商品库存", description = "根据订单ID扣减对应商品库存")
@PostMapping("/stock/deduct")
public Result<Map<String, Object>> deductStock(
@Parameter(description = "订单ID", required = true) @RequestParam String orderId) {
log.info("扣减库存 - 订单ID: {}", orderId);
// 查询订单
Order order = orderStore.get(orderId);
if (order == null) {
return Result.error("订单不存在: " + orderId);
}
// 检查订单状态
if ("paid".equals(order.getStatus())) {
return Result.error("订单已支付,库存已扣减");
}
if ("cancelled".equals(order.getStatus())) {
return Result.error("订单已取消");
}
// 查询商品
Product product = productStore.get(order.getProductId());
if (product == null) {
return Result.error("商品不存在: " + order.getProductId());
}
// 检查库存
synchronized (product) {
if (product.getStock() < order.getQuantity()) {
return Result.error("库存不足,当前库存: " + product.getStock() + ", 需要: " + order.getQuantity());
}
// 扣减库存
int newStock = product.getStock() - order.getQuantity();
product.setStock(newStock);
// 更新订单状态
order.setStatus("paid");
log.info("库存扣减成功 - 商品: {}, 扣减数量: {}, 剩余库存: {}",
product.getName(), order.getQuantity(), newStock);
}
Map<String, Object> result = new HashMap<>();
result.put("orderId", orderId);
result.put("productId", product.getId());
result.put("productName", product.getName());
result.put("deductedQuantity", order.getQuantity());
result.put("remainingStock", product.getStock());
result.put("orderStatus", order.getStatus());
return Result.OK(result);
}
/**
* 查询订单详情
*
* @param orderId 订单ID
* @return 订单详情
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "查询订单详情", description = "根据订单ID查询订单信息")
@GetMapping("/order")
public Result<Order> getOrder(
@Parameter(description = "订单ID", required = true) @RequestParam String orderId) {
log.info("查询订单 - 订单ID: {}", orderId);
Order order = orderStore.get(orderId);
if (order == null) {
return Result.error("订单不存在: " + orderId);
}
return Result.OK(order);
}
/**
* 获取所有商品分类
*
* @return 分类列表
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "获取商品分类", description = "获取所有商品的分类列表")
@GetMapping("/categories")
public Result<List<String>> getCategories() {
Set<String> categories = productStore.values().stream()
.map(Product::getCategory)
.collect(Collectors.toSet());
List<String> categoryList = new ArrayList<>(categories);
categoryList.sort(String::compareTo);
return Result.OK(categoryList);
}
/**
* 重置所有数据(仅用于测试)
*
* @return 重置结果
* @author chenrui
* @date 2025/11/6 14:30
*/
@Operation(summary = "重置数据", description = "清空所有订单并重置商品库存(仅用于测试)")
@PostMapping("/reset")
public Result<String> reset() {
log.info("重置商品和订单数据");
orderStore.clear();
orderIdGenerator.set(1000);
productStore.clear();
initProducts();
return Result.OK("数据重置成功");
}
}

View File

@ -0,0 +1,64 @@
package org.jeecg.modules.demo.shop.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* 订单实体
* @Author: chenrui
* @Date: 2025-11-06
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
/**
* 订单ID
*/
private String id;
/**
* 商品ID
*/
private String productId;
/**
* 商品名称
*/
private String productName;
/**
* 购买数量
*/
private Integer quantity;
/**
* 单价
*/
private BigDecimal unitPrice;
/**
* 总金额
*/
private BigDecimal totalAmount;
/**
* 订单状态: pending-待支付, paid-已支付, cancelled-已取消
*/
private String status;
/**
* 创建时间
*/
private Date createTime;
/**
* 用户信息(可选)
*/
private String userId;
}

View File

@ -0,0 +1,48 @@
package org.jeecg.modules.demo.shop.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 商品实体
* @Author: chenrui
* @Date: 2025-11-06
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
/**
* 商品ID
*/
private String id;
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private BigDecimal price;
/**
* 商品分类
*/
private String category;
/**
* 商品描述
*/
private String description;
/**
* 库存数量
*/
private Integer stock;
}

View File

@ -72,10 +72,10 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
Page<JeecgDemo> page = new Page<JeecgDemo>(pageNo, pageSize);
IPage<JeecgDemo> pageList = jeecgDemoService.page(page, queryWrapper);
log.info("查询当前页:" + pageList.getCurrent());
log.info("查询当前页数量:" + pageList.getSize());
log.info("查询结果数量:" + pageList.getRecords().size());
log.info("数据总数:" + pageList.getTotal());
log.debug("查询当前页:" + pageList.getCurrent());
log.debug("查询当前页数量:" + pageList.getSize());
log.debug("查询结果数量:" + pageList.getRecords().size());
log.debug("数据总数:" + pageList.getTotal());
return Result.OK(pageList);
}

View File

@ -1,438 +0,0 @@
package org.jeecg.modules.dlglong.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.MatchTypeEnum;
import org.jeecg.common.system.query.QueryCondition;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.constant.VxeSocketConst;
import org.jeecg.modules.demo.mock.vxe.websocket.VxeSocket;
import org.jeecg.modules.dlglong.entity.MockEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.util.*;
/**
* @Description: DlMockController
* @author: jeecg-boot
*/
@Slf4j
@RestController
@RequestMapping("/mock/dlglong")
public class DlMockController {
/**
* 模拟更改状态
*
* @param id
* @param status
* @return
*/
@GetMapping("/change1")
public Result mockChange1(@RequestParam("id") String id, @RequestParam("status") String status) {
/* id 为 行的idrowId只要获取到rowId那么只需要调用 VXESocket.sendMessageToAll() 即可 */
// 封装行数据
JSONObject rowData = new JSONObject();
// 这个字段就是要更改的行数据ID
rowData.put("id", id);
// 这个字段就是要更改的列的key和具体的值
rowData.put("status", status);
// 模拟更改数据
this.mockChange(rowData);
return Result.ok();
}
/**
* 模拟更改拖轮状态
*
* @param id
* @param tugStatus
* @return
*/
@GetMapping("/change2")
public Result mockChange2(@RequestParam("id") String id, @RequestParam("tug_status") String tugStatus) {
/* id 为 行的idrowId只要获取到rowId那么只需要调用 VXESocket.sendMessageToAll() 即可 */
// 封装行数据
JSONObject rowData = new JSONObject();
// 这个字段就是要更改的行数据ID
rowData.put("id", id);
// 这个字段就是要更改的列的key和具体的值
JSONObject status = JSON.parseObject(tugStatus);
rowData.put("tug_status", status);
// 模拟更改数据
this.mockChange(rowData);
return Result.ok();
}
/**
* 模拟更改进度条状态
*
* @param id
* @param progress
* @return
*/
@GetMapping("/change3")
public Result mockChange3(@RequestParam("id") String id, @RequestParam("progress") String progress) {
/* id 为 行的idrowId只要获取到rowId那么只需要调用 VXESocket.sendMessageToAll() 即可 */
// 封装行数据
JSONObject rowData = new JSONObject();
// 这个字段就是要更改的行数据ID
rowData.put("id", id);
// 这个字段就是要更改的列的key和具体的值
rowData.put("progress", progress);
// 模拟更改数据
this.mockChange(rowData);
return Result.ok();
}
private void mockChange(JSONObject rowData) {
// 封装socket数据
JSONObject socketData = new JSONObject();
// 这里的 socketKey 必须要和调度计划页面上写的 socketKey 属性保持一致
socketData.put("socketKey", "page-dispatch");
// 这里的 args 必须得是一个数组下标0是行数据下标1是caseId一般不用传
socketData.put("args", new Object[]{rowData, ""});
// 封装消息字符串,这里的 type 必须是 VXESocketConst.TYPE_UVT
String message = VxeSocket.packageMessage(VxeSocketConst.TYPE_UVT, socketData);
// 调用 sendMessageToAll 发送给所有在线的用户
VxeSocket.sendMessageToAll(message);
}
/**
* 模拟更改【大船待审】状态
*
* @param status
* @return
*/
@GetMapping("/change4")
public Result mockChange4(@RequestParam("status") String status) {
// 封装socket数据
JSONObject socketData = new JSONObject();
// 这里的 key 是前端注册时使用的key必须保持一致
socketData.put("key", "dispatch-dcds-status");
// 这里的 args 必须得是一个数组,每一位都是注册方法的参数,按顺序传递
socketData.put("args", new Object[]{status});
// 封装消息字符串,这里的 type 必须是 VXESocketConst.TYPE_UVT
String message = VxeSocket.packageMessage(VxeSocketConst.TYPE_CSD, socketData);
// 调用 sendMessageToAll 发送给所有在线的用户
VxeSocket.sendMessageToAll(message);
return Result.ok();
}
/**
* 【模拟】即时保存单行数据
*
* @param rowData 行数据,实际使用时可以替换成一个实体类
*/
@PutMapping("/immediateSaveRow")
public Result mockImmediateSaveRow(@RequestBody JSONObject rowData) throws Exception {
System.out.println("即时保存.rowData" + rowData.toJSONString());
// 延时1.5秒,模拟网慢堵塞真实感
Thread.sleep(500);
return Result.ok();
}
/**
* 【模拟】即时保存整个表格的数据
*
* @param tableData 表格数据实际使用时可以替换成一个List实体类
*/
@PostMapping("/immediateSaveAll")
public Result mockImmediateSaveAll(@RequestBody JSONArray tableData) throws Exception {
// 【注】:
// 1、tableData里包含该页所有的数据
// 2、如果你实现了“即时保存”那么除了新增的数据其他的都是已经保存过的了
// 不需要再进行一次update操作了所以可以在前端传数据的时候就遍历判断一下
// 只传新增的数据给后台insert即可否者将会造成性能上的浪费。
// 3、新增的行是没有id的通过这一点就可以判断是否是新增的数据
System.out.println("即时保存.tableData" + tableData.toJSONString());
// 延时1.5秒,模拟网慢堵塞真实感
Thread.sleep(1000);
return Result.ok();
}
/**
* 获取模拟数据
*
* @param pageNo 页码
* @param pageSize 页大小
* @param parentId 父ID不传则查询顶级
* @return
*/
@GetMapping("/getData")
public Result getMockData(
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
// 父级id根据父级id查询子级如果为空则查询顶级
@RequestParam(name = "parentId", required = false) String parentId
) {
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/dlglong.json";
// 读取JSON数据
JSONArray dataList = readJsonData(path);
if (dataList == null) {
return Result.error("读取数据失败!");
}
IPage<JSONObject> page = this.queryDataPage(dataList, parentId, pageNo, pageSize);
return Result.ok(page);
}
/**
* 获取模拟“调度计划”页面的数据
*
* @param pageNo 页码
* @param pageSize 页大小
* @param parentId 父ID不传则查询顶级
* @return
*/
@GetMapping("/getDdjhData")
public Result getMockDdjhData(
// SpringMVC 会自动将参数注入到实体里
MockEntity mockEntity,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
// 父级id根据父级id查询子级如果为空则查询顶级
@RequestParam(name = "parentId", required = false) String parentId,
@RequestParam(name = "status", required = false) String status,
// 高级查询条件
@RequestParam(name = "superQueryParams", required = false) String superQueryParams,
// 高级查询模式
@RequestParam(name = "superQueryMatchType", required = false) String superQueryMatchType,
HttpServletRequest request
) {
// 获取查询条件(前台传递的查询参数)
Map<String, String[]> parameterMap = request.getParameterMap();
// 遍历输出到控制台
System.out.println("\ngetDdjhData - 普通查询条件:");
for (String key : parameterMap.keySet()) {
System.out.println("-- " + key + ": " + JSON.toJSONString(parameterMap.get(key)));
}
// 输出高级查询
try {
System.out.println("\ngetDdjhData - 高级查询条件:");
// 高级查询模式
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
if (matchType == null) {
System.out.println("-- 高级查询模式:不识别(" + superQueryMatchType + "");
} else {
System.out.println("-- 高级查询模式:" + matchType.getValue());
}
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
if (conditions != null) {
for (QueryCondition condition : conditions) {
System.out.println("-- " + JSON.toJSONString(condition));
}
} else {
System.out.println("-- 没有传递任何高级查询条件");
}
System.out.println();
} catch (Exception e) {
log.error("-- 高级查询操作失败:" + superQueryParams, e);
e.printStackTrace();
}
/* 注:实际使用中不用写上面那种繁琐的代码,这里只是为了直观的输出到控制台里而写的示例,
使用下面这种写法更简洁方便 */
// 封装成 MyBatisPlus 能识别的 QueryWrapper可以直接使用这个对象进行SQL筛选条件拼接
// 这个方法也会自动封装高级查询条件但是高级查询参数名必须是superQueryParams和superQueryMatchType
QueryWrapper<MockEntity> queryWrapper = QueryGenerator.initQueryWrapper(mockEntity, parameterMap);
System.out.println("queryWrapper " + queryWrapper.getCustomSqlSegment());
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/ddjh.json";
String statusValue = "8";
if (statusValue.equals(status)) {
path = "classpath:org/jeecg/modules/dlglong/json/ddjh_s8.json";
}
// 读取JSON数据
JSONArray dataList = readJsonData(path);
if (dataList == null) {
return Result.error("读取数据失败!");
}
IPage<JSONObject> page = this.queryDataPage(dataList, parentId, pageNo, pageSize);
// 逐行查询子表数据,用于计算拖轮状态
List<JSONObject> records = page.getRecords();
for (JSONObject record : records) {
Map<String, Integer> tugStatusMap = new HashMap<>(5);
String id = record.getString("id");
// 查询出主表的拖轮
String tugMain = record.getString("tug");
// 判断是否有值
if (StringUtils.isNotBlank(tugMain)) {
// 拖轮根据分号分割
String[] tugs = tugMain.split(";");
// 查询子表数据
List<JSONObject> subRecords = this.queryDataPage(dataList, id, null, null).getRecords();
// 遍历子表和拖轮数据,找出进行计算反推拖轮状态
for (JSONObject subData : subRecords) {
String subTug = subData.getString("tug");
if (StringUtils.isNotBlank(subTug)) {
for (String tug : tugs) {
if (tug.equals(subTug)) {
// 计算拖轮状态逻辑
int statusCode = 0;
/* 如果有发船时间、作业开始时间、作业结束时间、回船时间,则主表中的拖轮列中的每个拖轮背景色要即时变色 */
// 有发船时间,状态 +1
String departureTime = subData.getString("departure_time");
if (StringUtils.isNotBlank(departureTime)) {
statusCode += 1;
}
// 有作业开始时间,状态 +1
String workBeginTime = subData.getString("work_begin_time");
if (StringUtils.isNotBlank(workBeginTime)) {
statusCode += 1;
}
// 有作业结束时间,状态 +1
String workEndTime = subData.getString("work_end_time");
if (StringUtils.isNotBlank(workEndTime)) {
statusCode += 1;
}
// 有回船时间,状态 +1
String returnTime = subData.getString("return_time");
if (StringUtils.isNotBlank(returnTime)) {
statusCode += 1;
}
// 保存拖轮状态key是拖轮的值value是状态前端根据不同的状态码显示不同的颜色这个颜色也可以后台计算完之后返回给前端直接使用
tugStatusMap.put(tug, statusCode);
break;
}
}
}
}
}
// 新加一个字段用于保存拖轮状态,不要直接覆盖原来的,这个字段可以不保存到数据库里
record.put("tug_status", tugStatusMap);
}
page.setRecords(records);
return Result.ok(page);
}
/**
* 模拟查询数据可以根据父ID查询可以分页
*
* @param dataList 数据列表
* @param parentId 父ID
* @param pageNo 页码
* @param pageSize 页大小
* @return
*/
private IPage<JSONObject> queryDataPage(JSONArray dataList, String parentId, Integer pageNo, Integer pageSize) {
// 根据父级id查询子级
JSONArray dataDb = dataList;
if (StringUtils.isNotBlank(parentId)) {
JSONArray results = new JSONArray();
List<String> parentIds = Arrays.asList(parentId.split(","));
this.queryByParentId(dataDb, parentIds, results);
dataDb = results;
}
// 模拟分页实际中应用SQL自带的分页
List<JSONObject> records = new ArrayList<>();
IPage<JSONObject> page;
long beginIndex, endIndex;
// 如果任意一个参数为null则不分页
if (pageNo == null || pageSize == null) {
page = new Page<>(0, dataDb.size());
beginIndex = 0;
endIndex = dataDb.size();
} else {
page = new Page<>(pageNo, pageSize);
beginIndex = page.offset();
endIndex = page.offset() + page.getSize();
}
for (long i = beginIndex; (i < endIndex && i < dataDb.size()); i++) {
JSONObject data = dataDb.getJSONObject((int) i);
data = JSON.parseObject(data.toJSONString());
// 不返回 children
data.remove("children");
records.add(data);
}
page.setRecords(records);
page.setTotal(dataDb.size());
return page;
}
private void queryByParentId(JSONArray dataList, List<String> parentIds, JSONArray results) {
for (int i = 0; i < dataList.size(); i++) {
JSONObject data = dataList.getJSONObject(i);
JSONArray children = data.getJSONArray("children");
// 找到了该父级
if (parentIds.contains(data.getString("id"))) {
if (children != null) {
// addAll 的目的是将多个子表的数据合并在一起
results.addAll(children);
}
} else {
if (children != null) {
queryByParentId(children, parentIds, results);
}
}
}
results.addAll(new JSONArray());
}
private JSONArray readJsonData(String path) {
try {
InputStream stream = getClass().getClassLoader().getResourceAsStream(path.replace("classpath:", ""));
if (stream != null) {
String json = IOUtils.toString(stream, "UTF-8");
return JSON.parseArray(json);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return null;
}
/**
* 获取车辆最后一个位置
*
* @return
*/
@PostMapping("/findLatestCarLngLat")
public List findLatestCarLngLat() {
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/CarLngLat.json";
// 读取JSON数据
return readJsonData(path);
}
/**
* 获取车辆最后一个位置
*
* @return
*/
@PostMapping("/findCarTrace")
public List findCarTrace() {
// 模拟JSON数据路径
String path = "classpath:org/jeecg/modules/dlglong/json/CarTrace.json";
// 读取JSON数据
return readJsonData(path);
}
}

View File

@ -1,27 +0,0 @@
package org.jeecg.modules.dlglong.entity;
import lombok.Data;
/**
* 模拟实体
* @author: jeecg-boot
*/
@Data
public class MockEntity {
/**
* id
*/
private String id;
/**
* 父级ID
*/
private String parentId;
/**
* 状态
*/
private String status;
/* -- 省略其他字段 -- */
}

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