mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-01-24 12:06:52 +08:00
Compare commits
7 Commits
main
...
main-jeecg
| Author | SHA1 | Date | |
|---|---|---|---|
| a466dcabe1 | |||
| 17d609cf34 | |||
| bfcd2797f1 | |||
| e112f4eea3 | |||
| 3b36f1909a | |||
| 5e5e11f952 | |||
| d6051addeb |
12
LICENSE
12
LICENSE
@ -200,14 +200,4 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
|
||||
|
||||
JeecgBoot 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京,地址:中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱:jeecgos@163.com
|
||||
本软件受适用的国家软件著作权法(包括国际条约)和开源协议 双重保护许可。
|
||||
|
||||
开源协议中文释意如下:
|
||||
1.JeecgBoot开源版本无任何限制,在遵循本开源协议条款下,允许商用使用,不会造成侵权行为。
|
||||
2.允许基于本平台软件开展业务系统开发。
|
||||
3.在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件。
|
||||
|
||||
最终解释权归:http://www.jeecg.com
|
||||
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
|
||||
178
README-AI.md
178
README-AI.md
@ -1,178 +0,0 @@
|
||||
AIGC应用平台介绍
|
||||
===============
|
||||
|
||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||
|
||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||
|
||||
|
||||
|
||||
### AI视频介绍
|
||||
|
||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||
|
||||
|
||||
##### 功能大模块
|
||||
|
||||
- AI应用开发平台
|
||||
- AI知识库系统
|
||||
- AI大模型管理
|
||||
- AI流程编排
|
||||
- AI对话支持图片
|
||||
- AI对话助手(智能问答)
|
||||
- AI建表(Online表单)
|
||||
- AI写文章(CMS)
|
||||
- AI表单字段建议(表单设计器)
|
||||
|
||||
|
||||
|
||||
#### Dify `VS` JEECG AI
|
||||
|
||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
||||
> - Markdown文档库导入:
|
||||
> JEECG AI允许用户直接导入整个Markdown文档库,这不仅保留markdown格式,还支持图片的导入,确保文档内容的完整性和可视化效果。
|
||||
> - 对话回复格式美观:
|
||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
||||
> - PDF文档导入与格式转换:
|
||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||
|
||||
|
||||
| 功能 | Dify | Jeecg AI |
|
||||
|------------|------------------|-----------------------------------------|
|
||||
| AI工作流 | 有 | 有 |
|
||||
| RAG 管道向量搜索 | 有 | 有 |
|
||||
| AI模型管理 | 有 | 有 |
|
||||
| AI应用管理 | 有 | 有 |
|
||||
| AI知识库 | 有 | 有 |
|
||||
| 产品方向 | 一款独立的 LLM 应用开发平台 | 低代码与AIGC应用二者结合的平台 |
|
||||
| 业务集成 | 业务集成能力弱 | 更方便与业务系统集成,调用系统接口和逻辑更加方便 |
|
||||
| AI业务流 | 侧重AI逻辑流程 | AI流程编排作为低代码的业务引擎,用户可以通过AI流程配置各种业务流和AI流程 |
|
||||
| 实现语言 | python + react | JAVA + vue3 |
|
||||
| 上传markdown文档库(支持图片) | 不支持 | 支持 |
|
||||
| AI对话支持发图和展示图片 | 支持 | 支持 |
|
||||
|
||||
|
||||
|
||||
### 技术文档
|
||||
|
||||
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
||||
- [安装向量库 pgvector](https://help.jeecg.com/aigc/config)
|
||||
|
||||
|
||||
|
||||
## 功能特点
|
||||
|
||||
- AI流程: 提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
||||
- AI流程即服务: 通过AI流程编排你需要的智能体,结合AI+自定义开发节点 实现功能性 API,让你瞬间拥有各种智能体API。
|
||||
- AI助手对话功能: 集成 ChatGPT、Deepseek、智普、私有大模型 等 AI 模型,提供智能对话和生成式 AI 功能,深度与知识库结合提供更精准的知识。
|
||||
- RAG 功能: 涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本,支持检索增强生成(RAG),将未训练数据与 AI 模型集成,提升智能交互能力。
|
||||
- AI 知识库: 通过导入文档或已有问答对进行训练,让 AI 模型能根据文档以交互式对话方式回答问题。
|
||||
- 模型管理:支持对接各种大模型,包括本地私有大模型(Deepseek/ Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等)和国外公共大模型(OpenAI / Claude / Gemini 等);
|
||||
- 无缝嵌入:Iframe一键嵌入,支持将AI聊天助手快速嵌入到第三方系统,让系统快速拥有智能问答能力,提高用户满意度。
|
||||
|
||||
|
||||
|
||||
|
||||
#### 在线体验
|
||||
|
||||
- JeecgBoot演示: https://boot3.jeecg.com
|
||||
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
||||
|
||||
|
||||
## 技术交流
|
||||
|
||||
- 开发文档:https://help.jeecg.com/aigc
|
||||
- QQ群:964611995、716488839(满)
|
||||
|
||||
|
||||
## 功能列表
|
||||
|
||||
- AI应用管理(普通应用、高级流程应用)
|
||||
- AI模型管理
|
||||
- AI知识库
|
||||
- AI应用平台(普通、对接AI流程)
|
||||
- AI流程编排
|
||||
- AI聊天支持嵌入第三方
|
||||
- AI向量库对接
|
||||
|
||||
|
||||
|
||||
## 支持AI模型
|
||||
|
||||
| AI大模型 | 支持 |
|
||||
|---------------| --- |
|
||||
| DeepSeek | √ |
|
||||
| ChatGTP | √ |
|
||||
| Qwq | √ |
|
||||
| 智库 | √ |
|
||||
| claude | √ |
|
||||
| vl模型 | √ |
|
||||
| 千帆大模型 | √ |
|
||||
| 通义千问 | √ |
|
||||
| Ollama本地搭建大模型 | √ |
|
||||
| 等等。。 | √ |
|
||||
|
||||
|
||||
|
||||
|
||||
## AIGC能做什么
|
||||
|
||||
AIGC模块是一个基于AI的自动化流程编排工具和聊天应用搭建平台,它可以帮助用户快速生成AI流程接口和聊天应用,提高效率。
|
||||
以下是一些具体的应用场景和示例:
|
||||
|
||||
- 你可能需要一个翻译接口,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个接口转换工具,可以通过AI流程编排搭建出来。(比如:jimureport所需要接口返回格式与你的系统不同,你通过AI接口实现自动转换)
|
||||
- 你可能需要一个聊天机器人,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个自动化流程,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个自动化处理文件的流程,可以通过AI流程结合python脚本实现操作电脑,文件等。
|
||||
|
||||
|
||||
## AI应用平台功能展示
|
||||
|
||||
AI模型列表
|
||||
|
||||

|
||||
|
||||
选择AI模型,配置你的参数
|
||||
|
||||

|
||||
|
||||
|
||||
AI知识库支持手工录入文本,导入pdf\\word\\excel等文档,支持问答对训练
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
AI流程,提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
||||
|
||||

|
||||
|
||||
|
||||
目前支持的节点有:开始、结束、AI知识库节点、AI节点、分类节点、分支节点、JAVA节点、脚本节点、子流程节点、http请求节点、直接回复节点等节点
|
||||
|
||||

|
||||
|
||||
节点项配置
|
||||
|
||||

|
||||
|
||||
在线运行看结果
|
||||
|
||||

|
||||
|
||||
|
||||
AI应用配置,支持AI流程配置和简单的AI配置
|
||||
|
||||

|
||||
|
||||
可以关联多个知识库,右侧是AI智能回复,你可以搭建自己的智能体,比如搭建一个 “诗词达人” “翻译助手”
|
||||
|
||||

|
||||
|
||||
可以将创建的聊天应用,集成到第三方系统中
|
||||
|
||||

|
||||
424
README.en-US.md
424
README.en-US.md
@ -1,424 +0,0 @@
|
||||
[中文](./README.md) | English
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
JEECG BOOT AI Low Code Platform
|
||||
===============
|
||||
|
||||
Current version: 3.9.1 (Release date: 2026-01-22)
|
||||
|
||||
|
||||
[](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)
|
||||
|
||||
|
||||
|
||||
Project introduction
|
||||
-----------------------------------
|
||||
|
||||
<h3 align="center">Java AI Low Code Platform</h3>
|
||||
|
||||
JeecgBoot is a `AI low code platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
|
||||
|
||||
JeecgBoot provides a series of low code modules to make Online development truly zero code: Online form development, online reports, report configuration capabilities, online chart design, large screen design, mobile configuration capabilities, form designer, online design flow, process automation configuration, plug-in capabilities (pluggable) and more!
|
||||
|
||||
|
||||
The purpose of JEECG is: simple functions are implemented by OnlineCoding configuration, so that zero code development; Complex functions are generated by code generator and manually Merge to achieve low code development, which ensures both intelligence and flexibility. The implementation of low code development and support flexible coding at the same time, to solve the current low code products are generally not flexible drawbacks!
|
||||
|
||||
JEECG Business process: Using workflow to implement and extend the task interface for developing and writing business logic, forms provides a variety of solutions: form designer, online configuration form, and coding form. At the same time, the separation design of process and form (loose coupling) is realized, and the flexible configuration of task nodes is supported, which not only ensures the confidentiality of the company's process, but also reduces the workload of developers.
|
||||
|
||||
AI Empowering Low-Code: Currently, JeecgBoot supports AI large models such as ChatGPT and DeepSeek. The latest version defaults to using DeepSeek, which offers faster speed and higher quality. It now provides features such as AI chat assistant, AI table creation, and AI report generation.
|
||||
|
||||
Technical support
|
||||
-----------------------------------
|
||||
|
||||
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
|
||||
|
||||
|
||||
##### Project description
|
||||
|
||||
| Project | description |
|
||||
|--------------------|------------------------|
|
||||
| `jeecg-boot` | SpringBoot background source code (support microservices) |
|
||||
| `jeecgboot-vue3` | Vue3+TS new front-end source code|
|
||||
| `jeecg-uniapp` | [APP development framework, a code multi terminal adaptation, and support APP, small program, H5](https://github.com/jeecgboot/jeecg-uniapp) |
|
||||
|
||||
|
||||
### Video Introduction
|
||||
|
||||
[](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
|
||||
|
||||
|
||||
|
||||
Download other source code
|
||||
-----------------------------------
|
||||
- APP SourceCode:https://github.com/jeecgboot/jeecg-uniapp
|
||||
|
||||
|
||||
|
||||
For the project
|
||||
-----------------------------------
|
||||
Jeecg-Boot AI low code platform can be applied in the development of any J2EE project, especially for SAAS projects, enterprise information management system (MIS), internal office system (OA), enterprise resource planning system (ERP), customer relationship management system (CRM), etc. Its semi-intelligent manual Merge development method, Can significantly improve the development efficiency of more than 70%, greatly reduce the development cost.
|
||||
|
||||
|
||||
Starts the project
|
||||
-----------------------------------
|
||||
|
||||
> Default account password: admin/123456
|
||||
|
||||
- [Development Environment setup](https://help.jeecg.com/java/setup/tools)
|
||||
- [IDEA Quick start(single model)](https://help.jeecg.com/java/setup/idea/startup)
|
||||
- [Docker Quick start(single model)](https://help.jeecg.com/java/docker/quick)
|
||||
- [IDEA Quick start(microservices model)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||
- [Docker Quick start(microservices model)](https://help.jeecg.com/java/docker/quickcloud)
|
||||
|
||||
|
||||
Technical documentation
|
||||
-----------------------------------
|
||||
|
||||
- Website: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- Demo : [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
|
||||
- Doc: [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat)
|
||||
- Newbie guide: [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
|
||||
- QQ group : 964611995、⑩716488839(满)、⑨808791225(满)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Star charts
|
||||
-----------------------------------
|
||||
|
||||
[](https://star-history.com/#jeecgboot/jeecg-boot)
|
||||
|
||||
|
||||
|
||||
|
||||
Background directory Structure
|
||||
-----------------------------------
|
||||
```
|
||||
project structure
|
||||
├─jeecg-boot-parent
|
||||
│ ├─jeecg-boot-base-core
|
||||
│ ├─jeecg-module-demo
|
||||
│ ├─jeecg-module-system
|
||||
│ │ ├─jeecg-system-biz
|
||||
│ │ ├─jeecg-system-start system (8080)
|
||||
│ │ ├─jeecg-system-api
|
||||
│ │ │ ├─jeecg-system-cloud-api
|
||||
│ │ │ ├─jeecg-system-local-api
|
||||
│ ├─jeecg-server-cloud
|
||||
├─jeecg-cloud-gateway (9999)
|
||||
├─jeecg-cloud-nacos --Nacos(8848)
|
||||
├─jeecg-system-cloud-start --System(7001)
|
||||
├─jeecg-demo-cloud-start --Demo(7002)
|
||||
├─jeecg-visual
|
||||
├─jeecg-cloud-monitor -- (9111)
|
||||
├─jeecg-cloud-xxljob -- (9080)
|
||||
├─jeecg-cloud-sentinel --sentinel (9000)
|
||||
├─jeecg-cloud-test
|
||||
├─jeecg-cloud-test-more
|
||||
├─jeecg-cloud-test-rabbitmq
|
||||
├─jeecg-cloud-test-seata
|
||||
├─jeecg-cloud-test-shardingsphere
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
Why JeecgBoot?
|
||||
-----------------------------------
|
||||
* Adopt the latest mainstream front and back separation framework (Springboot+Mybatis+antd), easy to use; Code generator has low dependency, flexible expansion ability, and can quickly realize secondary development;
|
||||
* Support microservices SpringCloud Alibaba(Nacos, Gateway, Sentinel, Skywalking), and provide switching mechanism to support free switching between single and microservices
|
||||
* High development efficiency, using code generator, single table, tree list, one-to-many, one-to-one and other data models, add, delete, change and search function one-key generation, menu configuration directly use;
|
||||
* Code generator provides powerful template mechanism, support custom template, currently provide four sets of style template (single table two sets, tree model one set, one to many three sets)
|
||||
* Code generator is very intelligent, online business modeling, online configuration, WYSIWYG support 23 kinds of controls, a key to generate front and back end code, greatly improve the development efficiency, no longer worry about repeated work.
|
||||
* Low code ability: Online online form (without coding, through online configuration of the form, to achieve the addition, deletion, change and check of the form, support single table, tree, one-to-many, one-to-one model, to achieve everyone can code)
|
||||
* Low code ability: Online online report (without coding, through online configuration, to achieve data report, can quickly extract data, reduce development pressure, to achieve everyone can code)
|
||||
* Low code ability: Online online chart (without coding, through online configuration, to achieve graphs, bar graphs, data reports, etc., support custom layout, to achieve everyone can code)
|
||||
* Complete encapsulation of user, role, menu, organization, data dictionary, online scheduled tasks and other basic functions, support access authorization, button permission, data permission and other functions
|
||||
* Commonly used common package, various tools (scheduled task, SMS interface, email sending,Excel import and export, etc.), basically meeting 80% of project requirements
|
||||
* Easy Excel import and export, support single table export and one-to-many table mode export, generated code with import and export function
|
||||
* Integrated simple report tools, image report and data export is very convenient, can be extremely convenient to generate graphical reports, pdf, excel, word and other reports;
|
||||
* Before and after the separation technology, the page UI style is exquisite, for the commonly used components to do the encapsulation: time, row table control, interception display control, report component, editor and so on
|
||||
* Query filter: query function automatically generated, the background dynamic spell SQL additional query conditions; Supports multiple matching modes (full matching, fuzzy query, included query, and unmatched query).
|
||||
* Data permission (fine data permission control, control to row level, list level, form field level, realize different people see different data, different people operate different fields on the same page
|
||||
* Page verification automatically generated (must be input, digital verification, amount verification, time and space, etc.);
|
||||
* Support SAAS service model and provide SaaS multi-tenant architecture solution.
|
||||
* Distributed file service, integration of minio, Ali OSS and other excellent third parties, to provide convenient file upload and management, but also support local storage.
|
||||
* Mainstream database compatibility, a set of code is fully compatible with Mysql, Postgresql, Oracle, Sqlserver, MariaDB, dream and other mainstream databases.
|
||||
* Integrate workflow flowable and realize only the configuration of flow direction in the page, which can greatly simplify the development of bpm workflow; Using bpm's process designer to draw the flow direction, a workflow is basically complete with a small amount of java code;
|
||||
* Low code ability: online process design, using open source Activiti process engine, to achieve online drawing process, custom form, form attachment, business flow
|
||||
* Multi-data source: its simple way of use, online configuration of data source configuration, convenient to grab data from other data;
|
||||
* Provide single sign-on CAS integration solution, and complete docking code has been provided in the project
|
||||
* Low code ability: form designer, support user custom form layout, support single table, one to many forms, support select, radio, checkbox, textarea, date, popup, list, macro and other controls
|
||||
* Professional interface docking mechanism, unified using restful interface, integrated swagger-ui online interface documentation, Jwt token security verification, convenient client docking
|
||||
* Interface security mechanism, can be refined control interface authorization, very simple to realize different clients only see their own data control
|
||||
* Advanced combination query function, online configuration support primary and sub-table associated query, can save the query history
|
||||
* Provide a variety of system monitoring, real-time tracking system running conditions (monitoring Redis, Tomcat, jvm, server information, request tracking, SQL monitoring)
|
||||
* Message center (support SMS, email, wechat push, etc.)
|
||||
* Integrate Websocket message notification mechanism
|
||||
* Excellent mobile adaptive effect, providing APP release scheme:
|
||||
* Support multiple languages and provide internationalization solutions;
|
||||
* Data change record log, can record each change of data content, through the version comparison function to view historical changes
|
||||
* The platform UI is powerful and mobile adaptation is implemented
|
||||
* Platform home page style, provide a variety of combination mode, support custom style
|
||||
* Provide easy to use print plug-in, support Google, Firefox, IE11+ and other browsers
|
||||
* Rich sample code, provide a lot of learning case reference
|
||||
* Using maven module development method
|
||||
* Support dynamic menu routing
|
||||
* RBAC (Role-Based Access Control) is used for permission control.
|
||||
* Provide new row edit table JVXETable, easily meet a variety of complex ERP layout, with higher performance, more flexible extension, more powerful functions
|
||||
|
||||
|
||||
|
||||
|
||||
Technical Architecture:
|
||||
-----------------------------------
|
||||
|
||||
#### Development Environment
|
||||
|
||||
- Language: Java Default Jdk17(support jdk8、jdk21)
|
||||
|
||||
- IDE(JAVA) : IDEA (lombok plug-in must be installed)
|
||||
|
||||
- IDE(front-end) : Vscode, WebStorm, IDEA
|
||||
|
||||
- Dependency management: Maven
|
||||
|
||||
- Cache: Redis
|
||||
|
||||
- Database: MySQL5.7 + [More Databases](https://my.oschina.net/jeecg/blog/4905722)
|
||||
|
||||
|
||||
#### backend
|
||||
|
||||
- Basic framework: Spring Boot 2.7.18
|
||||
|
||||
- Microservice framework: Spring Cloud Alibaba 2021.0.6.2
|
||||
|
||||
- Persistence layer framework: MybatisPlus 3.5.3.2
|
||||
|
||||
- Report tool: JimuReport 1.9.5
|
||||
|
||||
- Security framework: Apache Shiro 1.13.0, Jwt 4.5.0
|
||||
|
||||
- Microservice technology stack: Spring Cloud Alibaba, Nacos, Gateway, Sentinel, Skywalking
|
||||
|
||||
- Database connection pool: Alibaba Druid 1.1.24
|
||||
|
||||
- Log printing: logback
|
||||
|
||||
- Others: autopoi, fastjson, poi, Swagger-ui, quartz, lombok (simplified code), etc.
|
||||
|
||||
|
||||
#### The front end
|
||||
|
||||
- TechnologyStack:`Vue3.0+TypeScript+Vite+AntDesignVue+pinia+echarts`
|
||||
|
||||
#### Front-end environment requirements
|
||||
|
||||
* `Node.js 、npm 、pnpm`
|
||||
* pnpm `v9+` is now required.
|
||||
* Node.js Version suggestion: `v20.15.0`
|
||||
` ( Since Vite6 Node.js 18/20 + is now required )`
|
||||
|
||||
|
||||
#### Support library
|
||||
|
||||
| database | support |
|
||||
| --- | --- |
|
||||
| MySQL | √ |
|
||||
| Oracle11g | √ |
|
||||
| Sqlserver2017 | √ |
|
||||
| PostgreSQL | √ |
|
||||
| MariaDB | √ |
|
||||
| 达梦 | √ |
|
||||
| 人大金仓 | √ |
|
||||
| TiDB | √ |
|
||||
|
||||
|
||||
#### AI Support
|
||||
|
||||
| AI Model | Supported |
|
||||
| --- | --- |
|
||||
| DeepSeek | √ |
|
||||
| ChatGPT | √ |
|
||||
| Qwq | √ |
|
||||
| 智库 | √ |
|
||||
| Ollama本地搭建大模型 | √ |
|
||||
| 等等。。 | √ |
|
||||
|
||||
|
||||
AI Config: https://help.jeecg.com/java/ai/aichat
|
||||
|
||||
AI APP: https://help.jeecg.com/aigc
|
||||
|
||||
|
||||
## Microservice solutions
|
||||
|
||||
- 1. Service registration and discovery Nacos √
|
||||
- 2. Nacos √
|
||||
- 3. Route gateway gateway(Three loading modes) √
|
||||
- 4. Distributed http feign √
|
||||
- 5. fuse degrade current limiting Sentinel √
|
||||
- 6. Distributed files Minio and Alioss √
|
||||
- 7. Unified permission control
|
||||
- 8. Service monitoring SpringBootAdmin√
|
||||
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking)
|
||||
- 10. Messaging middleware RabbitMQ √
|
||||
- 11. Distributed task xxl-job √
|
||||
- 12. Distributed Transaction Seata
|
||||
- 13. Distributed log Loki+grafana
|
||||
- 14. Support docker-compose, k8s, jenkins
|
||||
- 15. CAS SSO √
|
||||
- 16. Route traffic limiting √
|
||||
|
||||
|
||||
#### Microservice architecture diagram
|
||||

|
||||
|
||||
### Jeecg Boot product functionality blueprint
|
||||

|
||||
|
||||
### quick start
|
||||
- Microservice Development: [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud)
|
||||
|
||||
|
||||
### Effect of system
|
||||
|
||||
##### ChatGPT AI Dialog
|
||||
> Go to the JeecgBoot background home page and click "AI Assistant" in the middle of the right side of the home page. The AI Assistant dialog screen is displayed.
|
||||

|
||||
|
||||
|
||||
##### PC
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### interactive
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### process Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### min process
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### dashboard Designer
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
##### report Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### form Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### bigscreen Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### uniapp
|
||||

|
||||
|
||||

|
||||
|
||||
##### low app
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### app
|
||||

|
||||

|
||||
|
||||
##### PAD
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
##### chart
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
##### swagger
|
||||

|
||||

|
||||
|
||||
|
||||
## donation
|
||||
|
||||
If so, buy the author a cup of coffee ☺
|
||||
|
||||

|
||||
571
README.md
571
README.md
@ -1,160 +1,143 @@
|
||||
中文 | [English](./README.en-US.md)
|
||||
|
||||
JeecgBoot AI低代码平台
|
||||
Jeecg AI应用平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.9.1(发布日期:2026-01-22)
|
||||
当前最新版本: 1.0.0(发布日期:2025-12-05)
|
||||
|
||||
|
||||
[](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/jeecg-ai)
|
||||
[](https://github.com/jeecgboot/jeecg-ai)
|
||||
[](https://github.com/jeecgboot/jeecg-ai)
|
||||
|
||||
|
||||
|
||||
项目介绍
|
||||
-----------------------------------
|
||||
|
||||
<h3 align="center">企业级AI低代码平台</h3>
|
||||
|
||||
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大模型。
|
||||
|
||||
`傻瓜式报表:` JimuReport是一款自主研发的强大开源企业级Web报表工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
||||
|
||||
`傻瓜式大屏:` JimuBI一款自主研发的强大的大屏和仪表盘设计工具。专注数字孪生与数据可视化,支持交互式大屏、仪表盘、门户和移动端,实现“一次开发,多端适配”。 大屏设计类Word风格,支持多屏切换,自由拖拽,轻松打造炫酷动态界面。
|
||||
|
||||
`成熟AI应用功能:` 提供一套完善AI应用平台: 涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表、MCP插件配置等功能。平台兼容主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
||||
|
||||
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
||||
|
||||
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批,扩展任务接口供开发人员编写业务逻辑,表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计(松耦合)及任务节点的灵活配置,既保障了企业流程的安全性与保密性,又大幅降低了开发人员的工作量。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
适用项目
|
||||
-----------------------------------
|
||||
JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,特别适用于SAAS、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)及AI知识库等场景。其半智能手工Merge开发模式,可显著提升70%以上的开发效率,极大降低开发成本。同时,JeecgBoot还是一款全栈式AI开发平台,助力企业快速构建和部署个性化AI应用。。
|
||||
|
||||
|
||||
**信创兼容说明**
|
||||
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
||||
- 数据库:达梦、人大金仓、TiDB
|
||||
- 中间件:东方通 TongWeb、TongRDS,宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
|
||||
|
||||
|
||||
版本说明
|
||||
-----------------------------------
|
||||
|
||||
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|
||||
|------|---------------------------------------------------------|----------------------------|-------------------|--------------------------------------------|
|
||||
| Github | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支|
|
||||
| Gitee | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支 |
|
||||
|
||||
|
||||
- `jeecg-boot` 是后端JAVA源码项目Springboot3+Shiro+Mybatis+SpringCloudAlibaba(支持单体和微服务切换).
|
||||
- `jeecgboot-vue3` 是前端VUE3源码项目(vue3+vite6+ts最新技术栈).
|
||||
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端,支持APP、小程序、H5、鸿蒙、鸿蒙Next.
|
||||
- `jeecg-boot-starter` 是[jeecg-boot对应的底层封装starter](https://github.com/jeecgboot/jeecg-boot-starter) :微服务启动、xxljob、分布式锁starter、rabbitmq、分布式事务、分库分表shardingsphere等.
|
||||
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo,制作一个精简版本
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
启动项目
|
||||
-----------------------------------
|
||||
|
||||
> 默认账号密码: admin/123456
|
||||
|
||||
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
|
||||
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
|
||||
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
|
||||
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
|
||||
|
||||
|
||||
技术文档
|
||||
-----------------------------------
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
||||
- 入门指南: [快速入门](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(满)、其他(满)
|
||||
|
||||
|
||||
AI 应用平台介绍
|
||||
-----------------------------------
|
||||
|
||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||
|
||||
JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||
|
||||
- [详细专题介绍,请点击查看](README-AI.md)
|
||||
|
||||
- AI视频介绍
|
||||
|
||||
### AI视频介绍
|
||||
|
||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||
|
||||
|
||||
为什么选择JeecgBoot?
|
||||
-----------------------------------
|
||||
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
||||
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
||||
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
||||
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
||||
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
|
||||
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
|
||||
- 7.低代码能力:在线表单(无需编码,通过在线配置表单,实现表单的增删改查,支持单表、树、一对多、一对一等模型,实现人人皆可编码),在线配置零代码开发、所见即所得支持23种类控件。
|
||||
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)。
|
||||
- 9.Online支持在线增强开发,提供在线代码编辑器,支持代码高亮、代码提示等功能,支持多种语言(Java、SQL、JavaScript等)。
|
||||
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能。
|
||||
- 11.前端UI提供丰富的组件库,支持各种常用组件,如表格、树形控件、下拉框、日期选择器等,满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
|
||||
- 12.提供APP配套框架,一份多代码多终端适配,一份代码多终端适配,小程序、H5、安卓、iOS、鸿蒙Next。
|
||||
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈,包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接:目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能,提供了丰富的组件。
|
||||
- 14.提供了一套成熟的AI应用平台功能,从AI模型、知识库到AI应用搭建,助力企业快速落地AI服务,加速智能化升级。
|
||||
- 15.AI能力:目前JeecgBoot支持AI大模型chatgpt和deepseek,现在最新版默认使用deepseek,速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
|
||||
- 16.提供新行编辑表格JVXETable,轻松满足各种复杂ERP布局,拥有更高的性能、更灵活的扩展、更强大的功能。
|
||||
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
|
||||
- 18.常用共通封装,各种工具类(定时任务、短信接口、邮件发送、Excel导入导出等),基本满足80%项目需求。
|
||||
- 19.简易Excel导入导出,支持单表导出和一对多表模式导出,生成的代码自带导入导出功能。
|
||||
- 20.集成智能报表工具,报表打印、图像报表和数据导出非常方便,可极其方便地生成PDF、Excel、Word等报表。
|
||||
- 21.采用前后分离技术,页面UI风格精美,针对常用组件做了封装:时间、行表格控件、截取显示控件、报表组件、编辑器等。
|
||||
- 22.查询过滤器:查询功能自动生成,后台动态拼SQL追加查询条件;支持多种匹配方式(全匹配/模糊查询/包含查询/不匹配查询)。
|
||||
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
|
||||
- 24.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制;也提供了基于AK和SK认证鉴权的OpenAPI功能。
|
||||
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
|
||||
- 26.权限控制采用RBAC(Role-Based Access Control,基于角色的访问控制)。
|
||||
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
|
||||
- 28.支持SaaS服务模式,提供SaaS多租户架构方案。
|
||||
- 29.分布式文件服务,集成MinIO、阿里OSS等优秀的第三方,提供便捷的文件上传与管理,同时也支持本地存储。
|
||||
- 30.主流数据库兼容,一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
|
||||
- 31.集成工作流Flowable,并实现了只需在页面配置流程转向,可极大简化BPM工作流的开发;用BPM的流程设计器画出了流程走向,一个工作流基本就完成了,只需写很少量的Java代码。
|
||||
- 32.低代码能力:在线流程设计,采用开源Flowable流程引擎,实现在线画流程、自定义表单、表单挂靠、业务流转。
|
||||
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
|
||||
- 34.提供单点登录CAS集成方案,项目中已经提供完善的对接代码。
|
||||
- 35.低代码能力:表单设计器,支持用户自定义表单布局,支持单表、一对多表单,支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
|
||||
- 36.专业接口对接机制,统一采用RESTful接口方式,集成Swagger-UI在线接口文档,JWT token安全验证,方便客户端对接。
|
||||
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
|
||||
- 38.提供各种系统监控,实时跟踪系统运行情况(监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控)。
|
||||
- 39.消息中心(支持短信、邮件、微信推送等);集成WebSocket消息通知机制。
|
||||
- 40.支持多语言,提供国际化方案。
|
||||
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
|
||||
- 42.提供简单易用的打印插件,支持谷歌、火狐、IE11+等各种浏览器。
|
||||
- 43.后端采用Maven分模块开发方式;前端支持菜单动态路由。
|
||||
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
|
||||
##### 功能大模块
|
||||
|
||||
- AI应用开发平台
|
||||
- AI知识库系统
|
||||
- AI大模型管理
|
||||
- AI流程编排
|
||||
- AI对话支持图片
|
||||
- AI对话助手(智能问答)
|
||||
- AI建表(Online表单)
|
||||
- AI写文章(CMS)
|
||||
- AI表单字段建议(表单设计器)
|
||||
|
||||
|
||||
|
||||
#### Dify `VS` JEECG AI
|
||||
|
||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
||||
> - Markdown文档库导入:
|
||||
> JEECG AI允许用户直接导入整个Markdown文档库,这不仅保留markdown格式,还支持图片的导入,确保文档内容的完整性和可视化效果。
|
||||
> - 对话回复格式美观:
|
||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
||||
> - PDF文档导入与格式转换:
|
||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||
|
||||
|
||||
| 功能 | Dify | Jeecg AI |
|
||||
|------------|------------------|-----------------------------------------|
|
||||
| AI工作流 | 有 | 有 |
|
||||
| RAG 管道向量搜索 | 有 | 有 |
|
||||
| AI模型管理 | 有 | 有 |
|
||||
| AI应用管理 | 有 | 有 |
|
||||
| AI知识库 | 有 | 有 |
|
||||
| 产品方向 | 一款独立的 LLM 应用开发平台 | 低代码与AIGC应用二者结合的平台 |
|
||||
| 业务集成 | 业务集成能力弱 | 更方便与业务系统集成,调用系统接口和逻辑更加方便 |
|
||||
| AI业务流 | 侧重AI逻辑流程 | AI流程编排作为低代码的业务引擎,用户可以通过AI流程配置各种业务流和AI流程 |
|
||||
| 实现语言 | python + react | JAVA + vue3 |
|
||||
| 上传markdown文档库(支持图片) | 不支持 | 支持 |
|
||||
| AI对话支持发图和展示图片 | 支持 | 支持 |
|
||||
|
||||
|
||||
|
||||
### 技术文档
|
||||
|
||||
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
||||
- [安装向量库 pgvector](https://help.jeecg.com/aigc/config)
|
||||
|
||||
|
||||
|
||||
## 功能特点
|
||||
|
||||
- AI流程: 提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
||||
- AI流程即服务: 通过AI流程编排你需要的智能体,结合AI+自定义开发节点 实现功能性 API,让你瞬间拥有各种智能体API。
|
||||
- AI助手对话功能: 集成 ChatGPT、Deepseek、智普、私有大模型 等 AI 模型,提供智能对话和生成式 AI 功能,深度与知识库结合提供更精准的知识。
|
||||
- RAG 功能: 涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本,支持检索增强生成(RAG),将未训练数据与 AI 模型集成,提升智能交互能力。
|
||||
- AI 知识库: 通过导入文档或已有问答对进行训练,让 AI 模型能根据文档以交互式对话方式回答问题。
|
||||
- 模型管理:支持对接各种大模型,包括本地私有大模型(Deepseek/ Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等)和国外公共大模型(OpenAI / Claude / Gemini 等);
|
||||
- 无缝嵌入:Iframe一键嵌入,支持将AI聊天助手快速嵌入到第三方系统,让系统快速拥有智能问答能力,提高用户满意度。
|
||||
|
||||
|
||||
|
||||
|
||||
#### 在线体验
|
||||
|
||||
- JeecgBoot演示: https://boot3.jeecg.com
|
||||
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
||||
|
||||
|
||||
## 技术交流
|
||||
|
||||
- 开发文档:https://help.jeecg.com/aigc
|
||||
- QQ群:964611995、716488839(满)
|
||||
|
||||
|
||||
## 功能列表
|
||||
|
||||
- AI应用管理(普通应用、高级流程应用)
|
||||
- AI模型管理
|
||||
- AI知识库
|
||||
- AI应用平台(普通、对接AI流程)
|
||||
- AI流程编排
|
||||
- AI聊天支持嵌入第三方
|
||||
- AI向量库对接
|
||||
|
||||
|
||||
|
||||
## 支持AI模型
|
||||
|
||||
| AI大模型 | 支持 |
|
||||
|---------------| --- |
|
||||
| DeepSeek | √ |
|
||||
| ChatGTP | √ |
|
||||
| Qwq | √ |
|
||||
| 智库 | √ |
|
||||
| claude | √ |
|
||||
| vl模型 | √ |
|
||||
| 千帆大模型 | √ |
|
||||
| 通义千问 | √ |
|
||||
| Ollama本地搭建大模型 | √ |
|
||||
| 等等。。 | √ |
|
||||
|
||||
|
||||
|
||||
|
||||
## AIGC能做什么
|
||||
|
||||
AIGC模块是一个基于AI的自动化流程编排工具和聊天应用搭建平台,它可以帮助用户快速生成AI流程接口和聊天应用,提高效率。
|
||||
以下是一些具体的应用场景和示例:
|
||||
|
||||
- 你可能需要一个翻译接口,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个接口转换工具,可以通过AI流程编排搭建出来。(比如:jimureport所需要接口返回格式与你的系统不同,你通过AI接口实现自动转换)
|
||||
- 你可能需要一个聊天机器人,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个自动化流程,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个自动化处理文件的流程,可以通过AI流程结合python脚本实现操作电脑,文件等。
|
||||
|
||||
|
||||
技术架构:
|
||||
@ -171,7 +154,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
||||
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||
- 最新技术栈:Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
||||
|
||||
|
||||
#### 后端
|
||||
|
||||
- IDE建议: IDEA (必须安装lombok插件 )
|
||||
@ -190,319 +172,52 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类
|
||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||
- 默认提供MySQL5.7+数据库脚本
|
||||
|
||||
#### 数据库支持
|
||||
|
||||
> jeecgboot平台支持以下数据库,默认我们只提供mysql脚本,其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
|
||||
## AI应用平台功能展示
|
||||
|
||||
| 数据库 | 支持 |
|
||||
| --- | --- |
|
||||
| MySQL | √ |
|
||||
| Oracle11g | √ |
|
||||
| Sqlserver2017 | √ |
|
||||
| PostgreSQL | √ |
|
||||
| MariaDB | √ |
|
||||
| 达梦 | √ |
|
||||
| 人大金仓 | √ |
|
||||
| TiDB | √ |
|
||||
| kingbase8 | √ |
|
||||
AI模型列表
|
||||
|
||||

|
||||
|
||||
选择AI模型,配置你的参数
|
||||
|
||||
|
||||
## 微服务解决方案
|
||||

|
||||
|
||||
- 1、服务注册和发现 Nacos √
|
||||
- 2、统一配置中心 Nacos √
|
||||
- 3、路由网关 gateway(三种加载方式) √
|
||||
- 4、分布式 http feign √
|
||||
- 5、熔断降级限流 Sentinel √
|
||||
- 6、分布式文件 Minio、阿里OSS √
|
||||
- 7、统一权限控制 JWT + Shiro √
|
||||
- 8、服务监控 SpringBootAdmin√
|
||||
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
|
||||
- 10、消息中间件 RabbitMQ √
|
||||
- 11、分布式任务 xxl-job √
|
||||
- 12、分布式事务 Seata
|
||||
- 13、轻量分布式日志 Loki+grafana套件
|
||||
- 14、支持 docker-compose、k8s、jenkins
|
||||
- 15、CAS 单点登录 √
|
||||
- 16、路由限流 √
|
||||
|
||||
#### 微服务架构图
|
||||

|
||||
AI知识库支持手工录入文本,导入pdf\\word\\excel等文档,支持问答对训练
|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||
|
||||
AI流程,提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
||||
|
||||
#### 系统功能架构图
|
||||

|
||||
|
||||

|
||||
|
||||
目前支持的节点有:开始、结束、AI知识库节点、AI节点、分类节点、分支节点、JAVA节点、脚本节点、子流程节点、http请求节点、直接回复节点等节点
|
||||
|
||||

|
||||
|
||||
### 开源版功能清单
|
||||
```
|
||||
├─系统管理
|
||||
│ ├─用户管理
|
||||
│ ├─角色管理
|
||||
│ ├─菜单管理
|
||||
│ ├─首页配置
|
||||
│ ├─权限设置(支持按钮权限、数据权限)
|
||||
│ ├─表单权限(控制字段禁用、隐藏)
|
||||
│ ├─部门管理
|
||||
│ ├─我的部门(二级管理员)
|
||||
│ └─字典管理
|
||||
│ └─分类字典
|
||||
│ └─系统公告
|
||||
│ └─职务管理
|
||||
│ └─通讯录
|
||||
│ ├─多数据源管理
|
||||
│ ├─白名单管理
|
||||
│ ├─第三方配置(对接钉钉和企业微信)
|
||||
│ └─多租户管理(租户管理、租户角色、我的租户、租户默认套餐管理)
|
||||
├─Online在线开发(低代码)
|
||||
│ ├─Online在线表单
|
||||
│ ├─Online代码生成器
|
||||
│ ├─Online在线报表
|
||||
│ ├─仪表盘设计器
|
||||
│ ├─系统编码规则
|
||||
│ ├─系统校验规则
|
||||
│ ├─APP版本管理
|
||||
├─AI应用平台
|
||||
│ ├─AI知识库问答系统
|
||||
│ ├─AI大模型管理
|
||||
│ ├─AI流程编排
|
||||
│ ├─AI流程设计器
|
||||
│ ├─AI对话支持图片
|
||||
│ ├─AI对话助手(智能问答)
|
||||
│ ├─AI建表(Online表单)
|
||||
│ ├─AI聊天窗口支持嵌入第三方
|
||||
│ ├─AI聊天窗口支持移动端
|
||||
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
|
||||
│ ├─AI OCR示例
|
||||
├─数据可视化
|
||||
│ ├─报表设计器(支持打印设计)
|
||||
│ ├─大屏设和仪表盘设计
|
||||
├─OpenAPI(基于AK和SK认证鉴权)
|
||||
│ ├─接口管理
|
||||
│ ├─接口授权
|
||||
│ ├─接口文档
|
||||
├─消息中心
|
||||
│ ├─消息管理
|
||||
│ ├─模板管理
|
||||
├─代码生成器(低代码)
|
||||
│ ├─代码生成器功能(一键生成前后端代码,生成后无需修改直接用,绝对是后端开发福音)
|
||||
│ ├─代码生成器模板(提供4套模板,分别支持单表和一对多模型,不同风格选择)
|
||||
│ ├─代码生成器模板(生成代码,自带excel导入导出)
|
||||
│ ├─查询过滤器(查询逻辑无需编码,系统根据页面配置自动生成)
|
||||
│ ├─高级查询器(弹窗自动组合查询条件)
|
||||
│ ├─Excel导入导出工具集成(支持单表,一对多 导入导出)
|
||||
│ ├─平台移动自适应支持
|
||||
│ ├─提供新版uniapp3的代码生成器模板
|
||||
├─系统监控
|
||||
│ ├─Gateway路由网关
|
||||
│ ├─基于AK和SK认证鉴权OpenAPI功能
|
||||
│ ├─定时任务
|
||||
│ ├─数据源管理
|
||||
│ ├─性能扫描监控
|
||||
│ │ ├─监控 Redis
|
||||
│ │ ├─Tomcat
|
||||
│ │ ├─jvm
|
||||
│ │ ├─服务器信息
|
||||
│ │ ├─请求追踪
|
||||
│ │ ├─磁盘监控
|
||||
│ ├─系统日志
|
||||
│ ├─消息中心(支持短信、邮件、微信推送等等)
|
||||
│ ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
|
||||
│ ├─SQL监控
|
||||
│ ├─在线用户
|
||||
│─报表示例
|
||||
│ ├─曲线图
|
||||
│ └─饼状图
|
||||
│ └─柱状图
|
||||
│ └─折线图
|
||||
│ └─面积图
|
||||
│ └─雷达图
|
||||
│ └─仪表图
|
||||
│ └─进度条
|
||||
│ └─排名列表
|
||||
│ └─等等
|
||||
│─大屏模板
|
||||
│ ├─作战指挥中心大屏
|
||||
│ └─物流服务中心大屏
|
||||
│─常用示例
|
||||
│ ├─自定义组件
|
||||
│ ├─对象存储(对接阿里云)
|
||||
│ ├─JVXETable示例(各种复杂ERP布局示例)
|
||||
│ ├─单表模型例子
|
||||
│ └─一对多模型例子
|
||||
│ └─打印例子
|
||||
│ └─一对多TAB例子
|
||||
│ └─内嵌table例子
|
||||
│ └─常用选择组件
|
||||
│ └─异步树table
|
||||
│ └─接口模拟测试
|
||||
│ └─表格合计示例
|
||||
│ └─异步树列表示例
|
||||
│ └─一对多JEditable
|
||||
│ └─JEditable组件示例
|
||||
│ └─图片拖拽排序
|
||||
│ └─图片翻页
|
||||
│ └─图片预览
|
||||
│ └─PDF预览
|
||||
│ └─分屏功能
|
||||
│─封装通用组件
|
||||
│ ├─行编辑表格JEditableTable
|
||||
│ └─省略显示组件
|
||||
│ └─时间控件
|
||||
│ └─高级查询
|
||||
│ └─用户选择组件
|
||||
│ └─报表组件封装
|
||||
│ └─字典组件
|
||||
│ └─下拉多选组件
|
||||
│ └─选人组件
|
||||
│ └─选部门组件
|
||||
│ └─通过部门选人组件
|
||||
│ └─封装曲线、柱状图、饼状图、折线图等等报表的组件(经过封装,使用简单)
|
||||
│ └─在线code编辑器
|
||||
│ └─上传文件组件
|
||||
│ └─验证码组件
|
||||
│ └─树列表组件
|
||||
│ └─表单禁用组件
|
||||
│ └─等等
|
||||
│─更多页面模板
|
||||
│ ├─各种高级表单
|
||||
│ ├─各种列表效果
|
||||
│ └─结果页面
|
||||
│ └─异常页面
|
||||
│ └─个人页面
|
||||
├─高级功能
|
||||
│ ├─提供单点登录CAS集成方案
|
||||
│ ├─提供APP发布方案
|
||||
│ ├─集成Websocket消息通知机制
|
||||
│ ├─支持electron桌面应用打包(支持windows、linux、macOS三大平台)
|
||||
│ ├─docker容器支持
|
||||
│ ├─提供移动APP框架及源码(Uniapp3版本)支持H5、小程序、APP、鸿蒙Next
|
||||
│ ├─提供移动APP低代码设计(Online表单、仪表盘)
|
||||
```
|
||||
节点项配置
|
||||
|
||||

|
||||
|
||||
在线运行看结果
|
||||
|
||||
### 系统效果
|
||||

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

|
||||
|
||||

|
||||
AI应用配置,支持AI流程配置和简单的AI配置
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
可以关联多个知识库,右侧是AI智能回复,你可以搭建自己的智能体,比如搭建一个 “诗词达人” “翻译助手”
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
可以将创建的聊天应用,集成到第三方系统中
|
||||
|
||||
##### 系统交互
|
||||

|
||||
|
||||

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

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

|
||||
|
||||

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

|
||||
|
||||
|
||||
##### 仪表盘设计器
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### 报表设计器
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### 手机端
|
||||

|
||||

|
||||
|
||||
##### PAD端
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
##### 图表示例
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
##### 在线接口文档
|
||||

|
||||

|
||||
|
||||
|
||||
##### UNIAPP效果
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### 大屏设计器
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 捐赠
|
||||
|
||||
如果觉得还不错,请作者喝杯咖啡吧 ☺
|
||||
|
||||

|
||||

|
||||
@ -1,154 +0,0 @@
|
||||
version: '2'
|
||||
services:
|
||||
jeecg-boot-mysql:
|
||||
build:
|
||||
context: ./jeecg-boot/db
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
TZ: Asia/Shanghai
|
||||
restart: always
|
||||
container_name: jeecg-boot-mysql
|
||||
image: jeecg-boot-mysql
|
||||
command:
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_general_ci
|
||||
--explicit_defaults_for_timestamp=true
|
||||
--lower_case_table_names=1
|
||||
--max_allowed_packet=128M
|
||||
--default-authentication-plugin=caching_sha2_password
|
||||
ports:
|
||||
- 13306:3306
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-redis:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
|
||||
# ports:
|
||||
# - 6379:6379
|
||||
restart: always
|
||||
hostname: jeecg-boot-redis
|
||||
container_name: jeecg-boot-redis
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-pgvector:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
|
||||
container_name: jeecg-boot-pgvector
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: vector_db
|
||||
ports:
|
||||
- 5432:5432
|
||||
restart: always
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-nacos:
|
||||
restart: always
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-cloud-nacos
|
||||
ports:
|
||||
- 8848:8848
|
||||
container_name: jeecg-boot-nacos
|
||||
depends_on:
|
||||
- jeecg-boot-mysql
|
||||
hostname: jeecg-boot-nacos
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-system:
|
||||
depends_on:
|
||||
- jeecg-boot-nacos
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-system-cloud-start
|
||||
container_name: jeecg-system-start
|
||||
hostname: jeecg-boot-system
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-demo:
|
||||
depends_on:
|
||||
- jeecg-boot-nacos
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-demo-cloud-start
|
||||
container_name: jeecg-demo-start
|
||||
hostname: jeecg-boot-demo
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-gateway:
|
||||
restart: on-failure
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-cloud-gateway
|
||||
ports:
|
||||
- 9999:9999
|
||||
depends_on:
|
||||
- jeecg-boot-nacos
|
||||
- jeecg-boot-system
|
||||
container_name: jeecg-boot-gateway
|
||||
hostname: jeecg-boot-gateway
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
# jeecg-boot-rabbitmq:
|
||||
# image: rabbitmq:3.7.7-management
|
||||
# ports:
|
||||
# - 5672:5672
|
||||
# - 15672:15672
|
||||
# restart: always
|
||||
# container_name: jeecg-boot-rabbitmq
|
||||
# hostname: jeecg-boot-rabbitmq
|
||||
# environment:
|
||||
# RABBITMQ_DEFAULT_USER: guest
|
||||
# RABBITMQ_DEFAULT_PASS: guest
|
||||
|
||||
jeecg-boot-sentinel:
|
||||
restart: on-failure
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel
|
||||
ports:
|
||||
- 9000:9000
|
||||
depends_on:
|
||||
- jeecg-boot-nacos
|
||||
- jeecg-boot-demo
|
||||
- jeecg-boot-system
|
||||
- jeecg-boot-gateway
|
||||
container_name: jeecg-boot-sentinel
|
||||
hostname: jeecg-boot-sentinel
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-xxljob:
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob
|
||||
ports:
|
||||
- 9080:9080
|
||||
container_name: jeecg-boot-xxljob
|
||||
hostname: jeecg-boot-xxljob
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-vue:
|
||||
build:
|
||||
context: ./jeecgboot-vue3
|
||||
dockerfile: Dockerfile.cloud
|
||||
container_name: jeecgboot-vue3-nginx
|
||||
image: jeecgboot-vue3
|
||||
depends_on:
|
||||
- jeecg-boot-system
|
||||
networks:
|
||||
- jeecg-boot
|
||||
ports:
|
||||
- 80:80
|
||||
|
||||
networks:
|
||||
jeecg-boot:
|
||||
name: jeecg_boot
|
||||
@ -198,16 +198,4 @@
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
|
||||
|
||||
JeecgBoot 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京,地址:中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱:jeecgos@163.com
|
||||
本软件受适用的国家软件著作权法(包括国际条约)和开源协议 双重保护许可。
|
||||
|
||||
开源协议中文释意如下:
|
||||
1.JeecgBoot开源版本无任何限制,在遵循本开源协议条款下,允许商用使用,不会造成侵权行为。
|
||||
2.允许基于本平台软件开展业务系统开发。
|
||||
3.在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件。
|
||||
|
||||
最终解释权归:http://www.jeecg.com
|
||||
limitations under the License.
|
||||
@ -1,253 +0,0 @@
|
||||
JeecgBoot 低代码开发平台
|
||||
===============
|
||||
|
||||
当前最新版本: 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)
|
||||
|
||||
|
||||
|
||||
项目介绍
|
||||
-----------------------------------
|
||||
|
||||
<h3 align="center">企业级AI低代码平台</h3>
|
||||
|
||||
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台,助力企业快速实现低代码开发和构建AI应用。
|
||||
采用前后端分离架构(Ant Design&Vue3,SpringBoot3,SpringCloud Alibaba,Mybatis-plus),强大代码生成器实现前后端一键生成,无需手写代码。
|
||||
平台引领AI低代码开发模式:AI生成→在线编码→代码生成→手工合并,解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
|
||||
具备强大且颗粒化的权限控制,支持按钮权限和数据权限设置,满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天,支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
|
||||
|
||||
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
|
||||
|
||||
`AI赋能低代码:` 提供完善成熟的AI应用平台,涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型,包括ChatGPT、DeepSeek、Ollama、智普、千问等,助力企业高效构建智能化应用,推动低代码开发与AI深度融合。
|
||||
|
||||
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建,同时针对复杂功能采用代码生成器生成代码并手工合并,打造智能且灵活的低代码开发模式,有效解决了当前低代码产品普遍缺乏灵活性的问题,提升开发效率的同时兼顾系统的扩展性和定制化能力。
|
||||
|
||||
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批,扩展任务接口供开发人员编写业务逻辑,表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计(松耦合)及任务节点的灵活配置,既保障了企业流程的安全性与保密性,又大幅降低了开发人员的工作量。
|
||||
|
||||
|
||||
适用项目
|
||||
-----------------------------------
|
||||
JeecgBoot低代码平台兼容所有J2EE项目开发,支持信创国产化,特别适用于SAAS、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)及AI知识库等场景。其半智能手工Merge开发模式,可显著提升70%以上的开发效率,极大降低开发成本。同时,JeecgBoot还是一款全栈式AI开发平台,助力企业快速构建和部署个性化AI应用。。
|
||||
|
||||
|
||||
**信创兼容说明**
|
||||
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
|
||||
- 数据库:达梦、人大金仓、TiDB
|
||||
- 中间件:东方通 TongWeb、TongRDS,宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
|
||||
|
||||
|
||||
|
||||
|
||||
#### 项目说明
|
||||
|
||||
| 项目名 | 说明 |
|
||||
|--------------------|------------------------------------|
|
||||
| `jeecg-boot` | 后端源码JAVA(SpringBoot3微服务架构) |
|
||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite6+antd4+ts最新技术栈) |
|
||||
|
||||
|
||||
|
||||
启动项目
|
||||
-----------------------------------
|
||||
|
||||
> 默认账号密码: admin/123456
|
||||
|
||||
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
|
||||
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
|
||||
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
|
||||
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
|
||||
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
|
||||
|
||||
|
||||
技术文档
|
||||
-----------------------------------
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
|
||||
- 入门指南: [快速入门](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)
|
||||
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
|
||||
- QQ交流群 : 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
|
||||
|
||||
|
||||
AI 应用平台介绍
|
||||
-----------------------------------
|
||||
|
||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||
|
||||
JeecgBoot平台提供了一套完善的AI应用管理系统模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||
|
||||
- [详细专题介绍,请点击查看](README-AI.md)
|
||||
|
||||
- AI视频介绍
|
||||
|
||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||
|
||||
|
||||
为什么选择JeecgBoot?
|
||||
-----------------------------------
|
||||
- 1.采用最新主流前后分离框架(Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3),容易上手;代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发。
|
||||
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
|
||||
- 3.支持微服务Spring Cloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供简易机制,支持单体和微服务自由切换(这样可以满足各类项目需求)。
|
||||
- 4.开发效率高,支持在线建表和AI建表,提供强大代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用。
|
||||
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
|
||||
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
|
||||
- 7.低代码能力:在线表单(无需编码,通过在线配置表单,实现表单的增删改查,支持单表、树、一对多、一对一等模型,实现人人皆可编码),在线配置零代码开发、所见即所得支持23种类控件。
|
||||
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)。
|
||||
- 9.Online支持在线增强开发,提供在线代码编辑器,支持代码高亮、代码提示等功能,支持多种语言(Java、SQL、JavaScript等)。
|
||||
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能。
|
||||
- 11.前端UI提供丰富的组件库,支持各种常用组件,如表格、树形控件、下拉框、日期选择器等,满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
|
||||
- 12.提供APP配套框架,一份多代码多终端适配,一份代码多终端适配,小程序、H5、安卓、iOS、鸿蒙Next。
|
||||
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈,包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接:目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能,提供了丰富的组件。
|
||||
- 14.提供了一套成熟的AI应用平台功能,从AI模型、知识库到AI应用搭建,助力企业快速落地AI服务,加速智能化升级。
|
||||
- 15.AI能力:目前JeecgBoot支持AI大模型chatgpt和deepseek,现在最新版默认使用deepseek,速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
|
||||
- 16.提供新行编辑表格JVXETable,轻松满足各种复杂ERP布局,拥有更高的性能、更灵活的扩展、更强大的功能。
|
||||
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
|
||||
- 18.常用共通封装,各种工具类(定时任务、短信接口、邮件发送、Excel导入导出等),基本满足80%项目需求。
|
||||
- 19.简易Excel导入导出,支持单表导出和一对多表模式导出,生成的代码自带导入导出功能。
|
||||
- 20.集成智能报表工具,报表打印、图像报表和数据导出非常方便,可极其方便地生成PDF、Excel、Word等报表。
|
||||
- 21.采用前后分离技术,页面UI风格精美,针对常用组件做了封装:时间、行表格控件、截取显示控件、报表组件、编辑器等。
|
||||
- 22.查询过滤器:查询功能自动生成,后台动态拼SQL追加查询条件;支持多种匹配方式(全匹配/模糊查询/包含查询/不匹配查询)。
|
||||
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
|
||||
- 24.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制;也提供了基于AK和SK认证鉴权的OpenAPI功能。
|
||||
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
|
||||
- 26.权限控制采用RBAC(Role-Based Access Control,基于角色的访问控制)。
|
||||
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
|
||||
- 28.支持SaaS服务模式,提供SaaS多租户架构方案。
|
||||
- 29.分布式文件服务,集成MinIO、阿里OSS等优秀的第三方,提供便捷的文件上传与管理,同时也支持本地存储。
|
||||
- 30.主流数据库兼容,一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
|
||||
- 31.集成工作流Flowable,并实现了只需在页面配置流程转向,可极大简化BPM工作流的开发;用BPM的流程设计器画出了流程走向,一个工作流基本就完成了,只需写很少量的Java代码。
|
||||
- 32.低代码能力:在线流程设计,采用开源Flowable流程引擎,实现在线画流程、自定义表单、表单挂靠、业务流转。
|
||||
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
|
||||
- 34.提供单点登录CAS集成方案,项目中已经提供完善的对接代码。
|
||||
- 35.低代码能力:表单设计器,支持用户自定义表单布局,支持单表、一对多表单,支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
|
||||
- 36.专业接口对接机制,统一采用RESTful接口方式,集成Swagger-UI在线接口文档,JWT token安全验证,方便客户端对接。
|
||||
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
|
||||
- 38.提供各种系统监控,实时跟踪系统运行情况(监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控)。
|
||||
- 39.消息中心(支持短信、邮件、微信推送等);集成WebSocket消息通知机制。
|
||||
- 40.支持多语言,提供国际化方案。
|
||||
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
|
||||
- 42.提供简单易用的打印插件,支持谷歌、火狐、IE11+等各种浏览器。
|
||||
- 43.后端采用Maven分模块开发方式;前端支持菜单动态路由。
|
||||
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
|
||||
|
||||
|
||||
技术架构:
|
||||
-----------------------------------
|
||||
|
||||
#### 后端
|
||||
|
||||
- IDE建议: IDEA (必须安装lombok插件 )
|
||||
- 语言:Java 默认jdk17(jdk21、jdk24)
|
||||
- 依赖管理:Maven
|
||||
- 基础框架:Spring Boot 3.5.5
|
||||
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
|
||||
- 持久层框架:MybatisPlus 3.5.12
|
||||
- 报表工具: JimuReport 2.1.3
|
||||
- 安全框架:Apache Shiro 2.0.4,Jwt 4.5.0
|
||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||
- 数据库连接池:阿里巴巴Druid 1.2.24
|
||||
- AI大模型:支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
|
||||
- 日志打印:logback
|
||||
- 缓存:Redis
|
||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||
- 默认提供MySQL5.7+数据库脚本
|
||||
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
||||
|
||||
|
||||
#### 前端
|
||||
|
||||
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
||||
|
||||
- 依赖管理:node、npm、pnpm
|
||||
- 前端IDE建议:IDEA、WebStorm、Vscode
|
||||
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||
- 最新技术栈:Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
||||
|
||||
|
||||
|
||||
|
||||
#### 支持库
|
||||
|
||||
| 数据库 | 支持 |
|
||||
| --- | --- |
|
||||
| MySQL | √ |
|
||||
| Oracle11g | √ |
|
||||
| Sqlserver2017 | √ |
|
||||
| PostgreSQL | √ |
|
||||
| MariaDB | √ |
|
||||
| 达梦 | √ |
|
||||
| 人大金仓 | √ |
|
||||
| TiDB | √ |
|
||||
|
||||
|
||||
|
||||
|
||||
## 微服务解决方案
|
||||
|
||||
|
||||
- 1、服务注册和发现 Nacos √
|
||||
- 2、统一配置中心 Nacos √
|
||||
- 3、路由网关 gateway(三种加载方式) √
|
||||
- 4、分布式 http feign √
|
||||
- 5、熔断降级限流 Sentinel √
|
||||
- 6、分布式文件 Minio、阿里OSS √
|
||||
- 7、统一权限控制 JWT + Shiro √
|
||||
- 8、服务监控 SpringBootAdmin√
|
||||
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
|
||||
- 10、消息中间件 RabbitMQ √
|
||||
- 11、分布式任务 xxl-job √
|
||||
- 12、分布式事务 Seata
|
||||
- 13、轻量分布式日志 Loki+grafana套件
|
||||
- 14、支持 docker-compose、k8s、jenkins
|
||||
- 15、CAS 单点登录 √
|
||||
- 16、路由限流 √
|
||||
|
||||
|
||||
|
||||
后台目录结构
|
||||
-----------------------------------
|
||||
```
|
||||
项目结构
|
||||
├─jeecg-boot-parent(父POM: 项目依赖、modules组织)
|
||||
│ ├─jeecg-boot-base-core(共通模块: 工具类、config、权限、查询过滤器、注解等)
|
||||
│ ├─jeecg-module-demo 示例代码
|
||||
│ ├─jeecg-module-system System系统管理目录
|
||||
│ │ ├─jeecg-system-biz System系统管理权限等功能
|
||||
│ │ ├─jeecg-system-start System单体启动项目(8080)
|
||||
│ │ ├─jeecg-system-api System系统管理模块对外api
|
||||
│ │ │ ├─jeecg-system-cloud-api System模块对外提供的微服务接口
|
||||
│ │ │ ├─jeecg-system-local-api System模块对外提供的单体接口
|
||||
│ ├─jeecg-server-cloud --微服务模块
|
||||
├─jeecg-cloud-gateway --微服务网关模块(9999)
|
||||
├─jeecg-cloud-nacos --Nacos服务模块(8848)
|
||||
├─jeecg-system-cloud-start --System微服务启动项目(7001)
|
||||
├─jeecg-demo-cloud-start --Demo微服务启动项目(7002)
|
||||
├─jeecg-visual
|
||||
├─jeecg-cloud-monitor --微服务监控模块 (9111)
|
||||
├─jeecg-cloud-xxljob --微服务xxljob定时任务服务端 (9080)
|
||||
├─jeecg-cloud-sentinel --sentinel服务端 (9000)
|
||||
├─jeecg-cloud-test -- 微服务测试示例(各种例子)
|
||||
├─jeecg-cloud-test-more -- 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
|
||||
├─jeecg-cloud-test-rabbitmq -- 微服务测试示例(rabbitmq)
|
||||
├─jeecg-cloud-test-seata -- 微服务测试示例(seata分布式事务)
|
||||
├─jeecg-cloud-test-shardingsphere -- 微服务测试示例(分库分表)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### 微服务架构图
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -8,6 +8,6 @@ RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
COPY ./tables_nacos.sql /docker-entrypoint-initdb.d
|
||||
|
||||
COPY ./jeecgboot-mysql-5.7.sql /docker-entrypoint-initdb.d
|
||||
COPY ./jeecgai-mysql-5.7.sql /docker-entrypoint-initdb.d
|
||||
|
||||
COPY ./tables_xxl_job.sql /docker-entrypoint-initdb.d
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,360 +0,0 @@
|
||||
#
|
||||
# XXL-JOB v2.4.0
|
||||
# Copyright (c) 2015-present, xuxueli.
|
||||
|
||||
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;
|
||||
use `xxl_job`;
|
||||
|
||||
/*
|
||||
Navicat Premium Data Transfer
|
||||
|
||||
Source Server : mysql5.7
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 50738 (5.7.38)
|
||||
Source Host : 127.0.0.1:3306
|
||||
Source Schema : xxl_job
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 50738 (5.7.38)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 10/02/2025 13:49:31
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_group
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_group`;
|
||||
CREATE TABLE `xxl_job_group` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`app_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '执行器AppName',
|
||||
`title` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '执行器名称',
|
||||
`address_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT '执行器地址类型:0=自动注册、1=手动录入',
|
||||
`address_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '执行器地址列表,多地址逗号分隔',
|
||||
`update_time` datetime NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_group
|
||||
-- ----------------------------
|
||||
INSERT INTO `xxl_job_group` VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2025-02-10 13:49:04');
|
||||
INSERT INTO `xxl_job_group` VALUES (2, 'jeecg-demo', '测试Demo模块', 0, NULL, '2025-02-10 13:49:04');
|
||||
INSERT INTO `xxl_job_group` VALUES (3, 'jeecg-system', '系统System模块', 0, NULL, '2025-02-10 13:49:04');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_info
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_info`;
|
||||
CREATE TABLE `xxl_job_info` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
|
||||
`job_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`add_time` datetime NULL DEFAULT NULL,
|
||||
`update_time` datetime NULL DEFAULT NULL,
|
||||
`author` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '作者',
|
||||
`alarm_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '报警邮件',
|
||||
`schedule_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
|
||||
`schedule_conf` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
|
||||
`misfire_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
|
||||
`executor_route_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器路由策略',
|
||||
`executor_handler` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务handler',
|
||||
`executor_param` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务参数',
|
||||
`executor_block_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '阻塞处理策略',
|
||||
`executor_timeout` int(11) NOT NULL DEFAULT 0 COMMENT '任务执行超时时间,单位秒',
|
||||
`executor_fail_retry_count` int(11) NOT NULL DEFAULT 0 COMMENT '失败重试次数',
|
||||
`glue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'GLUE类型',
|
||||
`glue_source` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'GLUE源代码',
|
||||
`glue_remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'GLUE备注',
|
||||
`glue_updatetime` datetime NULL DEFAULT NULL COMMENT 'GLUE更新时间',
|
||||
`child_jobid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
|
||||
`trigger_status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '调度状态:0-停止,1-运行',
|
||||
`trigger_last_time` bigint(13) NOT NULL DEFAULT 0 COMMENT '上次调度时间',
|
||||
`trigger_next_time` bigint(13) NOT NULL DEFAULT 0 COMMENT '下次调度时间',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_info
|
||||
-- ----------------------------
|
||||
INSERT INTO `xxl_job_info` VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2024-08-21 22:30:30', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJob', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '', 1, 1729353600000, 1739203200000);
|
||||
INSERT INTO `xxl_job_info` VALUES (2, 3, '测试jeecg xxljob', '2024-08-21 22:41:10', '2024-08-21 22:41:30', 'JEECG', '', 'CRON', '* * * * * ?', 'DO_NOTHING', 'FIRST', 'demoJob', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2024-08-21 22:41:10', '', 1, 1739166572000, 1739166573000);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_lock
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_lock`;
|
||||
CREATE TABLE `xxl_job_lock` (
|
||||
`lock_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '锁名称',
|
||||
PRIMARY KEY (`lock_name`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_lock
|
||||
-- ----------------------------
|
||||
INSERT INTO `xxl_job_lock` VALUES ('schedule_lock');
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_log`;
|
||||
CREATE TABLE `xxl_job_log` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
|
||||
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
|
||||
`executor_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
|
||||
`executor_handler` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务handler',
|
||||
`executor_param` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务参数',
|
||||
`executor_sharding_param` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
|
||||
`executor_fail_retry_count` int(11) NOT NULL DEFAULT 0 COMMENT '失败重试次数',
|
||||
`trigger_time` datetime NULL DEFAULT NULL COMMENT '调度-时间',
|
||||
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
|
||||
`trigger_msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '调度-日志',
|
||||
`handle_time` datetime NULL DEFAULT NULL COMMENT '执行-时间',
|
||||
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
|
||||
`handle_msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '执行-日志',
|
||||
`alarm_status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `I_trigger_time`(`trigger_time`) USING BTREE,
|
||||
INDEX `I_handle_code`(`handle_code`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 6761 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_log
|
||||
-- ----------------------------
|
||||
INSERT INTO `xxl_job_log` VALUES (6618, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:09', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6619, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:10', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6620, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:11', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6621, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:12', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6622, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:13', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6623, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:14', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6624, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:15', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6625, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:16', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6626, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:17', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6627, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:18', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6628, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:19', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6629, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:20', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6630, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:21', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6631, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:22', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6632, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:23', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6633, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:24', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6634, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:25', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6635, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:26', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6636, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:27', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6637, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:28', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6638, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:29', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6639, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:30', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6640, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:31', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6641, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:32', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6642, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:33', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6643, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:34', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6644, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:35', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6645, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:36', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6646, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:37', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6647, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:38', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6648, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:39', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6649, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:40', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6650, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:41', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6651, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:42', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6652, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:43', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6653, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:44', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6654, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:45', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6655, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:46', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6656, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:47', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6657, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:48', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6658, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:49', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6659, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:50', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6660, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:51', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6661, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:52', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6662, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:53', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6663, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:54', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6664, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:55', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6665, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:56', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6666, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:57', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6667, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:58', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6668, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:59', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6669, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:00', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6670, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:01', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6671, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:02', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6672, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:03', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6673, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:04', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6674, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:05', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6675, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:06', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6676, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:07', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6677, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:08', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6678, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:09', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6679, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:10', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6680, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:11', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6681, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:12', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6682, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:13', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6683, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:14', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6684, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:15', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6685, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:16', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6686, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:17', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6687, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:18', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6688, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:19', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6689, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:20', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6690, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:21', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6691, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:22', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6692, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:23', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6693, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:24', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6694, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:25', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6695, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:26', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6696, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:27', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6697, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:28', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6698, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:29', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6699, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:30', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6700, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:31', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6701, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:32', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6702, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:33', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6703, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:34', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6704, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:35', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6705, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:36', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6706, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:37', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6707, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:38', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6708, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:39', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6709, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:40', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6710, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:41', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6711, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:42', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6712, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:43', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6713, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:44', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6714, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:45', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6715, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:46', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6716, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:47', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6717, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:48', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6718, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:49', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6719, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:50', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6720, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:51', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6721, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:52', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6722, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:53', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6723, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:54', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6724, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:55', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6725, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:56', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6726, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:57', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6727, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:58', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6728, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:59', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6729, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:00', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6730, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:01', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6731, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:02', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6732, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:03', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6733, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:04', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6734, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:05', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6735, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:06', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6736, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:07', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6737, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:08', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6738, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:09', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6739, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:10', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6740, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:11', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6741, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:12', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6742, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:13', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6743, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:14', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6744, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:15', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6745, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:16', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6746, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:17', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6747, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:18', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6748, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:19', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6749, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:20', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6750, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:21', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6751, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:22', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6752, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:23', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6753, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:24', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
|
||||
INSERT INTO `xxl_job_log` VALUES (6754, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:25', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
|
||||
INSERT INTO `xxl_job_log` VALUES (6755, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:26', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
|
||||
INSERT INTO `xxl_job_log` VALUES (6756, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:27', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
|
||||
INSERT INTO `xxl_job_log` VALUES (6757, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:28', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
|
||||
INSERT INTO `xxl_job_log` VALUES (6758, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:29', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
|
||||
INSERT INTO `xxl_job_log` VALUES (6759, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:30', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
|
||||
INSERT INTO `xxl_job_log` VALUES (6760, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:31', 500, '任务触发类型:Cron触发<br>调度机器:192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表:null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间:0<br>失败重试次数:0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_log_report
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_log_report`;
|
||||
CREATE TABLE `xxl_job_log_report` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`trigger_day` datetime NULL DEFAULT NULL COMMENT '调度-时间',
|
||||
`running_count` int(11) NOT NULL DEFAULT 0 COMMENT '运行中-日志数量',
|
||||
`suc_count` int(11) NOT NULL DEFAULT 0 COMMENT '执行成功-日志数量',
|
||||
`fail_count` int(11) NOT NULL DEFAULT 0 COMMENT '执行失败-日志数量',
|
||||
`update_time` datetime NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `i_trigger_day`(`trigger_day`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_log_report
|
||||
-- ----------------------------
|
||||
INSERT INTO `xxl_job_log_report` VALUES (1, '2024-08-21 00:00:00', 70, 0, 5, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (2, '2024-08-20 00:00:00', 0, 0, 0, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (3, '2024-08-19 00:00:00', 0, 0, 0, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (4, '2024-09-10 00:00:00', 0, 0, 56, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (5, '2024-09-09 00:00:00', 0, 0, 0, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (6, '2024-09-08 00:00:00', 0, 0, 0, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (7, '2024-10-19 00:00:00', 0, 0, 6391, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (8, '2024-10-18 00:00:00', 0, 0, 0, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (9, '2024-10-17 00:00:00', 0, 0, 0, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (10, '2025-02-10 00:00:00', 0, 0, 116, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (11, '2025-02-09 00:00:00', 0, 0, 0, NULL);
|
||||
INSERT INTO `xxl_job_log_report` VALUES (12, '2025-02-08 00:00:00', 0, 0, 0, NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_logglue
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_logglue`;
|
||||
CREATE TABLE `xxl_job_logglue` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
|
||||
`glue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'GLUE类型',
|
||||
`glue_source` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'GLUE源代码',
|
||||
`glue_remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'GLUE备注',
|
||||
`add_time` datetime NULL DEFAULT NULL,
|
||||
`update_time` datetime NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_logglue
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_registry
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_registry`;
|
||||
CREATE TABLE `xxl_job_registry` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`registry_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`registry_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`registry_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`update_time` datetime NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `i_g_k_v`(`registry_group`, `registry_key`, `registry_value`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_registry
|
||||
-- ----------------------------
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for xxl_job_user
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `xxl_job_user`;
|
||||
CREATE TABLE `xxl_job_user` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '账号',
|
||||
`password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
|
||||
`role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
|
||||
`permission` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `i_username`(`username`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of xxl_job_user
|
||||
-- ----------------------------
|
||||
INSERT INTO `xxl_job_user` VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
oracle导出编码: export NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
|
||||
|
||||
导出用户: jeecgbootos
|
||||
|
||||
导入命令: imp scott/tiger@orcl file=jeecgboot-oracle11g.dmp
|
||||
@ -1,23 +0,0 @@
|
||||
# 版本升级方法
|
||||
|
||||
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
||||
|
||||
### 增量升级方案
|
||||
|
||||
#### 1.代码合并
|
||||
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
||||
|
||||
#### 2.数据库升级
|
||||
- 从3.6.2+版本增加flyway自动升级数据库机制,支持 mysql5.7、mysql8;
|
||||
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
||||
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
||||
|
||||
#### 3.其他数据库脚本说明
|
||||
原先官方默认提供oracle和SqlServer的脚本,但是维护成本太高,未提供脚本的数据库,可以参考下面的文档自己转
|
||||
https://my.oschina.net/jeecg/blog/4905722
|
||||
(注意:定时任务的表qrtz_*,需要删掉用原始的脚本重新执行一下)
|
||||
quartz-2.2.3-distribution.tar.gz放到百度网盘中,大家自己下载,执行所需数据库脚本
|
||||
https://pan.baidu.com/s/1WrmZdUuAPg3iBwJ-LoHWyg?pwd=8mdz
|
||||
|
||||
#### 4.兼容问题
|
||||
每次发版,会针对不兼容地方重点说明。
|
||||
@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<version>3.9.1</version>
|
||||
<version>3.9.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
@ -305,7 +305,7 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- minio文件存储服务 -->
|
||||
<!-- mini文件存储服务 -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
|
||||
@ -33,10 +33,4 @@ public class AiragFlowDTO implements Serializable {
|
||||
* 输入参数
|
||||
*/
|
||||
private Map<String, Object> inputParams;
|
||||
|
||||
/**
|
||||
* 是否流式返回
|
||||
*/
|
||||
private boolean isStream;
|
||||
|
||||
}
|
||||
|
||||
@ -47,8 +47,4 @@ public interface TenantConstant {
|
||||
*/
|
||||
String APP_ADMIN = "appAdmin";
|
||||
|
||||
/**
|
||||
* 增加SignatureCheck注解POST请求的URL
|
||||
*/
|
||||
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
|
||||
}
|
||||
|
||||
@ -3,7 +3,9 @@ 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;
|
||||
@ -11,7 +13,6 @@ 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.*;
|
||||
|
||||
@ -182,10 +183,10 @@ public class ResourceUtil {
|
||||
for (DictModel dm : dictItemList) {
|
||||
String value = dm.getValue();
|
||||
if (keySet.contains(value)) {
|
||||
// 修复bug:获取或创建该dictCode对应的list,而不是每次都创建新的list
|
||||
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
|
||||
List<DictModel> list = new ArrayList<>();
|
||||
list.add(new DictModel(value, dm.getText()));
|
||||
//break;
|
||||
map.put(code, list);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,9 +56,7 @@ 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);
|
||||
//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生成图片的后缀不一致的,导致不展示---
|
||||
fileName += "." + PoiPublicUtil.getFileExtendName(data);
|
||||
try {
|
||||
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
|
||||
File file = new File(basePath + File.separator + bizPath + File.separator );
|
||||
|
||||
@ -46,7 +46,7 @@ public class RestUtil {
|
||||
|
||||
public static String getBaseUrl() {
|
||||
String basepath = getDomain() + getPath();
|
||||
log.debug(" RestUtil.getBaseUrl: " + basepath);
|
||||
log.info(" 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.debug(" RestUtil --- request --- url = "+ url);
|
||||
log.info(" 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.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
log.info(" 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.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||
log.info(" 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.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1231,26 +1231,5 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
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,6 +12,7 @@ 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.1")
|
||||
.version("3.9.0")
|
||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||
.description("后台API接口")
|
||||
.termsOfService("NO terms of service")
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
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,8 +177,6 @@ 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");
|
||||
@ -190,8 +188,6 @@ public class ShiroConfig {
|
||||
// 企业微信证书排除
|
||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
||||
|
||||
filterChainDefinitionMap.put("/openapi/call/**", "anon");
|
||||
|
||||
// 添加自己的过滤器并且取名为jwt
|
||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||
@ -231,11 +227,6 @@ 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);
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
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签名校验失败!";
|
||||
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
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;
|
||||
@ -65,8 +64,6 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
|
||||
//------------------------------------------------------------
|
||||
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
|
||||
registration.addUrlPatterns(signUrlsArray);
|
||||
// 增加注解签名请求
|
||||
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
|
||||
return registration;
|
||||
}
|
||||
|
||||
|
||||
@ -33,104 +33,63 @@ public class SignAuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
|
||||
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);
|
||||
|
||||
try {
|
||||
// 调用验证逻辑
|
||||
validateSignature(request);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 验证失败,返回错误响应
|
||||
log.error("Sign 签名校验失败!{}", e.getMessage());
|
||||
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();
|
||||
Result<?> result = Result.error(e.getMessage());
|
||||
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);
|
||||
return true;
|
||||
} else {
|
||||
log.debug("sign allParams: {}", allParams);
|
||||
log.error("request URI = " + request.getRequestURI());
|
||||
log.error("Sign 签名校验失败!Header Sign : {}",headerSign);
|
||||
//校验失败返回前端
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json; charset=utf-8");
|
||||
PrintWriter out = response.getWriter();
|
||||
Result<?> result = Result.error("Sign签名校验失败!");
|
||||
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, Object bodyParam) throws IOException {
|
||||
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
|
||||
|
||||
SortedMap<String, String> result = new TreeMap<>();
|
||||
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
|
||||
@ -65,13 +65,7 @@ public class HttpUtils {
|
||||
Map<String, String> allRequestParam = new HashMap<>(16);
|
||||
// get请求不需要拿body参数
|
||||
if (!HttpMethod.GET.name().equals(request.getMethod())) {
|
||||
if (bodyParam != null) {
|
||||
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
|
||||
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
|
||||
} else {
|
||||
allRequestParam = getAllRequestParam(request);
|
||||
}
|
||||
allRequestParam = getAllRequestParam(request);
|
||||
}
|
||||
// 将URL的参数和body参数进行合并
|
||||
if (allRequestParam != null) {
|
||||
|
||||
@ -11,12 +11,7 @@ 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;
|
||||
|
||||
/**
|
||||
* 签名工具类
|
||||
@ -54,7 +49,12 @@ public class SignUtil {
|
||||
String paramsJsonStr = JSONObject.toJSONString(params);
|
||||
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
|
||||
//设置签名秘钥
|
||||
String signatureSecret = SignUtil.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} 缺少配置 !!");
|
||||
}
|
||||
try {
|
||||
//【issues/I484RW】2.4.6部署后,下拉搜索框提示“sign签名检验失败”
|
||||
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
|
||||
@ -63,129 +63,4 @@ 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,429 +0,0 @@
|
||||
#!/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.1</version>
|
||||
<version>3.9.0</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>3.2.3</apache-tika.version>
|
||||
<apache-tika.version>2.9.1</apache-tika.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -41,14 +41,14 @@
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-bom</artifactId>
|
||||
<version>1.9.1</version>
|
||||
<version>1.3.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-community-bom</artifactId>
|
||||
<version>1.9.1-beta17</version>
|
||||
<version>1.3.0-beta9</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.1-beta</version>
|
||||
<version>3.9.0.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-io</groupId>
|
||||
@ -107,16 +107,16 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-groovy</artifactId>
|
||||
<artifactId>liteflow-script-python</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>runtime</scope>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||
|
||||
<!-- aiflow 脚本依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-python</artifactId>
|
||||
<artifactId>liteflow-script-groovy</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
@ -151,11 +151,6 @@
|
||||
<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>
|
||||
@ -202,11 +197,7 @@
|
||||
<artifactId>langchain4j-pgvector</artifactId>
|
||||
<version>1.3.0-beta9</version>
|
||||
</dependency>
|
||||
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
|
||||
</dependency>
|
||||
<!-- langChain4j Document Parser -->
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
@ -233,12 +224,7 @@
|
||||
<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,19 +44,4 @@ 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,68 +104,4 @@ 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,6 +22,7 @@ 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应用
|
||||
@ -178,16 +179,4 @@ 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,7 +8,6 @@ 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;
|
||||
@ -103,19 +102,6 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
@ -127,22 +113,7 @@ public class AiragChatController {
|
||||
@IgnoreAuth
|
||||
@DeleteMapping(value = "/conversation/{id}")
|
||||
public Result<?> deleteConversation(@PathVariable("id") String 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);
|
||||
return chatService.deleteConversation(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,9 +139,8 @@ public class AiragChatController {
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages")
|
||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
|
||||
@RequestParam(value = "sessionType", required = false) String sessionType) {
|
||||
return chatService.getMessages(conversationId, sessionType);
|
||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
||||
return chatService.getMessages(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,21 +153,7 @@ public class AiragChatController {
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String 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);
|
||||
return chatService.clearMessage(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -261,25 +217,4 @@ 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,29 +173,6 @@ 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,6 +1,7 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -20,14 +21,4 @@ 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,7 +1,6 @@
|
||||
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;
|
||||
@ -60,23 +59,21 @@ public interface IAiragChatService {
|
||||
* 获取对话聊天记录
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType 类型
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/26 15:16
|
||||
*/
|
||||
Result<?> getMessages(String conversationId, String sessionType);
|
||||
Result<?> getMessages(String conversationId);
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 16:55
|
||||
*/
|
||||
Result<?> deleteConversation(String conversationId, String sessionType);
|
||||
Result<?> deleteConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
@ -90,12 +87,11 @@ public interface IAiragChatService {
|
||||
/**
|
||||
* 清空消息
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 19:49
|
||||
*/
|
||||
Result<?> clearMessage(String conversationId, String sessionType);
|
||||
Result<?> clearMessage(String conversationId);
|
||||
|
||||
/**
|
||||
* 初始化聊天(忽略租户)
|
||||
@ -115,27 +111,4 @@ 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);
|
||||
}
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
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;
|
||||
@ -11,15 +10,12 @@ 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;
|
||||
@ -27,8 +23,6 @@ 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;
|
||||
@ -37,7 +31,6 @@ 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应用
|
||||
@ -52,9 +45,6 @@ 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);
|
||||
@ -72,167 +62,81 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}else{
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||
return startSseChat(messages, params);
|
||||
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||
@Override
|
||||
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
|
||||
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
|
||||
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
|
||||
}
|
||||
// 构建变量描述
|
||||
StringBuilder variablesDesc = new StringBuilder();
|
||||
if (oConvertUtils.isNotEmpty(variables)) {
|
||||
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
|
||||
if (variableList != null && !variableList.isEmpty()) {
|
||||
for (AppVariableVo var : variableList) {
|
||||
if (var.getEnable() != null && !var.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
String name = var.getName();
|
||||
if (oConvertUtils.isNotEmpty(var.getAction())) {
|
||||
String action = var.getAction();
|
||||
if (oConvertUtils.isNotEmpty(name)) {
|
||||
try {
|
||||
// 使用正则替换未被{{}}包裹的变量名
|
||||
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
|
||||
action = action.replaceAll(regex, "{{" + name + "}}");
|
||||
} catch (Exception e) {
|
||||
log.warn("变量名替换异常: name={}", name, e);
|
||||
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> ";
|
||||
}
|
||||
}
|
||||
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);
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
||||
EventMessageData messageEventData = EventMessageData.builder()
|
||||
.message(resMessage)
|
||||
.build();
|
||||
eventData.setData(messageEventData);
|
||||
try {
|
||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
closeSSE(emitter, eventData);
|
||||
} else {
|
||||
// 异常结束
|
||||
log.error("调用模型异常:" + respText);
|
||||
if (respText.contains("insufficient Balance")) {
|
||||
respText = "大预言模型账号余额不足!";
|
||||
})
|
||||
.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(respText).build());
|
||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).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;
|
||||
})
|
||||
.start();
|
||||
return emitter;
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||
|
||||
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||
try {
|
||||
|
||||
@ -1,36 +1,24 @@
|
||||
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;
|
||||
@ -47,26 +35,18 @@ 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;
|
||||
@ -105,18 +85,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
|
||||
@Autowired
|
||||
AiragModelMapper airagModelMapper;
|
||||
|
||||
@Autowired
|
||||
IAiragFlowPluginService airagFlowPluginService;
|
||||
|
||||
@Autowired
|
||||
IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Autowired
|
||||
IAiragVariableService airagVariableService;
|
||||
|
||||
@Autowired
|
||||
JeecgBaseConfig jeecgBaseConfig;
|
||||
|
||||
/**
|
||||
* 重新接收消息
|
||||
@ -137,13 +105,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
||||
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
||||
}
|
||||
//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应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
|
||||
// 更新标题
|
||||
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||
chatConversation.setTitle(userMessage.length() > maxLength ? userMessage.substring(0, maxLength) : userMessage);
|
||||
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
||||
}
|
||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
// 保存工作流入参配置(如果有)
|
||||
@ -151,12 +116,6 @@ 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);
|
||||
}
|
||||
@ -171,9 +130,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
||||
AiragApp app = appDebugParams.getApp();
|
||||
app.setId("__DEBUG_APP");
|
||||
//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应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
// 保存工作流入参配置(如果有)
|
||||
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
||||
@ -183,9 +140,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 发送消息
|
||||
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
||||
//保存会话
|
||||
//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应用门户---
|
||||
saveChatConversation(chatConversation, true, null);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@ -292,11 +247,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> getMessages(String conversationId, String sessionType) {
|
||||
public Result<?> getMessages(String conversationId) {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||
//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应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
@ -320,7 +273,6 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
.role(msg.getRole())
|
||||
.content(msg.getContent())
|
||||
.images(msg.getImages())
|
||||
.files(msg.getFiles())
|
||||
.datetime(msg.getDatetime())
|
||||
.build();
|
||||
// 不设置toolExecutionRequests和toolExecutionResult
|
||||
@ -330,30 +282,21 @@ 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, String sessionType) {
|
||||
public Result<?> clearMessage(String conversationId) {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||
//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应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
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();
|
||||
//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应用门户---
|
||||
saveChatConversation(chatConversation);
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
@ -500,11 +443,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> deleteConversation(String conversationId, String sessionType) {
|
||||
public Result<?> deleteConversation(String conversationId) {
|
||||
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
||||
//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应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
if (oConvertUtils.isNotEmpty(key)) {
|
||||
Boolean delete = redisTemplate.delete(key);
|
||||
if (delete) {
|
||||
@ -522,18 +463,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
||||
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
||||
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType());
|
||||
String key = getConversationCacheKey(updateTitleParams.getId(), null);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
||||
return Result.ok();
|
||||
}
|
||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (chatConversation != null) {
|
||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||
}
|
||||
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||
saveChatConversation(chatConversation);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -542,21 +479,15 @@ 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, String sessionType) {
|
||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
|
||||
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;
|
||||
}
|
||||
@ -591,21 +522,18 @@ 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, String sessionType) {
|
||||
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
|
||||
if (oConvertUtils.isObjectEmpty(app)) {
|
||||
app = new AiragApp();
|
||||
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
||||
}
|
||||
ChatConversation chatConversation = 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应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
if (oConvertUtils.isNotEmpty(key)) {
|
||||
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
}
|
||||
@ -641,8 +569,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private void saveChatConversation(ChatConversation chatConversation, String sessionType) {
|
||||
saveChatConversation(chatConversation, false, null, sessionType);
|
||||
private void saveChatConversation(ChatConversation chatConversation) {
|
||||
saveChatConversation(chatConversation, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -653,19 +581,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest, String sessionType) {
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
|
||||
if (null == chatConversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
//如果是不保存会话直接返回
|
||||
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应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return;
|
||||
}
|
||||
@ -760,10 +680,6 @@ 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)) {
|
||||
// 系统消息,放到消息列表最前面,并且不记录历史
|
||||
@ -793,22 +709,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
textContent.append(((TextContent) content).text()).append("\n");
|
||||
}
|
||||
});
|
||||
//update-begin---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
if (oConvertUtils.isNotEmpty(saveContent)) {
|
||||
historyMessage.setContent(saveContent);
|
||||
} else {
|
||||
historyMessage.setContent(textContent.toString());
|
||||
}
|
||||
historyMessage.setContent(textContent.toString());
|
||||
historyMessage.setImages(images);
|
||||
// 保存文件信息
|
||||
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;
|
||||
@ -864,20 +766,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
|
||||
try {
|
||||
// 组装用户消息
|
||||
String content = sendParams.getContent();
|
||||
//将文件内容给提示词
|
||||
if(!CollectionUtils.isEmpty(sendParams.getFiles())){
|
||||
content = buildContentWithFiles(content, sendParams.getFiles());
|
||||
}
|
||||
UserMessage userMessage = aiChatHandler.buildUserMessage(content, sendParams.getImages());
|
||||
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
|
||||
// 追加消息
|
||||
//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);
|
||||
}
|
||||
appendMessage(messages, userMessage, chatConversation, topicId);
|
||||
/* 这里应该是有几种情况:
|
||||
* 1. 非ai应用:获取默认模型->开始聊天
|
||||
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
||||
@ -890,7 +781,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
||||
} else {
|
||||
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId());
|
||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
|
||||
}
|
||||
} else {
|
||||
// 发消息
|
||||
@ -898,13 +789,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||
}
|
||||
// 设置深度思考搜索参数
|
||||
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应用门户---
|
||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
|
||||
}
|
||||
// 发送就绪消息
|
||||
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
||||
@ -919,59 +804,6 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行流程
|
||||
*
|
||||
@ -1043,9 +875,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
sendMessage2Client(emitter, msgEventData);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
//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应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
}
|
||||
}else{
|
||||
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||
@ -1078,31 +908,16 @@ 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, String flowId, String memoryId) {
|
||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
|
||||
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);
|
||||
}
|
||||
@ -1128,9 +943,6 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1152,70 +964,16 @@ 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, "构造应用自定义参数完成");
|
||||
// 发消息
|
||||
//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);
|
||||
}
|
||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1226,12 +984,11 @@ 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, String sessionType) {
|
||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
|
||||
// 调用ai聊天
|
||||
if (null == aiChatParams) {
|
||||
aiChatParams = new AIChatParams();
|
||||
@ -1240,16 +997,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
|
||||
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
|
||||
}
|
||||
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){
|
||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||
} else {
|
||||
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
|
||||
}
|
||||
aiChatParams.setKnowIds(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 {
|
||||
// 打印流程耗时日志
|
||||
@ -1261,8 +1013,6 @@ 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) {
|
||||
@ -1348,8 +1098,6 @@ 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();
|
||||
@ -1365,9 +1113,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
//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应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
closeSSE(emitter, eventData);
|
||||
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
||||
// 上下文长度超过限制
|
||||
@ -1391,8 +1137,6 @@ 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) {
|
||||
@ -1457,7 +1201,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
*/
|
||||
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
||||
try {
|
||||
log.debug("发送消息:{}", eventData.getRequestId());
|
||||
log.info("发送消息:{}", eventData.getRequestId());
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
@ -1507,9 +1251,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
||||
return;
|
||||
}
|
||||
//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应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), null);
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return;
|
||||
}
|
||||
@ -1539,13 +1281,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
||||
cachedConversation.setTitle(summaryTitle);
|
||||
} else {
|
||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||
cachedConversation.setTitle(question.length() > maxLength ? question.substring(0, maxLength) : question);
|
||||
cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
|
||||
}
|
||||
//保存会话
|
||||
//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应用门户---
|
||||
saveChatConversation(cachedConversation);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1590,296 +1329,4 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
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,14 +47,4 @@ public class ChatConversation {
|
||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||
*/
|
||||
private Map<String, Object> flowInputs;
|
||||
|
||||
/**
|
||||
* portal 应用门户
|
||||
*/
|
||||
private String sessionType;
|
||||
|
||||
/**
|
||||
* 是否保存会话
|
||||
*/
|
||||
private Boolean izSaveSession;
|
||||
}
|
||||
|
||||
@ -47,11 +47,6 @@ public class ChatSendParams {
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private List<String> files;
|
||||
|
||||
/**
|
||||
* 工作流额外入参配置
|
||||
* key: 参数field, value: 参数值
|
||||
@ -64,39 +59,4 @@ 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,6 +1,5 @@
|
||||
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;
|
||||
|
||||
@ -12,15 +11,12 @@ 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());
|
||||
}
|
||||
}
|
||||
@ -1,209 +0,0 @@
|
||||
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,8 +1,5 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@ -38,11 +35,6 @@ public class LLMConsts {
|
||||
*/
|
||||
public static final String MODEL_TYPE_LLM = "LLM";
|
||||
|
||||
/**
|
||||
* 模型类型: 图像生成
|
||||
*/
|
||||
public static final String MODEL_TYPE_IMAGE = "IMAGE";
|
||||
|
||||
/**
|
||||
* 向量模型:默认维度
|
||||
*/
|
||||
@ -93,29 +85,4 @@ 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;
|
||||
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
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,18 +1,14 @@
|
||||
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;
|
||||
@ -26,6 +22,7 @@ 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;
|
||||
@ -83,9 +80,6 @@ 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("添加成功!");
|
||||
}
|
||||
@ -107,9 +101,6 @@ 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())) {
|
||||
// 更新了模型,重建文档
|
||||
@ -366,62 +357,5 @@ 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,7 +17,6 @@ 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;
|
||||
@ -173,16 +172,11 @@ 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 if(LLMConsts.MODEL_TYPE_EMBED.equals(airagModel.getModelType())){
|
||||
}else{
|
||||
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("测试模型连接失败,请检查模型配置是否正确!");
|
||||
|
||||
@ -7,7 +7,6 @@ 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;
|
||||
@ -31,10 +30,7 @@ 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;
|
||||
|
||||
@ -55,8 +51,6 @@ 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);
|
||||
@ -77,16 +71,22 @@ 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);
|
||||
//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 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);
|
||||
} else {
|
||||
throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName));
|
||||
}
|
||||
@ -95,27 +95,6 @@ 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,11 +102,4 @@ 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;
|
||||
}
|
||||
|
||||
@ -12,16 +12,13 @@ import org.jeecg.ai.handler.LLMHandler;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
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;
|
||||
@ -57,8 +54,6 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
@Autowired
|
||||
LLMHandler llmHandler;
|
||||
|
||||
@Autowired
|
||||
AiRagConfigBean aiRagConfigBean;
|
||||
|
||||
@Value(value = "${jeecg.path.upload:}")
|
||||
private String uploadpath;
|
||||
@ -290,7 +285,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
|
||||
// 默认超时时间
|
||||
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
|
||||
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT);
|
||||
params.setTimeout(60);
|
||||
}
|
||||
|
||||
//deepseek-reasoner 推理模型不支持插件tool
|
||||
@ -306,7 +301,6 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
/**
|
||||
* 构造插件和MCP工具
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
|
||||
* @param params
|
||||
* @author chenrui
|
||||
* @date 2025/10/31 14:04
|
||||
@ -316,7 +310,6 @@ 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())) {
|
||||
@ -332,18 +325,15 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
}
|
||||
|
||||
if ("mcp".equalsIgnoreCase(category)) {
|
||||
// MCP类型:构建McpToolProviderWrapper(包含连接引用用于后续关闭)
|
||||
// for [QQYUN-9234] MCP服务连接关闭
|
||||
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
|
||||
// MCP类型:构建McpToolProvider
|
||||
McpToolProvider mcpToolProvider = buildMcpToolProvider(
|
||||
airagMcp.getName(),
|
||||
airagMcp.getType(),
|
||||
airagMcp.getEndpoint(),
|
||||
airagMcp.getHeaders(),
|
||||
aiRagConfigBean.getAllowSensitiveNodes()
|
||||
airagMcp.getHeaders()
|
||||
);
|
||||
if (wrapper != null) {
|
||||
mcpToolProviders.add(wrapper.getMcpToolProvider());
|
||||
mcpToolProviderWrappers.add(wrapper);
|
||||
if (mcpToolProvider != null) {
|
||||
mcpToolProviders.add(mcpToolProvider);
|
||||
}
|
||||
} else if ("plugin".equalsIgnoreCase(category)) {
|
||||
// 插件类型:构建ToolSpecification和ToolExecutor
|
||||
@ -358,12 +348,6 @@ 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()) {
|
||||
@ -417,129 +401,5 @@ 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 (errMsg.contains(key)) {
|
||||
errMsg = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("AI模型调用异常: {}", errMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 图生图
|
||||
*
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param images
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> imageEdit(String modelId, String messages, List<String> images, AIChatParams params) {
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
params = mergeParams(airagModel, params);
|
||||
List<String> originalImageBase64List = getFirstImageBase64(images);
|
||||
try {
|
||||
return llmHandler.imageEdit(messages, originalImageBase64List, params);
|
||||
} catch (Exception e) {
|
||||
String errMsg = "调用绘画AI接口失败,详情请查看后台日志。";
|
||||
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||
// 根据常见异常关键字做细致翻译
|
||||
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (errMsg.contains(key)) {
|
||||
errMsg = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("AI模型调用异常: {}", errMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要将图片转换成Base64编码
|
||||
* @param images 图片路径列表
|
||||
* @return Base64编码字符串
|
||||
*/
|
||||
private List<String> getFirstImageBase64(List<String> images) {
|
||||
List<String> originalImageBase64List = new ArrayList<>();
|
||||
if (images != null && !images.isEmpty()) {
|
||||
for (String imageUrl : images) {
|
||||
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(imageUrl);
|
||||
try {
|
||||
byte[] fileContent;
|
||||
if (matcher.matches()) {
|
||||
// 来源于网络
|
||||
java.net.URL url = new java.net.URL(imageUrl);
|
||||
java.net.URLConnection conn = url.openConnection();
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(10000);
|
||||
try (java.io.InputStream in = conn.getInputStream()) {
|
||||
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
||||
int nRead;
|
||||
byte[] data = new byte[1024];
|
||||
while ((nRead = in.read(data, 0, data.length)) != -1) {
|
||||
buffer.write(data, 0, nRead);
|
||||
}
|
||||
buffer.flush();
|
||||
fileContent = buffer.toByteArray();
|
||||
}
|
||||
} else {
|
||||
// 本地文件
|
||||
String filePath = uploadpath + File.separator + imageUrl;
|
||||
Path path = Paths.get(filePath);
|
||||
fileContent = Files.readAllBytes(path);
|
||||
}
|
||||
originalImageBase64List.add(Base64.getEncoder().encodeToString(fileContent));
|
||||
} catch (Exception e) {
|
||||
log.error("图片读取失败: " + imageUrl, e);
|
||||
throw new JeecgBootException("图片读取失败: " + imageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalImageBase64List;
|
||||
}
|
||||
//================================================= end 【QQYUN-12145】【AI】AI 绘画创作 ========================================
|
||||
}
|
||||
|
||||
@ -17,17 +17,13 @@ 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;
|
||||
@ -98,21 +94,11 @@ 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
|
||||
*/
|
||||
@ -123,11 +109,6 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
|
||||
|
||||
/**
|
||||
* 向量存储元数据:创建时间
|
||||
*/
|
||||
public static final String EMBED_STORE_CREATE_TIME = "createTime";
|
||||
|
||||
/**
|
||||
* 向量存储缓存
|
||||
*/
|
||||
@ -194,26 +175,7 @@ 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_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】支持记忆---
|
||||
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()));
|
||||
Document from = Document.from(content, metadata);
|
||||
ingestor.ingest(from);
|
||||
return metadata.toMap();
|
||||
@ -246,47 +208,16 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
//命中的文档内容
|
||||
StringBuilder data = new StringBuilder();
|
||||
//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())
|
||||
// 对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"))
|
||||
.collect(Collectors.toList());
|
||||
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的时候需要限制几个字---
|
||||
|
||||
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -313,31 +244,11 @@ 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(filter)
|
||||
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||
.build();
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
@ -351,9 +262,6 @@ 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());
|
||||
}
|
||||
@ -387,32 +295,13 @@ 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(filter)
|
||||
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||
.build();
|
||||
retrievers.add(contentRetriever);
|
||||
}
|
||||
|
||||
@ -12,8 +12,6 @@ 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;
|
||||
@ -53,9 +51,8 @@ public class PluginToolBuilder {
|
||||
}
|
||||
|
||||
String baseUrl = airagMcp.getEndpoint();
|
||||
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
|
||||
// 如果baseUrl为空,使用当前系统地址
|
||||
if (isEmptyBaseUrl) {
|
||||
if (oConvertUtils.isEmpty(baseUrl)) {
|
||||
if (currentHttpRequest != null) {
|
||||
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
|
||||
log.info("插件[{}]的BaseURL为空,使用系统地址: {}", airagMcp.getName(), baseUrl);
|
||||
@ -67,10 +64,7 @@ public class PluginToolBuilder {
|
||||
|
||||
// 解析headers
|
||||
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
|
||||
|
||||
// 判断是否需要加签
|
||||
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
|
||||
|
||||
|
||||
// 解析并应用授权配置(从metadata中读取)
|
||||
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
|
||||
|
||||
@ -82,7 +76,7 @@ public class PluginToolBuilder {
|
||||
|
||||
try {
|
||||
ToolSpecification spec = buildToolSpecification(toolConfig);
|
||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign);
|
||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
|
||||
if (spec != null && executor != null) {
|
||||
tools.put(spec, executor);
|
||||
}
|
||||
@ -193,7 +187,7 @@ public class PluginToolBuilder {
|
||||
/**
|
||||
* 构建ToolExecutor
|
||||
*/
|
||||
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders, boolean isNeedSign) {
|
||||
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders) {
|
||||
String path = toolConfig.getString("path");
|
||||
String method = toolConfig.getString("method");
|
||||
JSONArray parameters = toolConfig.getJSONArray("parameters");
|
||||
@ -221,13 +215,8 @@ public class PluginToolBuilder {
|
||||
JSONObject urlVariables = buildUrlVariables(parameters, args);
|
||||
Object body = buildRequestBody(parameters, args, httpHeaders);
|
||||
|
||||
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);
|
||||
// 发送HTTP请求
|
||||
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
|
||||
|
||||
// 直接返回原始响应字符串,不进行解析
|
||||
return response.getBody() != null ? response.getBody() : "";
|
||||
@ -346,13 +335,7 @@ public class PluginToolBuilder {
|
||||
if (isQueryParam || !isOtherType) {
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
//如果是知识库的id赋值默认值
|
||||
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
urlVariables.put(paramName, defaultValue);
|
||||
} else {
|
||||
urlVariables.put(paramName, value);
|
||||
}
|
||||
urlVariables.put(paramName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -409,13 +392,7 @@ public class PluginToolBuilder {
|
||||
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
//如果是知识库的id赋值默认值
|
||||
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
body.put(paramName, defaultValue);
|
||||
} else {
|
||||
body.put(paramName, value);
|
||||
}
|
||||
body.put(paramName, value);
|
||||
} else {
|
||||
// 检查是否有默认值
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
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,8 +3,6 @@ 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知识库
|
||||
*
|
||||
@ -13,12 +11,4 @@ import java.util.Map;
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
|
||||
|
||||
/**
|
||||
* 构建知识库的工具
|
||||
*
|
||||
* @param memoryId
|
||||
* @return Map<String, Object>
|
||||
*/
|
||||
Map<String, Object> getPluginMemory(String memoryId);
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,231 +0,0 @@
|
||||
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,6 +131,7 @@ 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 -> {
|
||||
@ -329,7 +330,6 @@ 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,215 +1,18 @@
|
||||
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,14 +10,11 @@ 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;
|
||||
@ -25,7 +22,6 @@ 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;
|
||||
|
||||
@ -35,15 +31,12 @@ import java.util.stream.Collectors;
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@SuppressWarnings("removal")
|
||||
@Service("airagMcpServiceImpl")
|
||||
@Slf4j
|
||||
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
|
||||
@Autowired
|
||||
private AiRagConfigBean aiRagConfigBean;
|
||||
|
||||
/**
|
||||
* 新增或编辑Mcpserver
|
||||
@ -132,7 +125,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(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {});
|
||||
headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
|
||||
} catch (JSONException e) {
|
||||
headers = null;
|
||||
}
|
||||
@ -143,53 +136,19 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
||||
McpClient mcpClient = null;
|
||||
try {
|
||||
if ("sse".equalsIgnoreCase(type)) {
|
||||
//TODO 1.4.0-beta10被弃用,推荐使用http
|
||||
log.info("[MCP]使用SSE协议(HttpMcpTransport), endpoint:{}", endpoint);
|
||||
HttpMcpTransport.Builder builder = HttpMcpTransport.builder()
|
||||
HttpMcpTransport.Builder builder = new 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)) {
|
||||
//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);
|
||||
// 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);
|
||||
} else {
|
||||
return Result.error("不支持的MCP类型:" + type);
|
||||
}
|
||||
@ -245,29 +204,6 @@ 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 {
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
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";
|
||||
|
||||
}
|
||||
@ -1,213 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,167 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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> {
|
||||
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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> {
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,394 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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;
|
||||
|
||||
}
|
||||
@ -1,244 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
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";
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
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<>();
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
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<>();
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
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;
|
||||
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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> {
|
||||
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
@ -1,26 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
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.1</version>
|
||||
<version>3.9.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@ -1,326 +0,0 @@
|
||||
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 填 \"测试用户\""
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user