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

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
JEECG BOOT AI Low Code Platform
|
||||
===============
|
||||
|
||||
Current version: 3.9.0 (Release date: 2025-12-01)
|
||||
Current version: 3.9.1 (Release date: 2026-01-28)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://www.jeecg.com)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
@ -301,6 +301,41 @@ AI APP: https://help.jeecg.com/aigc
|
||||
|
||||
|
||||
##### PC
|
||||
|
||||
##### AI Model and Application Management
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
AI Workflow Orchestration
|
||||
|
||||

|
||||
|
||||
MCP and Tool Management
|
||||
|
||||

|
||||
|
||||
AI Knowledge Base (Supports various document formats, with excellent markdown compatibility)
|
||||
|
||||

|
||||
|
||||
AI Toolbox
|
||||
|
||||

|
||||
|
||||
AI Chat Assistant
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
157
README.md
157
README.md
@ -1,14 +1,15 @@
|
||||
中文 | [English](./README.en-US.md)
|
||||
|
||||
JeecgBoot AI低代码平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||
当前最新版本: 3.9.1(发布日期:2026-01-28)
|
||||
|
||||
|
||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||
[](https://jeecg.com)
|
||||
[](https://jeecg.blog.csdn.net)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
|
||||
@ -19,15 +20,17 @@ JeecgBoot AI低代码平台
|
||||
|
||||
<h3 align="center">企业级AI低代码平台</h3>
|
||||
|
||||
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用,支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”)!
|
||||
JeecgBoot 是一款融合代码生成与AI应用的低代码开发平台,助力企业快速实现低代码开发和构建AI应用。平台支持MCP和插件扩展,提供聊天式业务操作(如“一句话创建用户”),大幅提升开发效率与用户便捷性。
|
||||
|
||||
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-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 Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
||||
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
||||
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
|
||||
@ -154,7 +159,7 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
||||
|
||||
|
||||
|
||||
技术架构:
|
||||
技术架构
|
||||
-----------------------------------
|
||||
|
||||
#### 前端
|
||||
@ -225,38 +230,31 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
||||
- 15、CAS 单点登录 √
|
||||
- 16、路由限流 √
|
||||
|
||||
#### 微服务架构图
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
开源版与企业版区别?
|
||||
-----------------------------------
|
||||
|
||||
- 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 产品功能蓝图
|
||||

|
||||
|
||||
|
||||
|
||||
#### 系统功能架构图
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 开源版功能清单
|
||||
### 功能清单
|
||||
```
|
||||
├─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模型与应用管理
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
AI流程编排
|
||||
|
||||

|
||||
|
||||
MCP和工具管理
|
||||
|
||||

|
||||
|
||||
AI知识库(支持各种文档格式,尤其markdown适配很好)
|
||||
|
||||

|
||||
|
||||
AI工具箱
|
||||
|
||||

|
||||
|
||||
AI聊天助手
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
AI写文章
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
##### PC端
|
||||

|
||||
|
||||
@ -424,21 +451,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
||||

|
||||
|
||||
|
||||
##### AI功能
|
||||
|
||||
AI聊天助手
|
||||
|
||||

|
||||
|
||||
AI建表
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
AI写文章
|
||||
|
||||

|
||||
|
||||
|
||||
##### 仪表盘设计器
|
||||
@ -511,6 +523,21 @@ AI写文章
|
||||
|
||||
|
||||
|
||||
#### 微服务架构图
|
||||

|
||||
|
||||
|
||||
|
||||
### Jeecg Boot 产品功能蓝图
|
||||

|
||||
|
||||
|
||||
|
||||
#### 系统功能架构图
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 捐赠
|
||||
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
|
||||
JeecgBoot 低代码开发平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.9.0(发布日期:2025-12-01)
|
||||
当前最新版本: 3.9.1(发布日期: 2026-01-22)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://jeecg.com/aboutusIndex)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
||||
@ -33,4 +33,10 @@ public class AiragFlowDTO implements Serializable {
|
||||
* 输入参数
|
||||
*/
|
||||
private Map<String, Object> inputParams;
|
||||
|
||||
/**
|
||||
* 是否流式返回
|
||||
*/
|
||||
private boolean isStream;
|
||||
|
||||
}
|
||||
|
||||
@ -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" };
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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+")":""));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* ai配置类,通用的配置可以放到这里面
|
||||
*
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025/12/17 14:00
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
|
||||
public class AiRagConfigBean {
|
||||
public static final String PREFIX = "jeecg.airag";
|
||||
|
||||
/**
|
||||
* 敏感节点
|
||||
* stdio mpc命令行功能开启,sql:AI流程SQL节点开启
|
||||
*/
|
||||
private String allowSensitiveNodes = "";
|
||||
}
|
||||
@ -12,7 +12,6 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author eightmonth@qq.com
|
||||
* 启动程序修改DruidWallConfig配置
|
||||
* 允许SELECT语句的WHERE子句是一个永真条件
|
||||
* @author eightmonth
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* 任务调度器配置
|
||||
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
|
||||
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
|
||||
*
|
||||
* @author jeecg
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class TaskSchedulerConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
|
||||
public ThreadPoolTaskScheduler taskScheduler() {
|
||||
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
|
||||
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(10);
|
||||
scheduler.setThreadNamePrefix("airag-scheduler-");
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
scheduler.setAwaitTerminationSeconds(60);
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
@ -177,6 +177,8 @@ public class ShiroConfig {
|
||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
||||
//仪表盘(按钮通信)
|
||||
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
|
||||
//App vue3版本查询版本接口
|
||||
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
|
||||
|
||||
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
||||
//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);
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
package org.jeecg.config.sign.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 签名校验注解
|
||||
* 用于方法级别的签名验证,功能等同于yml中的jeecg.signUrls配置
|
||||
* 参考DragSignatureAspect的设计思路,使用AOP切面实现
|
||||
*
|
||||
* @author GitHub Copilot
|
||||
* @since 2025-12-15
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SignatureCheck {
|
||||
|
||||
/**
|
||||
* 是否启用签名校验
|
||||
* @return true-启用(默认), false-禁用
|
||||
*/
|
||||
boolean enabled() default true;
|
||||
|
||||
/**
|
||||
* 签名校验失败时的错误消息
|
||||
* @return 错误消息
|
||||
*/
|
||||
String errorMessage() default "Sign签名校验失败!";
|
||||
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package org.jeecg.config.sign.aspect;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.jeecg.config.sign.annotation.SignatureCheck;
|
||||
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 基于AOP的签名验证切面
|
||||
* 复用SignAuthInterceptor的成熟签名验证逻辑
|
||||
*
|
||||
* @author GitHub Copilot
|
||||
* @since 2025-12-15
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
@Component("signatureCheckAspect")
|
||||
public class SignatureCheckAspect {
|
||||
|
||||
/**
|
||||
* 复用SignAuthInterceptor的签名验证逻辑
|
||||
*/
|
||||
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
|
||||
|
||||
/**
|
||||
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
|
||||
*/
|
||||
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
|
||||
private void signatureCheckPointCut() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始验签
|
||||
*/
|
||||
@Before("signatureCheckPointCut()")
|
||||
public void doSignatureValidation(JoinPoint point) throws Exception {
|
||||
// 获取方法上的注解
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
|
||||
|
||||
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
|
||||
|
||||
// 如果注解被禁用,直接返回
|
||||
if (!signatureCheck.enabled()) {
|
||||
log.info("签名验证已禁用,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||
Object bodyParam = null;
|
||||
Object[] args = point.getArgs();
|
||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
Object arg = args[i];
|
||||
Annotation[] annotations = parameterAnnotations[i];
|
||||
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
|
||||
if (hasRequestBodyAnnotation) {
|
||||
// 捕获携带@RequestBody注解的参数,供签名校验使用
|
||||
bodyParam = arg;
|
||||
}
|
||||
}
|
||||
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数,解决签名校验时读取请求体为空的问题
|
||||
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
log.error("无法获取请求上下文");
|
||||
throw new IllegalArgumentException("无法获取请求上下文");
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
|
||||
|
||||
try {
|
||||
// 直接调用SignAuthInterceptor的验证逻辑
|
||||
signAuthInterceptor.validateSignature(request, bodyParam);
|
||||
log.info("AOP签名验证通过");
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 使用注解中配置的错误消息,或者保留原始错误消息
|
||||
String errorMessage = signatureCheck.errorMessage();
|
||||
log.error("AOP签名验证失败: {}", e.getMessage());
|
||||
|
||||
if ("Sign签名校验失败!".equals(errorMessage)) {
|
||||
// 如果是默认错误消息,使用原始的详细错误信息
|
||||
throw e;
|
||||
} else {
|
||||
// 如果是自定义错误消息,使用自定义消息
|
||||
throw new IllegalArgumentException(errorMessage, e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 包装其他异常
|
||||
String errorMessage = signatureCheck.errorMessage();
|
||||
log.error("AOP签名验证异常: {}", e.getMessage());
|
||||
throw new IllegalArgumentException(errorMessage, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package org.jeecg.config.sign.interceptor;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MCP Stdio 工具 - 修复编码问题
|
||||
确保所有输出都使用UTF-8编码
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
|
||||
# 强制使用UTF-8编码
|
||||
if sys.platform == "win32":
|
||||
# Windows需要特殊处理
|
||||
import io
|
||||
|
||||
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
else:
|
||||
# Unix-like系统
|
||||
sys.stdin.reconfigure(encoding='utf-8')
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
|
||||
# 设置环境变量
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
encoding='utf-8'
|
||||
)
|
||||
logger = logging.getLogger("mcp-tool")
|
||||
|
||||
|
||||
class FixedMCPServer:
|
||||
"""修复编码问题的MCP服务器"""
|
||||
|
||||
def __init__(self):
|
||||
self.tools = {}
|
||||
self.initialize_tools()
|
||||
|
||||
def initialize_tools(self):
|
||||
"""初始化工具集"""
|
||||
|
||||
# 获取时间
|
||||
self.tools["get_time"] = {
|
||||
"name": "get_time",
|
||||
"description": "获取当前时间",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "时间格式",
|
||||
"enum": ["iso", "timestamp", "human", "chinese"],
|
||||
"default": "iso"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 文本处理工具
|
||||
self.tools["text_process"] = {
|
||||
"name": "text_process",
|
||||
"description": "文本处理工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "输入文本"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"description": "操作类型",
|
||||
"enum": ["length", "upper", "lower", "reverse", "count_words"],
|
||||
"default": "length"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
}
|
||||
}
|
||||
|
||||
# 数据格式工具
|
||||
self.tools["format_data"] = {
|
||||
"name": "format_data",
|
||||
"description": "格式化数据",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "原始数据"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "格式类型",
|
||||
"enum": ["json", "yaml", "xml"],
|
||||
"default": "json"
|
||||
}
|
||||
},
|
||||
"required": ["data"]
|
||||
}
|
||||
}
|
||||
|
||||
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""处理请求"""
|
||||
try:
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
|
||||
if method == "tools/list":
|
||||
return self.handle_tools_list()
|
||||
elif method == "tools/call":
|
||||
return self.handle_tool_call(params)
|
||||
elif method == "ping":
|
||||
return {"result": "pong"}
|
||||
else:
|
||||
return self.create_error_response(
|
||||
code=-32601,
|
||||
message="Method not found"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling request: {e}")
|
||||
return self.create_error_response(
|
||||
code=-32603,
|
||||
message=f"Internal error: {str(e)}"
|
||||
)
|
||||
|
||||
def handle_tools_list(self) -> Dict[str, Any]:
|
||||
"""列出所有工具 - 确保返回标准JSON"""
|
||||
return {
|
||||
"result": {
|
||||
"tools": list(self.tools.values())
|
||||
}
|
||||
}
|
||||
|
||||
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""调用工具 - 修复响应格式"""
|
||||
name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if name not in self.tools:
|
||||
return self.create_error_response(
|
||||
code=-32602,
|
||||
message=f"Tool '{name}' not found"
|
||||
)
|
||||
|
||||
try:
|
||||
if name == "get_time":
|
||||
result = self.execute_get_time(arguments)
|
||||
elif name == "text_process":
|
||||
result = self.execute_text_process(arguments)
|
||||
elif name == "format_data":
|
||||
result = self.execute_format_data(arguments)
|
||||
else:
|
||||
return self.create_error_response(
|
||||
code=-32602,
|
||||
message="Tool not implemented"
|
||||
)
|
||||
|
||||
# 确保返回正确的MCP响应格式
|
||||
return self.create_success_response(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Tool execution error: {e}")
|
||||
return self.create_error_response(
|
||||
code=-32603,
|
||||
message=f"Tool execution failed: {str(e)}"
|
||||
)
|
||||
|
||||
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""获取时间 - 支持中文"""
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
format_type = args.get("format", "iso")
|
||||
now = datetime.now()
|
||||
|
||||
if format_type == "iso":
|
||||
result = now.isoformat()
|
||||
elif format_type == "timestamp":
|
||||
result = now.timestamp()
|
||||
elif format_type == "human":
|
||||
result = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
elif format_type == "chinese":
|
||||
result = now.strftime("%Y年%m月%d日 %H时%M分%S秒")
|
||||
else:
|
||||
result = now.isoformat()
|
||||
logger.info(f"当前系统时间:{result}")
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format_type,
|
||||
"time": result,
|
||||
"timestamp": now.timestamp(),
|
||||
"date": now.strftime("%Y-%m-%d"),
|
||||
"time_12h": now.strftime("%I:%M:%S %p")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""文本处理"""
|
||||
try:
|
||||
text = args.get("text", "")
|
||||
operation = args.get("operation", "length")
|
||||
|
||||
if operation == "length":
|
||||
result = len(text)
|
||||
result_str = f"文本长度: {result} 个字符"
|
||||
elif operation == "upper":
|
||||
result = text.upper()
|
||||
result_str = f"大写: {result}"
|
||||
elif operation == "lower":
|
||||
result = text.lower()
|
||||
result_str = f"小写: {result}"
|
||||
elif operation == "reverse":
|
||||
result = text[::-1]
|
||||
result_str = f"反转: {result}"
|
||||
elif operation == "count_words":
|
||||
words = len(text.split())
|
||||
result = words
|
||||
result_str = f"单词数: {words}"
|
||||
else:
|
||||
raise ValueError(f"未知操作: {operation}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"operation": operation,
|
||||
"original_text": text,
|
||||
"result": result,
|
||||
"result_str": result_str,
|
||||
"text_length": len(text)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"operation": args.get("operation", "")
|
||||
}
|
||||
|
||||
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化数据"""
|
||||
try:
|
||||
data_str = args.get("data", "")
|
||||
format_type = args.get("format", "json")
|
||||
|
||||
# 尝试解析为JSON
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
is_json = True
|
||||
except:
|
||||
data = data_str
|
||||
is_json = False
|
||||
|
||||
if format_type == "json":
|
||||
if is_json:
|
||||
result = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# 如果不是JSON,包装成JSON
|
||||
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
|
||||
elif format_type == "yaml":
|
||||
import yaml
|
||||
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
|
||||
elif format_type == "xml":
|
||||
# 简单的XML格式化
|
||||
if isinstance(data, dict):
|
||||
result = "<data>"
|
||||
for k, v in data.items():
|
||||
result += f"\n <{k}>{v}</{k}>"
|
||||
result += "\n</data>"
|
||||
else:
|
||||
result = f"<text>{data}</text>"
|
||||
else:
|
||||
result = str(data)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format_type,
|
||||
"original": data_str,
|
||||
"formatted": result,
|
||||
"length": len(result)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"format": args.get("format", "")
|
||||
}
|
||||
|
||||
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""创建成功响应 - 确保符合MCP规范"""
|
||||
# 将数据转换为JSON字符串作为文本内容
|
||||
content_text = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
return {
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": content_text
|
||||
}
|
||||
],
|
||||
"isError": False
|
||||
}
|
||||
}
|
||||
|
||||
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
|
||||
"""创建错误响应"""
|
||||
return {
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def safe_json_dump(data: Dict[str, Any]) -> str:
|
||||
"""安全的JSON序列化,确保UTF-8编码"""
|
||||
try:
|
||||
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
||||
except:
|
||||
# 如果失败,使用ASCII转义
|
||||
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 修复Stdio通信"""
|
||||
logger.info("启动MCP Stdio服务器 (修复编码版)...")
|
||||
|
||||
server = FixedMCPServer()
|
||||
|
||||
# 初始握手消息
|
||||
init_message = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "fixed-mcp-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 发送初始化响应
|
||||
try:
|
||||
sys.stdout.write(safe_json_dump(init_message) + "\n")
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"发送初始化消息失败: {e}")
|
||||
return
|
||||
|
||||
logger.info("MCP服务器已初始化")
|
||||
|
||||
# 主循环
|
||||
line_num = 0
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
logger.info("输入流结束")
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
line_num += 1
|
||||
|
||||
if not line:
|
||||
continue
|
||||
|
||||
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
logger.info(f"解析请求: {request.get('method', 'unknown')}")
|
||||
|
||||
# 处理请求
|
||||
response = server.handle_request(request)
|
||||
response["jsonrpc"] = "2.0"
|
||||
response["id"] = request.get("id")
|
||||
|
||||
# 发送响应
|
||||
response_json = safe_json_dump(response)
|
||||
sys.stdout.write(response_json + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {e}")
|
||||
error_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error at line {line_num}"
|
||||
},
|
||||
"id": None
|
||||
}
|
||||
sys.stdout.write(safe_json_dump(error_response) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("接收到中断信号")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"未处理的错误: {e}")
|
||||
break
|
||||
|
||||
logger.info("MCP服务器已停止")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<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>
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -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 = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
|
||||
public interface IAiragVariableService {
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
void updateVariable(String userId, String appId, String name, String value);
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
String additionalPrompt(String username, AiragApp app);
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
void initVariable(String userId, String appId, String name, String defaultValue);
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
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 {
|
||||
|
||||
@ -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 "";
|
||||
}).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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,194 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AI应用变量服务实现
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragVariableServiceImpl implements IAiragVariableService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate redisTemplate;
|
||||
|
||||
private static final String CACHE_PREFIX = "airag:app:var:";
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param username
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
@Override
|
||||
public void initVariable(String username, String appId, String name, String defaultValue) {
|
||||
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + username;
|
||||
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String additionalPrompt(String username, AiragApp app) {
|
||||
String memoryPrompt = app.getMemoryPrompt();
|
||||
String prompt = app.getPrompt();
|
||||
|
||||
if (oConvertUtils.isEmpty(memoryPrompt)) {
|
||||
return prompt;
|
||||
}
|
||||
String variablesStr = app.getVariables();
|
||||
if (oConvertUtils.isEmpty(variablesStr)) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
if (variableList == null || variableList.isEmpty()) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
String key = CACHE_PREFIX + app.getId() + ":" + username;
|
||||
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
|
||||
|
||||
for (AppVariableVo variable : variableList) {
|
||||
if (variable.getEnable() != null && !variable.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
String name = variable.getName();
|
||||
String value = variable.getDefaultValue();
|
||||
|
||||
// 优先使用Redis中的值
|
||||
if (savedValues.containsKey(name)) {
|
||||
Object savedVal = savedValues.get(name);
|
||||
if (savedVal != null) {
|
||||
value = String.valueOf(savedVal);
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
|
||||
// 替换 {{name}}
|
||||
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
|
||||
}
|
||||
return prompt + "\n" + memoryPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
@Override
|
||||
public void updateVariable(String userId, String appId, String name, String value) {
|
||||
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + userId;
|
||||
redisTemplate.opsForHash().put(key, name, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
@Override
|
||||
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
|
||||
if (params.getTools() == null) {
|
||||
params.setTools(new HashMap<>());
|
||||
}
|
||||
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
|
||||
return;
|
||||
}
|
||||
// 构建变量描述信息
|
||||
String variablesStr = aiApp.getVariables();
|
||||
List<AppVariableVo> variableList = null;
|
||||
if (oConvertUtils.isNotEmpty(variablesStr)) {
|
||||
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
}
|
||||
|
||||
//工具描述
|
||||
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
|
||||
if (variableList != null && !variableList.isEmpty()) {
|
||||
descriptionBuilder.append("\n\n可用变量列表:");
|
||||
for (AppVariableVo var : variableList) {
|
||||
if (var.getEnable() != null && !var.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
descriptionBuilder.append("\n- ").append(var.getName());
|
||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||
descriptionBuilder.append(": ").append(var.getDescription());
|
||||
}
|
||||
}
|
||||
descriptionBuilder.append("\n\n注意:variableName必须是上述列表中的名称之一。");
|
||||
}
|
||||
|
||||
//构建更新变量的工具
|
||||
ToolSpecification spec = ToolSpecification.builder()
|
||||
.name("update_variable")
|
||||
.description(descriptionBuilder.toString())
|
||||
.parameters(JsonObjectSchema.builder()
|
||||
.addStringProperty("variableName", "变量名称")
|
||||
.addStringProperty("value", "变量值")
|
||||
.required("variableName", "value")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
//监听工具的调用
|
||||
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
|
||||
try {
|
||||
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
||||
String name = args.getString("variableName");
|
||||
String value = args.getString("value");
|
||||
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
|
||||
//更新变量值
|
||||
variableService.updateVariable(username, aiApp.getId(), name, value);
|
||||
return "变量 " + name + " 已更新为: " + value;
|
||||
} catch (Exception e) {
|
||||
log.error("更新变量失败", e);
|
||||
return "更新变量失败: " + e.getMessage();
|
||||
}
|
||||
};
|
||||
|
||||
params.getTools().put(spec, executor);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description: ai写作生成实体类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2026/1/12 15:59
|
||||
*/
|
||||
@Data
|
||||
public class AiWriteGenerateVo {
|
||||
|
||||
/**
|
||||
* 写作类型
|
||||
*/
|
||||
private String activeMode;
|
||||
|
||||
/**
|
||||
* 写作内容提示
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 原文
|
||||
*/
|
||||
private String originalContent;
|
||||
|
||||
/**
|
||||
* 长度
|
||||
*/
|
||||
private String length;
|
||||
|
||||
/**
|
||||
* 格式
|
||||
*/
|
||||
private String format;
|
||||
|
||||
/**
|
||||
* 语气
|
||||
*/
|
||||
private String tone;
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
private String language;
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Description: 应用变量配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AppVariableVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 变量名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
private String defaultValue;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enable;
|
||||
|
||||
/**
|
||||
* 动作
|
||||
*/
|
||||
private String action;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer orderNum;
|
||||
}
|
||||
@ -47,4 +47,14 @@ public class ChatConversation {
|
||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||
*/
|
||||
private Map<String, Object> flowInputs;
|
||||
|
||||
/**
|
||||
* portal 应用门户
|
||||
*/
|
||||
private String sessionType;
|
||||
|
||||
/**
|
||||
* 是否保存会话
|
||||
*/
|
||||
private Boolean izSaveSession;
|
||||
}
|
||||
|
||||
@ -47,6 +47,11 @@ public class ChatSendParams {
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private List<String> 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;
|
||||
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
/**
|
||||
* @Description: 流程插件常量
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/12/23 19:37
|
||||
*/
|
||||
public interface FlowPluginContent {
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
String NAME = "name";
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
String DESCRIPTION = "description";
|
||||
|
||||
/**
|
||||
* 响应
|
||||
*/
|
||||
String RESPONSES = "responses";
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
String TYPE = "type";
|
||||
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
String PARAMETERS = "parameters";
|
||||
|
||||
/**
|
||||
* 是否必须
|
||||
*/
|
||||
String REQUIRED = "required";
|
||||
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
String DEFAULT_VALUE = "defaultValue";
|
||||
|
||||
/**
|
||||
* 路径
|
||||
*/
|
||||
String PATH = "path";
|
||||
|
||||
/**
|
||||
* 方法
|
||||
*/
|
||||
String METHOD = "method";
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
String LOCATION = "location";
|
||||
|
||||
/**
|
||||
* 认证类型
|
||||
*/
|
||||
String AUTH_TYPE = "authType";
|
||||
|
||||
/**
|
||||
* token参数名称
|
||||
*/
|
||||
String TOKEN_PARAM_NAME = "tokenParamName";
|
||||
|
||||
/**
|
||||
* token参数值
|
||||
*/
|
||||
String TOKEN_PARAM_VALUE = "tokenParamValue";
|
||||
|
||||
/**
|
||||
* token
|
||||
*/
|
||||
String TOKEN = "token";
|
||||
|
||||
/**
|
||||
* Path位置
|
||||
*/
|
||||
String LOCATION_PATH = "Path";
|
||||
|
||||
/**
|
||||
* Header位置
|
||||
*/
|
||||
String LOCATION_HEADER = "Header";
|
||||
|
||||
/**
|
||||
* Query位置
|
||||
*/
|
||||
String LOCATION_QUERY = "Query";
|
||||
|
||||
/**
|
||||
* Body位置
|
||||
*/
|
||||
String LOCATION_BODY = "Body";
|
||||
|
||||
/**
|
||||
* Form-Data位置
|
||||
*/
|
||||
String LOCATION_FORM_DATA = "Form-Data";
|
||||
|
||||
/**
|
||||
* String类型
|
||||
*/
|
||||
String TYPE_STRING = "String";
|
||||
|
||||
/**
|
||||
* string类型
|
||||
*/
|
||||
String TYPE_STRING_LOWER = "string";
|
||||
|
||||
/**
|
||||
* Number类型
|
||||
*/
|
||||
String TYPE_NUMBER = "Number";
|
||||
|
||||
/**
|
||||
* number类型
|
||||
*/
|
||||
String TYPE_NUMBER_LOWER = "number";
|
||||
|
||||
/**
|
||||
* Integer类型
|
||||
*/
|
||||
String TYPE_INTEGER = "Integer";
|
||||
|
||||
/**
|
||||
* integer类型
|
||||
*/
|
||||
String TYPE_INTEGER_LOWER = "integer";
|
||||
|
||||
/**
|
||||
* Boolean类型
|
||||
*/
|
||||
String TYPE_BOOLEAN = "Boolean";
|
||||
|
||||
/**
|
||||
* boolean类型
|
||||
*/
|
||||
String TYPE_BOOLEAN_LOWER = "boolean";
|
||||
|
||||
/**
|
||||
* 工具数量
|
||||
*/
|
||||
String TOOL_COUNT = "tool_count";
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
String ENABLED = "enabled";
|
||||
|
||||
/**
|
||||
* 输入
|
||||
*/
|
||||
String INPUTS = "inputs";
|
||||
|
||||
/**
|
||||
* 输出
|
||||
*/
|
||||
String OUTPUTS = "outputs";
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
String POST = "POST";
|
||||
|
||||
/**
|
||||
* token名称
|
||||
*/
|
||||
String X_ACCESS_TOKEN = "X-Access-Token";
|
||||
|
||||
/**
|
||||
* 插件名称
|
||||
*/
|
||||
String PLUGIN_NAME = "流程调用";
|
||||
|
||||
/**
|
||||
* 插件描述
|
||||
*/
|
||||
String PLUGIN_DESC = "调用工作流";
|
||||
|
||||
/**
|
||||
* 插件请求地址
|
||||
*/
|
||||
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
|
||||
|
||||
/**
|
||||
* 记忆库插件名称
|
||||
*/
|
||||
String PLUGIN_MEMORY_NAME = "记忆库";
|
||||
|
||||
/**
|
||||
* 记忆库插件描述
|
||||
*/
|
||||
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
|
||||
|
||||
/**
|
||||
* 添加记忆路径
|
||||
*/
|
||||
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
|
||||
|
||||
/**
|
||||
* 查询记忆路径
|
||||
*/
|
||||
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* airag baseAPI Controller
|
||||
*
|
||||
* @author sjlei
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
@RestController("airagBaseApiController")
|
||||
public class AiragBaseApiController implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
AiragBaseApiImpl airagBaseApi;
|
||||
|
||||
@PostMapping("/airag/api/knowledgeWriteTextDocument")
|
||||
public String knowledgeWriteTextDocument(
|
||||
@RequestParam("knowledgeId") String knowledgeId,
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam("content") String content
|
||||
) {
|
||||
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,14 +1,18 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 格式)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 绘画创作 ========================================
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
// ignore:token获取不到默认为admin
|
||||
username = "admin";
|
||||
}
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
metadata.put(EMBED_STORE_METADATA_USER_NAME, username);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
Document from = Document.from(content, metadata);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
package org.jeecg.modules.airag.llm.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 获取流程mcp服务
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025-12-22 15:34:20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragFlowPluginService {
|
||||
|
||||
/**
|
||||
* 同步所有启用的流程到MCP插件配置
|
||||
*
|
||||
* @param flowIds 多个流程id
|
||||
*/
|
||||
Map<String, Object> getFlowsToPlugin(String flowIds);
|
||||
}
|
||||
@ -3,6 +3,8 @@ package org.jeecg.modules.airag.llm.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import 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);
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* airag baseAPI 实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Primary
|
||||
@Service("airagBaseApiImpl")
|
||||
public class AiragBaseApiImpl implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeDocService airagKnowledgeDocService;
|
||||
|
||||
@Override
|
||||
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
|
||||
AssertUtils.assertNotEmpty("知识库ID不能为空", knowledgeId);
|
||||
AssertUtils.assertNotEmpty("写入内容不能为空", content);
|
||||
AiragKnowledgeDoc knowledgeDoc = new AiragKnowledgeDoc();
|
||||
knowledgeDoc.setKnowledgeId(knowledgeId);
|
||||
knowledgeDoc.setTitle(title);
|
||||
knowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||
knowledgeDoc.setContent(content);
|
||||
Result<?> result = airagKnowledgeDocService.editDocument(knowledgeDoc);
|
||||
if (!result.isSuccess()) {
|
||||
throw new JeecgBootBizTipException(result.getMessage());
|
||||
}
|
||||
if (knowledgeDoc.getId() == null) {
|
||||
throw new JeecgBootBizTipException("知识库文档ID为空");
|
||||
}
|
||||
log.info("[AI-KNOWLEDGE] 文档写入完成,知识库:{}, 文档ID:{}", knowledgeId, knowledgeDoc.getId());
|
||||
return knowledgeDoc.getId();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
|
||||
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
|
||||
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @Description: 流程同步到MCP服务实现类
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025-12-22
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragFlowPluginServiceImpl implements IAiragFlowPluginService {
|
||||
|
||||
@Autowired
|
||||
private IAiragFlowService airagFlowService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getFlowsToPlugin(String flowIds) {
|
||||
log.info("开始构建流程插件");
|
||||
// 1. 查询所有启用的流程
|
||||
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(AiragFlow::getStatus, FlowConsts.FLOW_STATUS_ENABLE);
|
||||
queryWrapper.in(AiragFlow::getId, Arrays.asList(flowIds.split(SymbolConstant.COMMA)));
|
||||
List<AiragFlow> flows = airagFlowService.list(queryWrapper);
|
||||
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||
if (flows.isEmpty()) {
|
||||
log.info("当前应用所选流程没有启用的流程");
|
||||
return null;
|
||||
}
|
||||
//返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
//插件
|
||||
//插件id
|
||||
AiragMcp tool = new AiragMcp();
|
||||
// 2. 构建插件
|
||||
String id = UUID.randomUUID().toString().replace("-", "");
|
||||
tool.setId(id);
|
||||
// 插件名称
|
||||
tool.setName(FlowPluginContent.PLUGIN_NAME);
|
||||
// 描述
|
||||
tool.setDescr(FlowPluginContent.PLUGIN_DESC);
|
||||
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||
tool.setCategory("plugin");
|
||||
tool.setEndpoint("");
|
||||
int toolCount = 0;
|
||||
//构建拆件工具
|
||||
for (AiragFlow flow : flows) {
|
||||
try {
|
||||
|
||||
SubFlowResult subFlow = new SubFlowResult(flow);
|
||||
// 获取入参参数
|
||||
JSONArray parameter = getInputParameter(flow, subFlow);
|
||||
// 获取出参参数
|
||||
JSONArray outParams = getOutputParameter(flow, subFlow);
|
||||
// name必须符合 ^[a-zA-Z0-9_-]+$
|
||||
String validToolName = "flow_" + flow.getId();
|
||||
// 将原始名称拼接到描述中
|
||||
String description = flow.getName();
|
||||
if (oConvertUtils.isNotEmpty(flow.getDescr())) {
|
||||
description += " : " + flow.getDescr();
|
||||
}
|
||||
//构造工具参数
|
||||
String flowTool = buildParameter(parameter, outParams, flow.getId(), tool.getTools(), validToolName, description);
|
||||
tool.setTools(flowTool);
|
||||
toolCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("处理流程[{}]转换插件失败: {}", flow.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||
//构建元数据(请求头)
|
||||
String meataData = buildMetadata(toolCount, tenantId);
|
||||
tool.setMetadata(meataData);
|
||||
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||
result.put("pluginTool", tools);
|
||||
result.put("pluginId", id);
|
||||
log.info("构建流程插件结束");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建元数据
|
||||
*
|
||||
* @param toolCount
|
||||
* @param tenantId
|
||||
*/
|
||||
private String buildMetadata(int toolCount, String tenantId) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOOL_COUNT, toolCount);
|
||||
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||
return jsonObject.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建参数
|
||||
*
|
||||
* @param parameter
|
||||
* @param outParams
|
||||
* @param flowId
|
||||
* @param tools
|
||||
* @param description
|
||||
* @param name
|
||||
*/
|
||||
private String buildParameter(JSONArray parameter, JSONArray outParams, String flowId, String tools, String name, String description) {
|
||||
JSONArray paramArray = new JSONArray();
|
||||
JSONObject parameterObject = new JSONObject();
|
||||
parameterObject.put(FlowPluginContent.NAME, name);
|
||||
parameterObject.put(FlowPluginContent.DESCRIPTION, description);
|
||||
parameterObject.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_REQUEST_URL + flowId);
|
||||
parameterObject.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
parameterObject.put(FlowPluginContent.ENABLED, true);
|
||||
parameterObject.put(FlowPluginContent.PARAMETERS, parameter);
|
||||
parameterObject.put(FlowPluginContent.RESPONSES, outParams);
|
||||
if (oConvertUtils.isNotEmpty(tools)) {
|
||||
paramArray = JSONArray.parseArray(tools);
|
||||
paramArray.add(parameterObject);
|
||||
} else {
|
||||
paramArray.add(parameterObject);
|
||||
}
|
||||
return paramArray.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数
|
||||
*
|
||||
* @param flow
|
||||
* @param subFlow
|
||||
*/
|
||||
private JSONArray getInputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.INPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.INPUTS);
|
||||
jsonArray.forEach(item -> {
|
||||
if (oConvertUtils.isNotEmpty(item.toString())) {
|
||||
JSONObject json = JSONObject.parseObject(item.toString());
|
||||
json.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
}
|
||||
});
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
//需要获取子流程的参数,子流程的参数是单独封装的,否则在流程执行的时候会报错缺少参数
|
||||
List<FlowNodeConfig.NodeParam> inputParams = subFlow.getInputParams();
|
||||
if (inputParams != null) {
|
||||
for (FlowNodeConfig.NodeParam param : inputParams) {
|
||||
JSONObject p = new JSONObject();
|
||||
// 参数名
|
||||
p.put(FlowPluginContent.NAME, param.getField());
|
||||
String paramDesc = param.getName();
|
||||
if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
paramDesc = param.getField();
|
||||
}
|
||||
// 参数描述
|
||||
p.put(FlowPluginContent.DESCRIPTION, paramDesc);
|
||||
// 类型
|
||||
p.put(FlowPluginContent.TYPE, oConvertUtils.getString(param.getType(), FlowPluginContent.TYPE_STRING));
|
||||
// 所有参数都在Body中
|
||||
p.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
boolean required = param.getRequired() != null && param.getRequired();
|
||||
p.put(FlowPluginContent.REQUIRED, required);
|
||||
parameters.add(p);
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值
|
||||
*/
|
||||
private JSONArray getOutputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.OUTPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.OUTPUTS);
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
// List<FlowNodeConfig.NodeParam> outputParams = subFlow.getOutputParams();
|
||||
// if (outputParams != null) {
|
||||
// for (FlowNodeConfig.NodeParam param : outputParams) {
|
||||
// JSONObject p = new JSONObject();
|
||||
// // 参数名
|
||||
// p.put("name", param.getField());
|
||||
// String paramDesc = param.getName();
|
||||
// if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
// paramDesc = param.getField();
|
||||
// }
|
||||
// // 参数描述
|
||||
// p.put("description", paramDesc);
|
||||
// // 类型
|
||||
// p.put("type", oConvertUtils.getString(param.getType(), "String"));
|
||||
// parameters.add(p);
|
||||
// }
|
||||
// }
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@ -131,7 +131,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
|
||||
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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.prompts.consts;
|
||||
|
||||
/**
|
||||
* AI提示词常量类
|
||||
*
|
||||
*/
|
||||
public class AiPromptsConsts {
|
||||
|
||||
/**
|
||||
* 状态:进行中
|
||||
*/
|
||||
public static final String STATUS_RUNNING = "run";
|
||||
/**
|
||||
* 状态:完成
|
||||
*/
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
/**
|
||||
* 状态:失败
|
||||
*/
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
/**
|
||||
* 业务类型:评估器
|
||||
*/
|
||||
public static final String BIZ_TYPE_EVALUATOR = "evaluator";
|
||||
/**
|
||||
* 业务类型:轨迹
|
||||
*/
|
||||
public static final String BIZ_TYPE_TRACK = "track";
|
||||
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_ext_data")
|
||||
@RestController
|
||||
@RequestMapping("/airag/extData")
|
||||
@Slf4j
|
||||
public class AiragExtDataController extends JeecgController<AiragExtData, IAiragExtDataService> {
|
||||
@Autowired
|
||||
private IAiragExtDataService airagExtDataService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-分页列表查询")
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragExtData>> queryPageList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
/**
|
||||
* 调用轨迹列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/getTrackList")
|
||||
public Result<IPage<AiragExtData>> getTrackList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||
String metadata = airagExtData.getMetadata();
|
||||
if(oConvertUtils.isEmpty(metadata)){
|
||||
return Result.OK();
|
||||
}
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-添加")
|
||||
@Operation(summary="airag_ext_data-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtData.setBizType(AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
airagExtDataService.save(airagExtData);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-编辑")
|
||||
@Operation(summary="airag_ext_data-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-通过id删除")
|
||||
@Operation(summary="airag_ext_data-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagExtDataService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-批量删除")
|
||||
@Operation(summary="airag_ext_data-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagExtDataService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragExtData> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
if(airagExtData==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagExtData);
|
||||
}
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryTrackById")
|
||||
public Result<List<AiragExtData>> queryTrackById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
String status = airagExtData.getStatus();
|
||||
if(AiPromptsConsts.STATUS_RUNNING.equals(status)) {
|
||||
return Result.error("处理中,请稍后刷新");
|
||||
}
|
||||
List<AiragExtData> trackList = airagExtDataService.queryTrackById(id);
|
||||
return Result.OK(trackList);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param debugVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/evaluator/debug")
|
||||
public Result<?> debugEvaluator(@RequestBody AiragDebugVo debugVo) {
|
||||
return airagExtDataService.debugEvaluator(debugVo);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagExtData
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragExtData airagExtData) {
|
||||
return super.exportXls(request, airagExtData, AiragExtData.class, "airag_ext_data");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragExtData.class);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_prompts")
|
||||
@RestController
|
||||
@RequestMapping("/airag/prompts")
|
||||
@Slf4j
|
||||
public class AiragPromptsController extends JeecgController<AiragPrompts, IAiragPromptsService> {
|
||||
@Autowired
|
||||
private IAiragPromptsService airagPromptsService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-分页列表查询")
|
||||
@Operation(summary="airag_prompts-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragPrompts>> queryPageList(AiragPrompts airagPrompts,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragPrompts> queryWrapper = QueryGenerator.initQueryWrapper(airagPrompts, req.getParameterMap());
|
||||
Page<AiragPrompts> page = new Page<AiragPrompts>(pageNo, pageSize);
|
||||
IPage<AiragPrompts> pageList = airagPromptsService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-添加")
|
||||
@Operation(summary="airag_prompts-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPrompts.setDelFlag(CommonConstant.DEL_FLAG_0);
|
||||
airagPrompts.setStatus("0");
|
||||
airagPromptsService.save(airagPrompts);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-编辑")
|
||||
@Operation(summary="airag_prompts-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPromptsService.updateById(airagPrompts);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-通过id删除")
|
||||
@Operation(summary="airag_prompts-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagPromptsService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-批量删除")
|
||||
@Operation(summary="airag_prompts-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagPromptsService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-通过id查询")
|
||||
@Operation(summary="airag_prompts-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragPrompts> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragPrompts airagPrompts = airagPromptsService.getById(id);
|
||||
if(airagPrompts==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagPrompts);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param experimentVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/experiment")
|
||||
public Result<?> promptExperiment(@RequestBody AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||
return airagPromptsService.promptExperiment(experimentVo,request);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagPrompts
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragPrompts airagPrompts) {
|
||||
return super.exportXls(request, airagPrompts, AiragPrompts.class, "airag_prompts");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragPrompts.class);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_ext_data")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_ext_data")
|
||||
public class AiragExtData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**业务类型标识(evaluator:评估器;track:测试追踪)*/
|
||||
@Excel(name = "业务类型标识(evaluator:评估器;track:测试追踪)", width = 15)
|
||||
@Schema(description = "业务类型标识(evaluator:评估器;track:测试追踪)")
|
||||
private java.lang.String bizType;
|
||||
/**名称*/
|
||||
@Excel(name = "名称", width = 15)
|
||||
@Schema(description = "名称")
|
||||
private java.lang.String name;
|
||||
/**描述信息*/
|
||||
@Excel(name = "描述信息", width = 15)
|
||||
@Schema(description = "描述信息")
|
||||
private java.lang.String descr;
|
||||
/**标签,多个用逗号分隔*/
|
||||
@Excel(name = "标签,多个用逗号分隔", width = 15)
|
||||
@Schema(description = "标签,多个用逗号分隔")
|
||||
private java.lang.String tags;
|
||||
/**实际存储内容,json*/
|
||||
@Excel(name = "实际存储内容,json", width = 15)
|
||||
@Schema(description = "实际存储内容,json")
|
||||
private java.lang.String dataValue;
|
||||
/**元数据,用于存储补充业务数据信息*/
|
||||
@Excel(name = "元数据,用于存储补充业务数据信息", width = 15)
|
||||
@Schema(description = "元数据,用于存储补充业务数据信息")
|
||||
private java.lang.String metadata;
|
||||
/**评测集数据*/
|
||||
@Excel(name = "评测集数据", width = 15)
|
||||
@Schema(description = "评测集数据")
|
||||
private java.lang.String datasetValue;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建时间")
|
||||
private java.util.Date createTime;
|
||||
/**修改人*/
|
||||
@Schema(description = "修改人")
|
||||
private java.lang.String updateBy;
|
||||
/**修改时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "修改时间")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
/**状态*/
|
||||
@Excel(name = "状态(run:进行中 completed:已完成)", width = 15)
|
||||
@Schema(description = "状态(run:进行中 completed:已完成)")
|
||||
private java.lang.String status;
|
||||
/**版本*/
|
||||
@Excel(name = "版本", width = 15)
|
||||
@Schema(description = "版本")
|
||||
private java.lang.Integer version;
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_prompts")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_prompts")
|
||||
public class AiragPrompts implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示词名称", width = 15)
|
||||
@Schema(description = "提示词名称")
|
||||
private java.lang.String name;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示key", width = 15)
|
||||
@Schema(description = "提示key")
|
||||
private java.lang.String promptKey;
|
||||
/**提示词功能描述*/
|
||||
@Excel(name = "提示词功能描述", width = 15)
|
||||
@Schema(description = "提示词功能描述")
|
||||
private java.lang.String description;
|
||||
/**提示词模板内容,支持变量占位符如 {{variable}}*/
|
||||
@Excel(name = "提示词模板内容,支持变量占位符如 {{variable}}", width = 15)
|
||||
@Schema(description = "提示词模板内容,支持变量占位符如 {{variable}}")
|
||||
private java.lang.String content;
|
||||
/**提示词分类*/
|
||||
@Excel(name = "提示词分类", width = 15)
|
||||
@Schema(description = "提示词分类")
|
||||
private java.lang.String category;
|
||||
/**标签,多个逗号分割*/
|
||||
@Excel(name = "标签,多个逗号分割", width = 15)
|
||||
@Schema(description = "标签,多个逗号分割")
|
||||
private java.lang.String tags;
|
||||
/**适配的大模型ID*/
|
||||
@Excel(name = "适配的大模型ID", width = 15)
|
||||
@Schema(description = "适配的大模型ID")
|
||||
private java.lang.String modelId;
|
||||
/**大模型的参数配置*/
|
||||
@Excel(name = "大模型的参数配置", width = 15)
|
||||
@Schema(description = "大模型的参数配置")
|
||||
private java.lang.String modelParam;
|
||||
/**状态(0:未发布 1:已发布)*/
|
||||
@Excel(name = "状态(0:未发布 1:已发布)", width = 15)
|
||||
@Schema(description = "状态(0:未发布 1:已发布)")
|
||||
private java.lang.String status;
|
||||
/**版本号(格式 0.0.1)*/
|
||||
@Excel(name = "版本号(格式 0.0.1)", width = 15)
|
||||
@Schema(description = "版本号(格式 0.0.1)")
|
||||
private java.lang.String version;
|
||||
/**删除状态(0未删除 1已删除)*/
|
||||
@Excel(name = "删除状态(0未删除 1已删除)", width = 15)
|
||||
@Schema(description = "删除状态(0未删除 1已删除)")
|
||||
@TableLogic
|
||||
private java.lang.Integer delFlag;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private java.util.Date createTime;
|
||||
/**更新人*/
|
||||
@Schema(description = "更新人")
|
||||
private java.lang.String updateBy;
|
||||
/**更新日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragExtDataMapper extends BaseMapper<AiragExtData> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragPromptsMapper extends BaseMapper<AiragPrompts> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper">
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper">
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jeecg.modules.airag.prompts.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragExtDataService extends IService<AiragExtData> {
|
||||
|
||||
Result debugEvaluator(AiragDebugVo debugVo);
|
||||
|
||||
List<AiragExtData> queryTrackById(String id);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package org.jeecg.modules.airag.prompts.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragPromptsService extends IService<AiragPrompts> {
|
||||
|
||||
Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request);
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package org.jeecg.modules.airag.prompts.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service("airagExtDataServiceImpl")
|
||||
public class AiragExtDataServiceImpl extends ServiceImpl<AiragExtDataMapper, AiragExtData> implements IAiragExtDataService {
|
||||
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Override
|
||||
public Result debugEvaluator(AiragDebugVo debugVo) {
|
||||
//1.提示词
|
||||
String prompt = debugVo.getPrompts();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
|
||||
//2.测试内容
|
||||
String content = debugVo.getContent();
|
||||
AssertUtils.assertNotEmpty("请输入测试内容", content);
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(content));
|
||||
|
||||
//3.模型数据
|
||||
String modelId = debugVo.getModelId();
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
//4.模型参数
|
||||
String modelParam = debugVo.getModelParam();
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject param = JSON.parseObject(modelParam);
|
||||
if(param.containsKey("temperature")){
|
||||
params.setTemperature(param.getDoubleValue("temperature"));
|
||||
}
|
||||
if(param.containsKey("topP")){
|
||||
params.setTemperature(param.getDoubleValue("topP"));
|
||||
}
|
||||
if(param.containsKey("presencePenalty")){
|
||||
params.setTemperature(param.getDoubleValue("presencePenalty"));
|
||||
}
|
||||
if(param.containsKey("frequencyPenalty")){
|
||||
params.setTemperature(param.getDoubleValue("frequencyPenalty"));
|
||||
}
|
||||
}
|
||||
//5.AI问答
|
||||
String promptValue = aiChatHandler.completions(modelId,messages, params);
|
||||
if (promptValue == null || promptValue.isEmpty()) {
|
||||
return Result.error("生成失败");
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询AI问答记录
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<AiragExtData> queryTrackById(String id) {
|
||||
LambdaQueryWrapper<AiragExtData> lqw = new LambdaQueryWrapper<AiragExtData>()
|
||||
.eq(AiragExtData::getMetadata, id)
|
||||
.orderByDesc(AiragExtData::getVersion)
|
||||
.orderByDesc(AiragExtData::getCreateTime);
|
||||
List<AiragExtData> list = this.baseMapper.selectList(lqw);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,394 @@
|
||||
package org.jeecg.modules.airag.prompts.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("airagPromptsServiceImpl")
|
||||
public class AiragPromptsServiceImpl extends ServiceImpl<AiragPromptsMapper, AiragPrompts> implements IAiragPromptsService {
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Autowired
|
||||
IAiragExtDataService airagExtDataService;
|
||||
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
// 创建静态线程池,确保整个应用生命周期中只有一个实例
|
||||
private static final ExecutorService executor = new ThreadPoolExecutor(
|
||||
4, // 核心线程数
|
||||
8, // 最大线程数
|
||||
60L, TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<>(100), // 防止内存溢出
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
|
||||
/**
|
||||
* 提示词实验
|
||||
* @param experimentVo
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||
log.info("开始执行提示词实验,参数:{}", JSON.toJSONString(experimentVo));
|
||||
|
||||
// 参数验证
|
||||
String promptKey = experimentVo.getPromptKey();
|
||||
AssertUtils.assertNotEmpty("请选择提示词", promptKey);
|
||||
String dataId = experimentVo.getExtDataId();
|
||||
AssertUtils.assertNotEmpty("请选择数据集", dataId);
|
||||
|
||||
Map<String, String> fieldMappings = experimentVo.getMappings();
|
||||
AssertUtils.assertNotEmpty("请配置字段映射", fieldMappings);
|
||||
|
||||
try {
|
||||
//1.查询提示词
|
||||
AiragPrompts airagPrompts = this.baseMapper.selectOne(new LambdaQueryWrapper<AiragPrompts>().eq(AiragPrompts::getPromptKey, promptKey));
|
||||
AssertUtils.assertNotEmpty("未找到指定的提示词", airagPrompts);
|
||||
String modelParam = airagPrompts.getModelParam();
|
||||
// 过滤提示词变量
|
||||
JSONArray promptVariables;
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject airagPromptsParams = JSON.parseObject(modelParam);
|
||||
if(airagPromptsParams.containsKey("promptVariables")){
|
||||
promptVariables = airagPromptsParams.getJSONArray("promptVariables");
|
||||
} else {
|
||||
promptVariables = null;
|
||||
}
|
||||
} else {
|
||||
promptVariables = null;
|
||||
}
|
||||
//2.查询数据集
|
||||
AiragExtData airagExtData = airagExtDataService.getById(dataId);
|
||||
AssertUtils.assertNotEmpty("未找到指定的数据集", airagExtData);
|
||||
String datasetValue = airagExtData.getDatasetValue();
|
||||
if(oConvertUtils.isEmpty(datasetValue)){
|
||||
return Result.error("评测集不能为空!");
|
||||
}
|
||||
|
||||
//3.异步调用 根据映射字段,调用评估器测评
|
||||
JSONObject datasetObj = JSONObject.parseObject(datasetValue);
|
||||
//评测列配置
|
||||
JSONArray columns = datasetObj.getJSONArray("columns");
|
||||
//评测题库
|
||||
JSONArray datasetArray = datasetObj.getJSONArray("dataSource");
|
||||
AssertUtils.assertNotEmpty("数据集中没有找到数据源", datasetArray);
|
||||
AssertUtils.assertTrue("数据源为空", datasetArray.size() > 0);
|
||||
|
||||
//测评结果集 - 使用线程安全的CopyOnWriteArrayList
|
||||
List<JSONObject> scoreResult = new CopyOnWriteArrayList<>();
|
||||
|
||||
// 批量提交任务
|
||||
List<CompletableFuture<Void>> futures = IntStream.range(0, datasetArray.size())
|
||||
.mapToObj(i -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
log.info("开始处理第{}条数据", i + 1);
|
||||
//定义返回结果
|
||||
JSONObject result = new JSONObject();
|
||||
//评测数据
|
||||
JSONObject dataset = datasetArray.getJSONObject(i);
|
||||
result.putAll(dataset);
|
||||
//用户问题
|
||||
String userQuery = dataset.getString(fieldMappings.get("user_query"));
|
||||
result.put("userQuery", userQuery);
|
||||
//变量处理
|
||||
if(!CollectionUtils.isEmpty(promptVariables)){
|
||||
String content = airagPrompts.getContent();
|
||||
for (Object var : promptVariables){
|
||||
JSONObject variable = JSONObject.parseObject(var.toString());
|
||||
String name = dataset.getString(fieldMappings.get(variable.getString("name")));
|
||||
//提示词默认变量值
|
||||
String defaultValue = variable.getString("value");
|
||||
// 获取目标类型
|
||||
String dataType = findDataType(columns, variable);
|
||||
if("FILE".equals(dataType)){
|
||||
defaultValue = getFileAccessHttpUrl(request, defaultValue);
|
||||
name = getFileAccessHttpUrl(request, name);
|
||||
}
|
||||
if(oConvertUtils.isNotEmpty(name)){
|
||||
//提示词 评估集变量值替换
|
||||
content = content.replaceAll(variable.getString("name"), name);
|
||||
}else if(oConvertUtils.isNotEmpty(defaultValue)){
|
||||
content = content.replaceAll(variable.getString("name"), defaultValue);
|
||||
}
|
||||
}
|
||||
airagPrompts.setContent(content);
|
||||
}
|
||||
|
||||
//提示词答案
|
||||
String promptAnswer = getPromptAnswer(airagPrompts, dataset, fieldMappings);
|
||||
result.put("promptAnswer", promptAnswer);
|
||||
|
||||
//评估器答案
|
||||
String answerScore = getAnswerScore(promptAnswer, dataset, fieldMappings, airagExtData);
|
||||
result.put("answerScore", answerScore);
|
||||
|
||||
scoreResult.add(result);
|
||||
log.info("第{}条数据处理完成", i + 1);
|
||||
} catch (Exception e) {
|
||||
log.error("处理第{}条数据时发生异常", i + 1, e);
|
||||
// 重新抛出异常,让CompletableFuture捕获
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, executor))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 非阻塞方式处理完成
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||
.whenComplete((result, ex) -> {
|
||||
if (ex != null) {
|
||||
log.error("批量处理失败", ex);
|
||||
// 更新状态为失败
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_FAILED);
|
||||
} else {
|
||||
log.info("所有数据处理完成,共处理{}条数据", scoreResult.size());
|
||||
// 查询已存在的评测记录
|
||||
List<AiragExtData> existingTracks = airagExtDataService.queryTrackById(dataId);
|
||||
Integer version = 1;
|
||||
if(!CollectionUtils.isEmpty(existingTracks)) {
|
||||
version = existingTracks.stream()
|
||||
.map(AiragExtData::getVersion)
|
||||
.max(Integer::compareTo)
|
||||
.orElse(0) + 1;
|
||||
}
|
||||
for (JSONObject item : scoreResult) {
|
||||
// 保存结果
|
||||
AiragExtData track = new AiragExtData();
|
||||
//关联评估器ID
|
||||
track.setMetadata(dataId);
|
||||
//定义类型
|
||||
track.setBizType(AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||
//定义版本
|
||||
track.setVersion(version);
|
||||
//定义状态
|
||||
track.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||
//定义评测结果
|
||||
track.setDataValue(item.toJSONString());
|
||||
airagExtDataService.save(track);
|
||||
}
|
||||
// 更新状态为完成
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||
}
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
});
|
||||
|
||||
//4.修改状态进行中
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_RUNNING);
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
|
||||
log.info("提示词实验已提交,共{}条数据待处理", datasetArray.size());
|
||||
return Result.OK("实验已开始,正在处理数据");
|
||||
} catch (Exception e) {
|
||||
log.error("提示词实验执行失败", e);
|
||||
return Result.error("实验执行失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提示词回答的结果
|
||||
* @param airagPrompts
|
||||
* @param questions
|
||||
* @param fieldMappings
|
||||
* @return
|
||||
*/
|
||||
public String getPromptAnswer(AiragPrompts airagPrompts, JSONObject questions, Map<String, String> fieldMappings) {
|
||||
try {
|
||||
//0.判断是否配置了判断fieldMappings的value值 是否包含actual_output
|
||||
if (!fieldMappings.containsValue("actual_output")) {
|
||||
log.warn("字段映射中没有配置actual_output");
|
||||
return null;
|
||||
}
|
||||
|
||||
//1.提示词
|
||||
String prompt = airagPrompts.getContent();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
|
||||
String userQuery = questions.getString(fieldMappings.get("user_query"));
|
||||
AssertUtils.assertNotEmpty("请输入测试内容", userQuery);
|
||||
|
||||
//2.ai问题组装
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||
|
||||
//3.模型数据
|
||||
String modelId = airagPrompts.getModelId();
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
//4.模型参数
|
||||
String modelParam = airagPrompts.getModelParam();
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject param = JSON.parseObject(modelParam);
|
||||
if(param.containsKey("temperature")){
|
||||
params.setTemperature(param.getDoubleValue("temperature"));
|
||||
}
|
||||
if(param.containsKey("topP")){
|
||||
params.setTopP(param.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(param.containsKey("presencePenalty")){
|
||||
params.setPresencePenalty(param.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(param.containsKey("frequencyPenalty")){
|
||||
params.setFrequencyPenalty(param.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("调用AI模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||
//5.AI问答
|
||||
String promptAnswer = aiChatHandler.completions(modelId, messages, params);
|
||||
log.debug("AI模型返回结果:{}", promptAnswer);
|
||||
|
||||
return promptAnswer;
|
||||
} catch (Exception e) {
|
||||
log.error("获取提示词回答失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评测答案分数
|
||||
* @return
|
||||
*/
|
||||
public String getAnswerScore(String promptAnswer, JSONObject questions, Map<String, String> fieldMappings, AiragExtData airagExtData) {
|
||||
try {
|
||||
//1.提示词
|
||||
String prompt = airagExtData.getDataValue();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
prompt += "定义返回格式: 得分:最终的得分,必须输出一个数字,表示满足Prompt中评分标准的程度。得分范围从 0.0 到 1.0,1.0 表示完全满足评分标准,0.0 表示完全不满足评分标准。\n" +
|
||||
"原因:{对得分的可读性的解释,说明打分原因}。最后,必须用一句话结束理由,该句话为:因此,应该给出的分数是<你评测的的得分>。请勿返回提问的问题、添加分析过程、解释说明等内容,只返回要求的格式内容";
|
||||
|
||||
String userQuery = "输入的内容:";
|
||||
//2.拼接测试内容
|
||||
for (Map.Entry<String, String> entry : fieldMappings.entrySet()) {
|
||||
// 评估器中的key
|
||||
String key = entry.getKey();
|
||||
// 评估器中的映射的key
|
||||
String value = entry.getValue();
|
||||
String valueData;
|
||||
if("actual_output".equalsIgnoreCase(value)){
|
||||
valueData = promptAnswer;
|
||||
}else{
|
||||
valueData = questions.getString(value);
|
||||
}
|
||||
userQuery += (key + ":" + valueData + " ");
|
||||
}
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||
|
||||
//3.模型数据
|
||||
String metadata = airagExtData.getMetadata();
|
||||
if(oConvertUtils.isNotEmpty(metadata)){
|
||||
JSONObject modelParam = JSONObject.parseObject(metadata);
|
||||
String modelId = modelParam.getString("modelId");
|
||||
AssertUtils.assertNotEmpty("评估器模型ID不能为空", modelId);
|
||||
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
if(modelParam.containsKey("temperature")){
|
||||
params.setTemperature(modelParam.getDoubleValue("temperature"));
|
||||
}
|
||||
if(modelParam.containsKey("topP")){
|
||||
params.setTopP(modelParam.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(modelParam.containsKey("presencePenalty")){
|
||||
params.setPresencePenalty(modelParam.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(modelParam.containsKey("frequencyPenalty")){
|
||||
params.setFrequencyPenalty(modelParam.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("调用评估器模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||
//5.AI问答
|
||||
String answerScore = aiChatHandler.completions(modelId, messages, params);
|
||||
log.debug("评估器模型返回结果:{}", answerScore);
|
||||
|
||||
return answerScore;
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("获取答案评分失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param columns
|
||||
* @param variable
|
||||
* @return
|
||||
*/
|
||||
public static String findDataType(JSONArray columns, JSONObject variable) {
|
||||
// 获取目标字段值
|
||||
String targetName = variable.getString("name");
|
||||
|
||||
// 使用 Stream API 查找并获取 dataType
|
||||
return columns.stream()
|
||||
.map(obj -> JSONObject.parseObject(obj.toString()))
|
||||
.filter(column -> targetName.equals(column.getString("name")))
|
||||
.findFirst()
|
||||
.map(column -> column.getString("dataType"))
|
||||
.orElse(null); // 如果没有找到,返回 null
|
||||
}
|
||||
/**
|
||||
* 获取图片地址
|
||||
* @param request
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
private String getFileAccessHttpUrl(HttpServletRequest request,String url){
|
||||
if(oConvertUtils.isNotEmpty(url) && url.startsWith("http")){
|
||||
return url;
|
||||
}else{
|
||||
return CommonUtils.getBaseUrl(request) + "/sys/common/static/" + url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package org.jeecg.modules.airag.prompts.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: AiragDebugVo
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AiragDebugVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String prompts;
|
||||
/**
|
||||
* 输入内容
|
||||
*/
|
||||
private String content;
|
||||
/**适配的大模型ID*/
|
||||
private String modelId;
|
||||
/**大模型的参数配置*/
|
||||
private String modelParam;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.prompts.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AiragExperimentVo
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AiragExperimentVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String promptKey;
|
||||
/**
|
||||
* 输入内容
|
||||
*/
|
||||
private String extDataId;
|
||||
/**
|
||||
* 映射关系
|
||||
*/
|
||||
private Map<String,String> mappings;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.wordtpl.consts;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author chenrui
|
||||
* @ClassName: TitleLevelEnum
|
||||
* @Description: 标题级别
|
||||
* @date 2024年5月4日07:38:30
|
||||
*/
|
||||
@Getter
|
||||
public enum WordTitleEnum {
|
||||
|
||||
FIRST("first", "标题1"),
|
||||
SECOND("second", "标题2"),
|
||||
THIRD("third", "标题3"),
|
||||
FOURTH("fourth", "标题4"),
|
||||
FIFTH("fifth", "标题5"),
|
||||
SIXTH("sixth", "标题6");
|
||||
|
||||
WordTitleEnum(String code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
final String code;
|
||||
|
||||
final String name;
|
||||
|
||||
}
|
||||
@ -0,0 +1,244 @@
|
||||
package org.jeecg.modules.airag.wordtpl.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name = "word模版管理")
|
||||
@RestController("eoaWordTemplateController")
|
||||
@RequestMapping("/airag/word")
|
||||
@Slf4j
|
||||
public class EoaWordTemplateController extends JeecgController<EoaWordTemplate, IEoaWordTemplateService> {
|
||||
@Autowired
|
||||
private IEoaWordTemplateService eoaWordTemplateService;
|
||||
|
||||
@Autowired
|
||||
WordTplUtils wordTplUtils;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "word模版管理-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<EoaWordTemplate>> queryPageList(EoaWordTemplate eoaWordTemplate,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<EoaWordTemplate> queryWrapper = QueryGenerator.initQueryWrapper(eoaWordTemplate, req.getParameterMap());
|
||||
Page<EoaWordTemplate> page = new Page<EoaWordTemplate>(pageNo, pageSize);
|
||||
IPage<EoaWordTemplate> pageList = eoaWordTemplateService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-添加")
|
||||
@Operation(summary = "word模版管理-添加")
|
||||
// @RequiresPermissions("wordtpl:template:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||
boolean isCodeExists = eoaWordTemplateService.exists(Wrappers.lambdaQuery(EoaWordTemplate.class).eq(EoaWordTemplate::getCode, eoaWordTemplate.getCode()));
|
||||
AssertUtils.assertFalse("模版编码已存在", isCodeExists);
|
||||
eoaWordTemplateService.save(eoaWordTemplate);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-编辑")
|
||||
@Operation(summary = "word模版管理-编辑")
|
||||
// @RequiresPermissions("wordtpl:template:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||
// 避免编辑时修改编码
|
||||
eoaWordTemplate.setCode(null);
|
||||
eoaWordTemplateService.updateById(eoaWordTemplate);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-通过id删除")
|
||||
@Operation(summary = "word模版管理-通过id删除")
|
||||
// @RequiresPermissions("wordtpl:template:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
eoaWordTemplateService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-批量删除")
|
||||
@Operation(summary = "word模版管理-批量删除")
|
||||
// @RequiresPermissions("wordtpl:template:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.eoaWordTemplateService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "word模版管理-通过id查询")
|
||||
@Operation(summary = "word模版管理-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<EoaWordTemplate> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
EoaWordTemplate eoaWordTemplate = eoaWordTemplateService.getById(id);
|
||||
if (eoaWordTemplate == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(eoaWordTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载word模版
|
||||
* @param id
|
||||
* @param response
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/9 14:38
|
||||
*/
|
||||
@GetMapping(value = "/download")
|
||||
public void downloadTemplate(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
|
||||
AssertUtils.assertNotEmpty("请先选择模版", id);
|
||||
EoaWordTemplate template = eoaWordTemplateService.getById(id);
|
||||
try (ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||
String fileName = template.getName();
|
||||
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||
response.addHeader("filename", encodedFileName + ".docx");
|
||||
byte[] bytes = wordTemplateOut.toByteArray();
|
||||
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||
bos.write(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("下载word模版失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析word模版文件
|
||||
* @param file
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/9 14:38
|
||||
*/
|
||||
@PostMapping(value = "/parse/file")
|
||||
public Result<?> parseWOrdFile(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
InputStream inputStream = file.getInputStream();
|
||||
EoaWordTemplate eoaWordTemplate = wordTplUtils.parseWordFile(inputStream);
|
||||
log.info("解析的模版信息: {}", eoaWordTemplate);
|
||||
return Result.OK("解析成功", eoaWordTemplate);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析word模版失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成word文档
|
||||
*
|
||||
* @param wordTplGenDTO
|
||||
* @param response
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 15:39
|
||||
*/
|
||||
@PostMapping(value = "/generate/word")
|
||||
public void generateWord(@RequestBody WordTplGenDTO wordTplGenDTO, HttpServletResponse response) {
|
||||
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||
EoaWordTemplate template ;
|
||||
if (oConvertUtils.isNotEmpty(wordTplGenDTO.getTemplateId())) {
|
||||
template = eoaWordTemplateService.getById(wordTplGenDTO.getTemplateId());
|
||||
}else{
|
||||
AssertUtils.assertNotEmpty("请先选择模版", wordTplGenDTO.getTemplateCode());
|
||||
template = eoaWordTemplateService.getOne(Wrappers.lambdaQuery(EoaWordTemplate.class)
|
||||
.eq(EoaWordTemplate::getCode, wordTplGenDTO.getTemplateCode()));
|
||||
}
|
||||
AssertUtils.assertNotEmpty("未找到对应的模版", template);
|
||||
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||
eoaWordTemplateService.generateWordFromTpl(wordTplGenDTO, outputStream);
|
||||
String fileName = template.getName();
|
||||
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||
response.addHeader("filename", encodedFileName + ".docx");
|
||||
byte[] bytes = outputStream.toByteArray();
|
||||
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||
bos.write(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("生成word文档失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 合并列DTO
|
||||
* @author chenrui
|
||||
* @date 2025/7/4 18:36
|
||||
*/
|
||||
@Data
|
||||
public class MergeColDTO {
|
||||
|
||||
/**
|
||||
* 合并列的行号
|
||||
*/
|
||||
private int row;
|
||||
|
||||
/**
|
||||
* 合并列的起始列号
|
||||
*/
|
||||
private int from;
|
||||
|
||||
/**
|
||||
* 合并列的结束列号
|
||||
*/
|
||||
private int to;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @ClassName: DocImageDto
|
||||
* @Description: word文档图片用实体类
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:17:59
|
||||
*/
|
||||
@Data
|
||||
public class WordImageDTO {
|
||||
|
||||
/**
|
||||
* @Fields type : 类型
|
||||
* @author chenrui
|
||||
* @date 2024-09-29 08:53:27
|
||||
*/
|
||||
private String type = "image";
|
||||
/**
|
||||
* @Fields value : 内容
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:12
|
||||
*/
|
||||
private String value = "";
|
||||
|
||||
/**
|
||||
* @Fields width : 图片宽度
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:22:33
|
||||
*/
|
||||
private double width;
|
||||
|
||||
/**
|
||||
* @Fields height : 图片高度
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:22:40
|
||||
*/
|
||||
private double height;
|
||||
|
||||
/**
|
||||
* @Fields rowFlex : 水平对齐方式,默认left
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:12:18
|
||||
*/
|
||||
private String rowFlex = "left";
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableCellDTO {
|
||||
|
||||
/**
|
||||
* @Fields colspan : 合并列数
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:37:27
|
||||
*/
|
||||
private int colspan;
|
||||
|
||||
/**
|
||||
* @Fields rowspan : 合并行数
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:38:22
|
||||
*/
|
||||
private int rowspan;
|
||||
|
||||
/**
|
||||
* @Fields value : 单元格数据
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:42:14
|
||||
*/
|
||||
private List<Object> value = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* @Fields verticalAlign : 垂直对齐方式,默认top
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:16:56
|
||||
*/
|
||||
private String verticalAlign = "top";
|
||||
|
||||
/**
|
||||
* @Fields backgroundColor : 背景颜色
|
||||
* @author chenrui
|
||||
* @date 2024-11-18 09:56:28
|
||||
*/
|
||||
private String backgroundColor;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableDTO {
|
||||
|
||||
private String value = "";
|
||||
|
||||
private String type = "table";
|
||||
|
||||
private List<WordTableRowDTO> trList;
|
||||
|
||||
private int width;
|
||||
|
||||
private int height;
|
||||
|
||||
private List<JSONObject> colgroup = new ArrayList<>();
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableRowDTO {
|
||||
|
||||
/**
|
||||
* @Fields height : 行高
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:45:30
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* @Fields minHeight : 行最小高度
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:47:28
|
||||
*/
|
||||
private int minHeight = 42;
|
||||
|
||||
/**
|
||||
* @Fields tdList : 行数据
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:46:02
|
||||
*/
|
||||
private List<WordTableCellDTO> tdList;
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author chenrui
|
||||
* @ClassName: DocTextDto
|
||||
* @Description: word文本实体类
|
||||
* @date 2024-09-24 10:19:57
|
||||
*/
|
||||
@Data
|
||||
public class WordTextDTO {
|
||||
|
||||
/**
|
||||
* @Fields type : 类型
|
||||
* @author chenrui
|
||||
* @date 2024-09-29 08:53:27
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* @Fields value : 内容
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:12
|
||||
*/
|
||||
private String value = "";
|
||||
|
||||
/**
|
||||
* @Fields bold : 是否加粗 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:33
|
||||
*/
|
||||
private boolean bold = false;
|
||||
|
||||
/**
|
||||
* @Fields color : 字体颜色
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:08
|
||||
*/
|
||||
private String color;
|
||||
|
||||
/**
|
||||
* @Fields italic : 是否斜体 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:25
|
||||
*/
|
||||
private boolean italic = false;
|
||||
|
||||
/**
|
||||
* @Fields underline : 是否下划线 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:47
|
||||
*/
|
||||
private boolean underline = false;
|
||||
|
||||
/**
|
||||
* @Fields strikeout : 删除线 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:22:06
|
||||
*/
|
||||
private boolean strikeout = false;
|
||||
|
||||
/**
|
||||
* @Fields size : 字号大小
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:44:42
|
||||
*/
|
||||
private int size;
|
||||
|
||||
/**
|
||||
* @Fields font : 字体,默认微软雅黑
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:45:31
|
||||
*/
|
||||
private String font = "微软雅黑";
|
||||
|
||||
/**
|
||||
* @Fields highlight : 高亮颜色
|
||||
* @author chenrui
|
||||
* @date 2024-09-25 11:20:23
|
||||
*/
|
||||
private String highlight;
|
||||
|
||||
/**
|
||||
* @Fields rowFlex : 水平对齐方式,默认left
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:12:18
|
||||
*/
|
||||
private String rowFlex = "left";
|
||||
|
||||
private List<Object> dashArray = new ArrayList<>();
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* word模版生成入参
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 14:38
|
||||
*/
|
||||
@Data
|
||||
public class WordTplGenDTO {
|
||||
|
||||
/**
|
||||
* 模版id
|
||||
*/
|
||||
String templateId;
|
||||
|
||||
|
||||
/**
|
||||
* 模版code
|
||||
*/
|
||||
String templateCode;
|
||||
|
||||
/**
|
||||
* 数据
|
||||
*/
|
||||
Map<String,Object> data;
|
||||
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
package org.jeecg.modules.airag.wordtpl.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("aigc_word_template")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "word模版管理")
|
||||
public class EoaWordTemplate implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private Date createTime;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
/**
|
||||
* 模版名称
|
||||
*/
|
||||
@Excel(name = "模版名称", width = 15)
|
||||
@Schema(description = "模版名称")
|
||||
private String name;
|
||||
/**
|
||||
* 模版编码
|
||||
*/
|
||||
@Excel(name = "模版编码", width = 15)
|
||||
@Schema(description = "模版编码")
|
||||
private String code;
|
||||
/**
|
||||
* 页眉
|
||||
*/
|
||||
@Excel(name = "页眉", width = 15)
|
||||
@Schema(description = "页眉")
|
||||
private String header;
|
||||
/**
|
||||
* 页脚
|
||||
*/
|
||||
@Excel(name = "页脚", width = 15)
|
||||
@Schema(description = "页脚")
|
||||
private String footer;
|
||||
/**
|
||||
* 主体内容
|
||||
*/
|
||||
@Excel(name = "主体内容", width = 15)
|
||||
@Schema(description = "主体内容")
|
||||
private String main;
|
||||
/**
|
||||
* 页边距
|
||||
*/
|
||||
@Excel(name = "页边距", width = 15)
|
||||
@Schema(description = "页边距")
|
||||
private String margins;
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
@Excel(name = "宽度", width = 15)
|
||||
@Schema(description = "宽度")
|
||||
private Integer width;
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
@Excel(name = "高度", width = 15)
|
||||
@Schema(description = "高度")
|
||||
private Integer height;
|
||||
/**
|
||||
* 纸张方向 vertical纵向 horizontal横向
|
||||
*/
|
||||
@Excel(name = "纸张方向 vertical纵向 horizontal横向", width = 15)
|
||||
@Schema(description = "纸张方向 vertical纵向 horizontal横向")
|
||||
private String paperDirection;
|
||||
/**
|
||||
* 水印
|
||||
*/
|
||||
@Excel(name = "水印", width = 15)
|
||||
@Schema(description = "水印")
|
||||
private String watermark;
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package org.jeecg.modules.airag.wordtpl.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface EoaWordTemplateMapper extends BaseMapper<EoaWordTemplate> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper">
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,26 @@
|
||||
package org.jeecg.modules.airag.wordtpl.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IEoaWordTemplateService extends IService<EoaWordTemplate> {
|
||||
|
||||
/**
|
||||
* 通过模版生成word文档
|
||||
*
|
||||
* @param wordTplGenDTO
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 14:40
|
||||
*/
|
||||
void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream);
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package org.jeecg.modules.airag.wordtpl.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
import org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper;
|
||||
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("eoaWordTemplateService")
|
||||
public class EoaWordTemplateServiceImpl extends ServiceImpl<EoaWordTemplateMapper, EoaWordTemplate> implements IEoaWordTemplateService {
|
||||
|
||||
/**
|
||||
* 内置的系统变量键列表
|
||||
*/
|
||||
private static final String[] SYSTEM_KEYS = {
|
||||
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
|
||||
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
|
||||
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
|
||||
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
|
||||
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
|
||||
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
|
||||
DataBaseConstant.SYS_BASE_PATH
|
||||
};
|
||||
|
||||
@Autowired
|
||||
WordTplUtils wordTplUtils;
|
||||
|
||||
@Override
|
||||
public void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream) {
|
||||
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||
AssertUtils.assertNotEmpty("模版ID不能为空", wordTplGenDTO.getTemplateId());
|
||||
String templateId = wordTplGenDTO.getTemplateId();
|
||||
// 生成word模版 date:2025/7/10
|
||||
EoaWordTemplate template = getById(templateId);
|
||||
ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||
//根据word模版和数据生成word文件
|
||||
Map<String, Object> data = wordTplGenDTO.getData();
|
||||
mergeSystemVarsToData(data);
|
||||
try {
|
||||
XWPFTemplate.compile(new ByteArrayInputStream(wordTemplateOut.toByteArray())).render(data).write(wordOutputStream);
|
||||
}catch (Exception e){
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("生成word文档失败,请检查模版和数据是否正确");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将系统变量合并到数据中
|
||||
*
|
||||
* @param data
|
||||
* @author chenrui
|
||||
* @date 2025/7/3 17:43
|
||||
*/
|
||||
private static void mergeSystemVarsToData(Map<String, Object> data) {
|
||||
for (String key : SYSTEM_KEYS) {
|
||||
if (!data.containsKey(key)) {
|
||||
String value = JwtUtil.getUserSystemData(key, null);
|
||||
if (value != null) {
|
||||
data.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<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>
|
||||
|
||||
|
||||
@ -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 填 \"测试用户\""
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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("数据重置成功");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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 为 行的id(rowId),只要获取到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 为 行的id(rowId),只要获取到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 为 行的id(rowId),只要获取到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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
Reference in New Issue
Block a user