Compare commits

..

13 Commits

311 changed files with 48583 additions and 112606 deletions

View File

@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform
===============
Current version: 3.9.0 (Release date: 2025-12-01)
Current version: 3.9.1 (Release date: 2026-01-22)
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.9.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.9.1-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@ -56,7 +56,9 @@ public class CommonUtils {
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
String dbPath = null;
String fileName = "image" + Math.round(Math.random() * 100000000000L);
fileName += "." + PoiPublicUtil.getFileExtendName(data);
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示---
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示---
try {
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
File file = new File(basePath + File.separator + bizPath + File.separator );

View File

@ -46,7 +46,7 @@ public class RestUtil {
public static String getBaseUrl() {
String basepath = getDomain() + getPath();
log.info(" RestUtil.getBaseUrl: " + basepath);
log.debug(" RestUtil.getBaseUrl: " + basepath);
return basepath;
}
@ -199,7 +199,7 @@ public class RestUtil {
* @return ResponseEntity<responseType>
*/
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
log.info(" RestUtil --- request --- url = "+ url);
log.debug(" RestUtil --- request --- url = "+ url);
if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空");
}
@ -230,7 +230,7 @@ public class RestUtil {
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength);
log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
log.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
}
}
// 发送请求
@ -252,7 +252,7 @@ public class RestUtil {
*/
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
JSONObject variables, Object params, Class<T> responseType, int timeout) {
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
log.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空");
@ -302,7 +302,7 @@ public class RestUtil {
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength);
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
log.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
}
}

View File

@ -1231,5 +1231,26 @@ public class oConvertUtils {
.filter(name -> beanWrapper.getPropertyValue(name) == null)
.toArray(String[]::new);
}
/**
* String转换long类型
*
* @param v
* @param def
* @return
*/
public static long getLong(Object v, long def) {
if (v == null) {
return def;
};
if (v instanceof Number) {
return ((Number) v).longValue();
}
try {
return Long.parseLong(v.toString());
} catch (Exception e) {
return def;
}
}
}

View File

@ -0,0 +1,24 @@
package org.jeecg.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* ai配置类通用的配置可以放到这里面
*
* @Author: wangshuai
* @Date: 2025/12/17 14:00
*/
@Data
@Component
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
public class AiRagConfigBean {
public static final String PREFIX = "jeecg.airag";
/**
* 敏感节点
* stdio mpc命令行功能开启sqlAI流程SQL节点开启
*/
private String allowSensitiveNodes = "";
}

View File

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

View File

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

View File

@ -0,0 +1,32 @@
package org.jeecg.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 任务调度器配置
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
*
* @author jeecg
*/
@Slf4j
@Configuration
public class TaskSchedulerConfig {
@Bean
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
public ThreadPoolTaskScheduler taskScheduler() {
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("airag-scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
return scheduler;
}
}

View File

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

View File

@ -1,6 +1,5 @@
package org.jeecg.config.sign.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
@ -13,7 +12,12 @@ 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的签名验证切面
@ -56,7 +60,22 @@ public class SignatureCheckAspect {
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("无法获取请求上下文");
@ -68,7 +87,7 @@ public class SignatureCheckAspect {
try {
// 直接调用SignAuthInterceptor的验证逻辑
signAuthInterceptor.validateSignature(request);
signAuthInterceptor.validateSignature(request, bodyParam);
log.info("AOP签名验证通过");
} catch (IllegalArgumentException e) {

View File

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

View File

@ -1,10 +1,12 @@
package org.jeecg.config.sign.interceptor;
import com.alibaba.fastjson.JSON;
import java.io.PrintWriter;
import java.util.SortedMap;
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.constant.CommonConstant;
import org.jeecg.common.util.DateUtils;
@ -14,8 +16,9 @@ import org.jeecg.config.sign.util.HttpUtils;
import org.jeecg.config.sign.util.SignUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.PrintWriter;
import java.util.SortedMap;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
/**
* 签名拦截器
@ -47,7 +50,7 @@ public class SignAuthInterceptor implements HandlerInterceptor {
return false;
}
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
@ -55,12 +58,22 @@ public class SignAuthInterceptor implements HandlerInterceptor {
* @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);
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
log.debug("提取参数: {}", allParams);
//对参数进行签名验证

View File

@ -35,7 +35,7 @@ public class HttpUtils {
* @date 20210621
* @param request
*/
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException {
SortedMap<String, String> result = new TreeMap<>();
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
@ -65,7 +65,13 @@ public class HttpUtils {
Map<String, String> allRequestParam = new HashMap<>(16);
// get请求不需要拿body参数
if (!HttpMethod.GET.name().equals(request.getMethod())) {
allRequestParam = getAllRequestParam(request);
if (bodyParam != null) {
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
} else {
allRequestParam = getAllRequestParam(request);
}
}
// 将URL的参数和body参数进行合并
if (allRequestParam != null) {

View File

@ -11,7 +11,12 @@ import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 签名工具类
@ -49,12 +54,7 @@ public class SignUtil {
String paramsJsonStr = JSONObject.toJSONString(params);
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
//设置签名秘钥
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
String signatureSecret = SignUtil.getSignatureSecret();
try {
//【issues/I484RW】2.4.6部署后下拉搜索框提示“sign签名检验失败”
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
@ -63,4 +63,129 @@ public class SignUtil {
return null;
}
}
}
/**
* 通过前端签名算法生成签名
*
* @param url 请求的完整URL包含查询参数
* @param requestParams 使用 @RequestParam 获取的参数集合
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
* @return 计算得到的签名大写MD5若参数不足返回 null
*/
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
if (oConvertUtils.isEmpty(url)) {
return null;
}
// 解析URL上的查询参数与路径变量
Map<String, String> urlParams = parseQueryString(url);
// 合并URL参数与@RequestParam参数确保数值和布尔类型转换为字符串
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
// 按需合并@RequestBody参数
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
mergedParams = mergeObject(mergedParams, requestBodyParams);
}
// 按键名升序排序,保持与前端一致的签名顺序
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
// 去除时间戳字段,避免参与签名
sortedParams.remove("_t");
// 序列化为JSON字符串
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
// 读取签名秘钥
String signatureSecret = getSignatureSecret();
// 计算MD5摘要并转大写
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
}
/**
* 解析URL中的查询参数并处理末尾逗号分隔的路径变量片段。
*
* @param url 请求的完整URL
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
*/
private static Map<String, String> parseQueryString(String url) {
Map<String, String> result = new HashMap<>(16);
int fragmentIndex = url.indexOf('#');
if (fragmentIndex >= 0) {
url = url.substring(0, fragmentIndex);
}
int questionIndex = url.indexOf('?');
String paramString = null;
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
paramString = url.substring(questionIndex + 1);
}
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
String lastPathVariable = url.substring(lastSlashIndex + 1);
int qIndexInPath = lastPathVariable.indexOf('?');
if (qIndexInPath >= 0) {
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
}
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
result.put(X_PATH_VARIABLE, decodedPathVariable);
}
}
if (oConvertUtils.isNotEmpty(paramString)) {
String[] pairs = paramString.split(SymbolConstant.AND);
for (String pair : pairs) {
int equalIndex = pair.indexOf('=');
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
String key = pair.substring(0, equalIndex);
String value = pair.substring(equalIndex + 1);
// 解码并统一类型为字符串
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
result.put(decodedKey, decodedValue);
}
}
}
return result;
}
/**
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
*
* @param target 初始参数映射
* @param source 待合并的参数映射
* @return 合并后的新映射
*/
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
Map<String, String> merged = new HashMap<>(16);
if (target != null && !target.isEmpty()) {
merged.putAll(target);
}
if (source != null && !source.isEmpty()) {
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Number) {
// 数值类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value instanceof Boolean) {
// 布尔类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value != null) {
merged.put(key, String.valueOf(value));
}
}
}
return merged;
}
/**
* 读取并校验签名秘钥配置。
*
* @return 有效的签名秘钥
*/
private static String getSignatureSecret() {
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
return signatureSecret;
}
}

View File

@ -0,0 +1,429 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP Stdio 工具 - 修复编码问题
确保所有输出都使用UTF-8编码
"""
import json
import sys
import os
from typing import Dict, Any
import logging
# 强制使用UTF-8编码
if sys.platform == "win32":
# Windows需要特殊处理
import io
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
else:
# Unix-like系统
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
# 设置环境变量
os.environ['PYTHONIOENCODING'] = 'utf-8'
os.environ['PYTHONUTF8'] = '1'
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
encoding='utf-8'
)
logger = logging.getLogger("mcp-tool")
class FixedMCPServer:
"""修复编码问题的MCP服务器"""
def __init__(self):
self.tools = {}
self.initialize_tools()
def initialize_tools(self):
"""初始化工具集"""
# 获取时间
self.tools["get_time"] = {
"name": "get_time",
"description": "获取当前时间",
"inputSchema": {
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "时间格式",
"enum": ["iso", "timestamp", "human", "chinese"],
"default": "iso"
}
}
}
}
# 文本处理工具
self.tools["text_process"] = {
"name": "text_process",
"description": "文本处理工具",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "输入文本"
},
"operation": {
"type": "string",
"description": "操作类型",
"enum": ["length", "upper", "lower", "reverse", "count_words"],
"default": "length"
}
},
"required": ["text"]
}
}
# 数据格式工具
self.tools["format_data"] = {
"name": "format_data",
"description": "格式化数据",
"inputSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"description": "原始数据"
},
"format": {
"type": "string",
"description": "格式类型",
"enum": ["json", "yaml", "xml"],
"default": "json"
}
},
"required": ["data"]
}
}
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""处理请求"""
try:
method = request.get("method")
params = request.get("params", {})
if method == "tools/list":
return self.handle_tools_list()
elif method == "tools/call":
return self.handle_tool_call(params)
elif method == "ping":
return {"result": "pong"}
else:
return self.create_error_response(
code=-32601,
message="Method not found"
)
except Exception as e:
logger.error(f"Error handling request: {e}")
return self.create_error_response(
code=-32603,
message=f"Internal error: {str(e)}"
)
def handle_tools_list(self) -> Dict[str, Any]:
"""列出所有工具 - 确保返回标准JSON"""
return {
"result": {
"tools": list(self.tools.values())
}
}
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""调用工具 - 修复响应格式"""
name = params.get("name")
arguments = params.get("arguments", {})
if name not in self.tools:
return self.create_error_response(
code=-32602,
message=f"Tool '{name}' not found"
)
try:
if name == "get_time":
result = self.execute_get_time(arguments)
elif name == "text_process":
result = self.execute_text_process(arguments)
elif name == "format_data":
result = self.execute_format_data(arguments)
else:
return self.create_error_response(
code=-32602,
message="Tool not implemented"
)
# 确保返回正确的MCP响应格式
return self.create_success_response(result)
except Exception as e:
logger.error(f"Tool execution error: {e}")
return self.create_error_response(
code=-32603,
message=f"Tool execution failed: {str(e)}"
)
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""获取时间 - 支持中文"""
from datetime import datetime
try:
format_type = args.get("format", "iso")
now = datetime.now()
if format_type == "iso":
result = now.isoformat()
elif format_type == "timestamp":
result = now.timestamp()
elif format_type == "human":
result = now.strftime("%Y-%m-%d %H:%M:%S")
elif format_type == "chinese":
result = now.strftime("%Y年%m月%d%H时%M分%S秒")
else:
result = now.isoformat()
logger.info(f"当前系统时间:{result}")
return {
"status": "success",
"format": format_type,
"time": result,
"timestamp": now.timestamp(),
"date": now.strftime("%Y-%m-%d"),
"time_12h": now.strftime("%I:%M:%S %p")
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""文本处理"""
try:
text = args.get("text", "")
operation = args.get("operation", "length")
if operation == "length":
result = len(text)
result_str = f"文本长度: {result} 个字符"
elif operation == "upper":
result = text.upper()
result_str = f"大写: {result}"
elif operation == "lower":
result = text.lower()
result_str = f"小写: {result}"
elif operation == "reverse":
result = text[::-1]
result_str = f"反转: {result}"
elif operation == "count_words":
words = len(text.split())
result = words
result_str = f"单词数: {words}"
else:
raise ValueError(f"未知操作: {operation}")
return {
"status": "success",
"operation": operation,
"original_text": text,
"result": result,
"result_str": result_str,
"text_length": len(text)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"operation": args.get("operation", "")
}
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""格式化数据"""
try:
data_str = args.get("data", "")
format_type = args.get("format", "json")
# 尝试解析为JSON
try:
data = json.loads(data_str)
is_json = True
except:
data = data_str
is_json = False
if format_type == "json":
if is_json:
result = json.dumps(data, ensure_ascii=False, indent=2)
else:
# 如果不是JSON包装成JSON
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
elif format_type == "yaml":
import yaml
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
elif format_type == "xml":
# 简单的XML格式化
if isinstance(data, dict):
result = "<data>"
for k, v in data.items():
result += f"\n <{k}>{v}</{k}>"
result += "\n</data>"
else:
result = f"<text>{data}</text>"
else:
result = str(data)
return {
"status": "success",
"format": format_type,
"original": data_str,
"formatted": result,
"length": len(result)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"format": args.get("format", "")
}
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""创建成功响应 - 确保符合MCP规范"""
# 将数据转换为JSON字符串作为文本内容
content_text = json.dumps(data, ensure_ascii=False, indent=2)
return {
"result": {
"content": [
{
"type": "text",
"text": content_text
}
],
"isError": False
}
}
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
"""创建错误响应"""
return {
"error": {
"code": code,
"message": message
}
}
def safe_json_dump(data: Dict[str, Any]) -> str:
"""安全的JSON序列化确保UTF-8编码"""
try:
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
except:
# 如果失败使用ASCII转义
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
def main():
"""主函数 - 修复Stdio通信"""
logger.info("启动MCP Stdio服务器 (修复编码版)...")
server = FixedMCPServer()
# 初始握手消息
init_message = {
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "fixed-mcp-server",
"version": "1.0.0"
}
}
}
# 发送初始化响应
try:
sys.stdout.write(safe_json_dump(init_message) + "\n")
sys.stdout.flush()
except Exception as e:
logger.error(f"发送初始化消息失败: {e}")
return
logger.info("MCP服务器已初始化")
# 主循环
line_num = 0
while True:
try:
line = sys.stdin.readline()
if not line:
logger.info("输入流结束")
break
line = line.strip()
line_num += 1
if not line:
continue
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
try:
request = json.loads(line)
logger.info(f"解析请求: {request.get('method', 'unknown')}")
# 处理请求
response = server.handle_request(request)
response["jsonrpc"] = "2.0"
response["id"] = request.get("id")
# 发送响应
response_json = safe_json_dump(response)
sys.stdout.write(response_json + "\n")
sys.stdout.flush()
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}")
error_response = {
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": f"Parse error at line {line_num}"
},
"id": None
}
sys.stdout.write(safe_json_dump(error_response) + "\n")
sys.stdout.flush()
except KeyboardInterrupt:
logger.info("接收到中断信号")
break
except Exception as e:
logger.error(f"未处理的错误: {e}")
break
logger.info("MCP服务器已停止")
if __name__ == "__main__":
main()

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-module</artifactId>
<version>3.9.0</version>
<version>3.9.1</version>
</parent>
<artifactId>jeecg-boot-module-airag</artifactId>
@ -33,7 +33,7 @@
<properties>
<kotlin.version>2.2.0</kotlin.version>
<liteflow.version>2.15.0</liteflow.version>
<apache-tika.version>2.9.1</apache-tika.version>
<apache-tika.version>3.2.3</apache-tika.version>
</properties>
<dependencyManagement>
@ -41,14 +41,14 @@
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.3.0</version>
<version>1.9.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>1.3.0-beta9</version>
<version>1.9.1-beta17</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -75,7 +75,7 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>3.9.0.1</version>
<version>3.9.1-beta</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
@ -107,16 +107,16 @@
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>provided</scope>
<scope>runtime</scope>
</dependency>
<!-- end 注意这几个依赖体积较大每个约50MB。若发布时需要使用请将 <scope>provided</scope> 删除 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<artifactId>liteflow-script-python</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
@ -151,6 +151,11 @@
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<!-- langChain4j mcp support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
@ -197,7 +202,11 @@
<artifactId>langchain4j-pgvector</artifactId>
<version>1.3.0-beta9</version>
</dependency>
<!-- langChain4j Document Parser -->
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
@ -224,7 +233,12 @@
<artifactId>tika-parser-text-module</artifactId>
<version>${apache-tika.version}</version>
</dependency>
<!-- word模版引擎 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
</dependencies>
</project>

View File

@ -44,4 +44,19 @@ public class AiAppConsts {
*/
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
/**
* 是否开启记忆
*/
public static final Integer IZ_OPEN_MEMORY = 1;
/**
* 会话标题最大长度
*/
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
/**
* AI写作的应用id
*/
public static final String WRITER_APP_ID = "2010634128233779202";
}

View File

@ -104,4 +104,68 @@ public class Prompts {
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度低于0.7时启动重写\"\n" +
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
/**
* 提示词生成角色及通用要求
*/
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
"你是一位AI提示词专家请根据提供的配置信息生成针对AI智能体的“使用指南”提示词。\n" +
"\n" +
"## 通用要求\n" +
"1. 生成的内容将作为系统提示词的一部分。\n" +
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
"3. **只输出提示词内容**不要包含任何解释、寒暄或Markdown代码块标记。\n" +
"4. 语气专业、清晰、指令性强。\n" +
"5. 说明内容请使用中文。\n\n";
/**
* 变量生成提示词
*/
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
"### 输入信息\n" +
"**变量列表**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**变量使用指南**。\n" +
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}\n" +
" 例如针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
" 例如针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
"4. **通用更新指令**请在变量指南的最后单独生成一条指令明确指示AI“当从用户对话中获取到上述变量<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
/**
* 记忆库生成提示词
*/
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
"### 输入信息\n" +
"**记忆库描述**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
" - **全自动检索(强制优先)**\n" +
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
" - **行为准则**\n" +
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
" - **示例演示**\n" +
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
/**
* ai写作提示词
*/
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
/**
* ai写作回复提示词
*/
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
}

View File

@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* @Description: AI应用
@ -179,4 +178,16 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
}
/**
* 根据应用ID生成变量和记忆提示词 (SSE)
* for: 【QQYUN-14479】提示词单独拆分
* @param variables
* @return
*/
@PostMapping(value = "/prompt/generateMemoryByAppId")
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
@RequestParam(name = "memoryId") String memoryId) {
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
}
}

View File

@ -8,6 +8,7 @@ import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
import org.springframework.beans.factory.annotation.Autowired;
@ -102,6 +103,19 @@ public class AiragChatController {
return chatService.getConversations(appId);
}
/**
* 根据类型获取所有对话
*
* @return 返回一个Result对象包含所有对话的信息
* @author wangshuai
* @date 2025/12/11 11:42
*/
@IgnoreAuth
@GetMapping(value = "/getConversationsByType")
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
return chatService.getConversationsByType(sessionType);
}
/**
* 删除会话
*
@ -113,7 +127,22 @@ public class AiragChatController {
@IgnoreAuth
@DeleteMapping(value = "/conversation/{id}")
public Result<?> deleteConversation(@PathVariable("id") String id) {
return chatService.deleteConversation(id);
return chatService.deleteConversation(id,"");
}
/**
* 删除会话
*
* @param id
* @return
* @author wangshuai
* @date 2025/12/11 20:00
*/
@IgnoreAuth
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
public Result<?> deleteConversationByType(@PathVariable("id") String id,
@PathVariable("sessionType") String sessionType) {
return chatService.deleteConversation(id,sessionType);
}
/**
@ -139,8 +168,9 @@ public class AiragChatController {
*/
@IgnoreAuth
@GetMapping(value = "/messages")
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
return chatService.getMessages(conversationId);
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
@RequestParam(value = "sessionType", required = false) String sessionType) {
return chatService.getMessages(conversationId, sessionType);
}
/**
@ -153,7 +183,21 @@ public class AiragChatController {
@IgnoreAuth
@GetMapping(value = "/messages/clear/{conversationId}")
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
return chatService.clearMessage(conversationId);
return chatService.clearMessage(conversationId, "");
}
/**
* 清空消息
*
* @return
* @author wangshuai
* @date 2025/12/11 19:06
*/
@IgnoreAuth
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
@PathVariable(value = "sessionType") String sessionType) {
return chatService.clearMessage(conversationId, sessionType);
}
/**
@ -217,4 +261,25 @@ public class AiragChatController {
return result;
}
/**
* ai海报生成
* @return
*/
@PostMapping("/genAiPoster")
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
String imageUrl = chatService.genAiPoster(chatSendParams);
return Result.OK(imageUrl);
}
/**
* 生成ai写作
*
* @param aiWriteGenerateVo
* @return
*/
@PostMapping("/genAiWriter")
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
return chatService.genAiWriter(aiWriteGenerateVo);
}
}

View File

@ -173,6 +173,29 @@ public class AiragApp implements Serializable {
@Schema(description = "插件")
private java.lang.String plugins;
/**
* 是否开启记忆(0 不开启1开启)
*/
@Schema(description = "是否开启记忆(0 不开启1开启)")
private java.lang.Integer izOpenMemory;
/**
* 记忆库知识库的id
*/
@Schema(description = "记忆库")
private java.lang.String memoryId;
/**
* 变量
*/
@Schema(description = "变量")
private java.lang.String variables;
/**
* 记忆和变量提示词
*/
@Schema(description = "记忆和变量提示词")
private java.lang.String memoryPrompt;
/**
* 知识库ids
*/

View File

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

View File

@ -1,6 +1,7 @@
package org.jeecg.modules.airag.app.service;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
@ -59,21 +60,23 @@ public interface IAiragChatService {
* 获取对话聊天记录
*
* @param conversationId
* @param sessionType 类型
* @return
* @author chenrui
* @date 2025/2/26 15:16
*/
Result<?> getMessages(String conversationId);
Result<?> getMessages(String conversationId, String sessionType);
/**
* 删除会话
*
* @param conversationId
* @param sessionType
* @return
* @author chenrui
* @date 2025/3/3 16:55
*/
Result<?> deleteConversation(String conversationId);
Result<?> deleteConversation(String conversationId, String sessionType);
/**
* 更新会话标题
@ -87,11 +90,12 @@ public interface IAiragChatService {
/**
* 清空消息
* @param conversationId
* @param sessionType
* @return
* @author chenrui
* @date 2025/3/3 19:49
*/
Result<?> clearMessage(String conversationId);
Result<?> clearMessage(String conversationId, String sessionType);
/**
* 初始化聊天(忽略租户)
@ -111,4 +115,27 @@ public interface IAiragChatService {
* @date 2025/8/11 17:39
*/
SseEmitter receiveByRequestId(String requestId);
/**
* 根据类型获取会话列表
*
* @param sessionType
* @return
*/
Result<?> getConversationsByType(String sessionType);
/**
* 生成海报图片
* @param chatSendParams
* @return
*/
String genAiPoster(ChatSendParams chatSendParams);
/**
* 生成ai创作
*
* @param chatSendParams
* @return
*/
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
}

View File

@ -0,0 +1,44 @@
package org.jeecg.modules.airag.app.service;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.common.handler.AIChatParams;
public interface IAiragVariableService {
/**
* 更新变量值
*
* @param userId
* @param appId
* @param name
* @param value
*/
void updateVariable(String userId, String appId, String name, String value);
/**
* 追加提示词
*
* @param username
* @param app
* @return
*/
String additionalPrompt(String username, AiragApp app);
/**
* 初始化变量(仅不存在时设置)
*
* @param userId
* @param appId
* @param name
* @param defaultValue
*/
void initVariable(String userId, String appId, String name, String defaultValue);
/**
* 添加变量更新工具
*
* @param params
* @param aiApp
* @param username
*/
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import dev.langchain4j.data.message.AiMessage;
@ -10,12 +11,15 @@ import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.UUIDGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.app.consts.Prompts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.vo.AppVariableVo;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
@ -23,6 +27,8 @@ import org.jeecg.modules.airag.common.utils.AiragLocalCache;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@ -31,6 +37,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
/**
* @Description: AI应用
@ -45,6 +52,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
@Autowired
IAIChatHandler aiChatHandler;
@Autowired
private IAiragKnowledgeService airagKnowledgeService;
@Override
public Object generatePrompt(String prompt, boolean blocking) {
AssertUtils.assertNotEmpty("请输入提示词", prompt);
@ -62,81 +72,167 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
}
return Result.OK("success", promptValue);
}else{
SseEmitter emitter = new SseEmitter(-0L);
// 异步运行(流式)
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
return startSseChat(messages, params);
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
}
}
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆将记忆提示词单独拆分---
@Override
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
}
// 构建变量描述
StringBuilder variablesDesc = new StringBuilder();
if (oConvertUtils.isNotEmpty(variables)) {
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
if (variableList != null && !variableList.isEmpty()) {
for (AppVariableVo var : variableList) {
if (var.getEnable() != null && !var.getEnable()) {
continue;
}
String name = var.getName();
if (oConvertUtils.isNotEmpty(var.getAction())) {
String action = var.getAction();
if (oConvertUtils.isNotEmpty(name)) {
try {
// 使用正则替换未被{{}}包裹的变量名
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
action = action.replaceAll(regex, "{{" + name + "}}");
} catch (Exception e) {
log.warn("变量名替换异常: name={}", name, e);
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
variablesDesc.append(action).append("\n");
} else {
variablesDesc.append("- {{").append(name).append("}}");
if (oConvertUtils.isNotEmpty(var.getDescription())) {
variablesDesc.append(": ").append(var.getDescription());
}
variablesDesc.append("\n");
}
}
}
}
// 构建Prompt
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
if (!variablesDesc.isEmpty()) {
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
}
// 构建记忆状态描述
if (oConvertUtils.isNotEmpty(memoryId)) {
String memoryDescr = "";
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
memoryDescr += "记忆库描述:" + memory.getDescr();
}
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
}
String prompt = promptBuilder.toString();
List<ChatMessage> messages = List.of(new UserMessage(prompt));
AIChatParams params = new AIChatParams();
params.setTemperature(0.7);
if(blocking){
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
if (promptValue == null || promptValue.isEmpty()) {
return Result.error("生成失败");
}
return Result.OK("success", promptValue);
}else{
return startSseChat(messages, params);
}
}
/**
* 发送聊天
* @param messages
* @param params
* @return
*/
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
SseEmitter emitter = new SseEmitter(-0L);
// 异步运行(流式)
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onCompleteResponse((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onCompleteResponse((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try {
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
closeSSE(emitter, eventData);
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
return emitter;
}
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
return emitter;
}
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆将记忆提示词单独拆分---
private static void closeSSE(SseEmitter emitter, EventData eventData) {
try {

View File

@ -1,24 +1,36 @@
package org.jeecg.modules.airag.app.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.image.Image;
import dev.langchain4j.data.message.*;
import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.tika.parser.AutoDetectParser;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.vo.Path;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.consts.Prompts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.service.IAiragVariableService;
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
@ -35,18 +47,26 @@ import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
@ -85,6 +105,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Autowired
AiragModelMapper airagModelMapper;
@Autowired
IAiragFlowPluginService airagFlowPluginService;
@Autowired
IAiragKnowledgeService airagKnowledgeService;
@Autowired
IAiragVariableService airagVariableService;
@Autowired
JeecgBaseConfig jeecgBaseConfig;
/**
* 重新接收消息
@ -105,10 +137,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
}
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId, chatSendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
// 更新标题
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
chatConversation.setTitle(userMessage.length() > maxLength ? userMessage.substring(0, maxLength) : userMessage);
}
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 保存工作流入参配置(如果有)
@ -116,6 +151,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
}
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
//是否保存会话
if(null != chatSendParams.getIzSaveSession()){
chatConversation.setIzSaveSession(chatSendParams.getIzSaveSession());
}
// 保存变量
saveVariables(app);
// 发送消息
return doChat(chatConversation, topicId, chatSendParams);
}
@ -130,7 +171,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
AiragApp app = appDebugParams.getApp();
app.setId("__DEBUG_APP");
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId, "");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
//update-begin---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
// 保存工作流入参配置(如果有)
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
@ -140,7 +183,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 发送消息
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
//保存会话
saveChatConversation(chatConversation, true, null);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
saveChatConversation(chatConversation, true, null, "");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
return emitter;
}
@ -247,9 +292,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
@Override
public Result<?> getMessages(String conversationId) {
public Result<?> getMessages(String conversationId, String sessionType) {
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
String key = getConversationCacheKey(conversationId, null);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
String key = getConversationCacheKey(conversationId, null, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) {
return Result.ok(Collections.emptyList());
}
@ -273,6 +320,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
.role(msg.getRole())
.content(msg.getContent())
.images(msg.getImages())
.files(msg.getFiles())
.datetime(msg.getDatetime())
.build();
// 不设置toolExecutionRequests和toolExecutionResult
@ -282,21 +330,30 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
result.put("messages", messages);
result.put("flowInputs", chatConversation.getFlowInputs());
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if(oConvertUtils.isNotEmpty(sessionType)){
result.put("appData", chatConversation.getApp());
}
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
return Result.ok(result);
//update-end---author:chenrui ---date:20251106 for[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
}
@Override
public Result<?> clearMessage(String conversationId) {
public Result<?> clearMessage(String conversationId, String sessionType) {
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
String key = getConversationCacheKey(conversationId, null);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
String key = getConversationCacheKey(conversationId, null,sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) {
return Result.ok(Collections.emptyList());
}
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
chatConversation.getMessages().clear();
saveChatConversation(chatConversation);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
saveChatConversation(chatConversation,sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
}
return Result.ok();
}
@ -443,9 +500,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
@Override
public Result<?> deleteConversation(String conversationId) {
public Result<?> deleteConversation(String conversationId, String sessionType) {
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
String key = getConversationCacheKey(conversationId, null);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
String key = getConversationCacheKey(conversationId, null, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isNotEmpty(key)) {
Boolean delete = redisTemplate.delete(key);
if (delete) {
@ -463,14 +522,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
String key = getConversationCacheKey(updateTitleParams.getId(), null);
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType());
if (oConvertUtils.isEmpty(key)) {
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
return Result.ok();
}
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
chatConversation.setTitle(updateTitleParams.getTitle());
saveChatConversation(chatConversation);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (chatConversation != null) {
chatConversation.setTitle(updateTitleParams.getTitle());
}
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
return Result.ok();
}
@ -479,15 +542,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
*
* @param conversationId
* @param httpRequest
* @param sessionType 会话类型
* @return
* @author chenrui
* @date 2025/2/25 19:27
*/
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest, String sessionType) {
if (oConvertUtils.isEmpty(conversationId)) {
return null;
}
String key = getConversationDirCacheKey(httpRequest);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if(oConvertUtils.isNotEmpty(sessionType)){
key = key + ":" + sessionType;
}
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
key = key + ":" + conversationId;
return key;
}
@ -522,18 +591,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
*
* @param app
* @param conversationId
* @param sessionType
* @return
* @author chenrui
* @date 2025/2/25 19:19
*/
@NotNull
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId, String sessionType) {
if (oConvertUtils.isObjectEmpty(app)) {
app = new AiragApp();
app.setId(AiAppConsts.DEFAULT_APP_ID);
}
ChatConversation chatConversation = null;
String key = getConversationCacheKey(conversationId, null);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
String key = getConversationCacheKey(conversationId, null,sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isNotEmpty(key)) {
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
}
@ -569,8 +641,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private void saveChatConversation(ChatConversation chatConversation) {
saveChatConversation(chatConversation, false, null);
private void saveChatConversation(ChatConversation chatConversation, String sessionType) {
saveChatConversation(chatConversation, false, null, sessionType);
}
/**
@ -581,11 +653,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest, String sessionType) {
if (null == chatConversation) {
return;
}
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
//如果是不保存会话直接返回
if(null != chatConversation.getIzSaveSession() && !chatConversation.getIzSaveSession()){
return;
}
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
String key = getConversationCacheKey(chatConversation.getId(), httpRequest, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) {
return;
}
@ -680,6 +760,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @date 2025/2/25 19:05
*/
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
appendMessage(messages, message, chatConversation, topicId, null, null);
}
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId, List<String> files, String saveContent) {
if (message.type().equals(ChatMessageType.SYSTEM)) {
// 系统消息,放到消息列表最前面,并且不记录历史
@ -709,8 +793,22 @@ public class AiragChatServiceImpl implements IAiragChatService {
textContent.append(((TextContent) content).text()).append("\n");
}
});
historyMessage.setContent(textContent.toString());
//update-begin---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
if (oConvertUtils.isNotEmpty(saveContent)) {
historyMessage.setContent(saveContent);
} else {
historyMessage.setContent(textContent.toString());
}
historyMessage.setImages(images);
// 保存文件信息
if (oConvertUtils.isNotEmpty(files)) {
List<MessageHistory.FileHistory> fileHistories = new ArrayList<>();
for (String file : files) {
fileHistories.add(new MessageHistory.FileHistory(file));
}
historyMessage.setFiles(fileHistories);
}
//update-end---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
} else if (message.type().equals(ChatMessageType.AI)) {
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
AiMessage aiMessage = (AiMessage) message;
@ -766,9 +864,20 @@ public class AiragChatServiceImpl implements IAiragChatService {
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
try {
// 组装用户消息
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
String content = sendParams.getContent();
//将文件内容给提示词
if(!CollectionUtils.isEmpty(sendParams.getFiles())){
content = buildContentWithFiles(content, sendParams.getFiles());
}
UserMessage userMessage = aiChatHandler.buildUserMessage(content, sendParams.getImages());
// 追加消息
appendMessage(messages, userMessage, chatConversation, topicId);
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
appendMessage(messages, userMessage, chatConversation, topicId, sendParams.getFiles(), sendParams.getContent());
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手支持多模态能力- 文档---
// 绘画AI逻辑当开启生成绘画时调用
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableDraw()) && sendParams.getEnableDraw()) {
return genImageChat(emitter,sendParams,requestId,messages,chatConversation,topicId);
}
/* 这里应该是有几种情况:
* 1. 非ai应用:获取默认模型->开始聊天
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
@ -781,7 +890,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
} else {
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId());
}
} else {
// 发消息
@ -789,7 +898,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
}
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
// 设置深度思考搜索参数
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
aiChatParams.setReturnThinking(sendParams.getEnableThink());
}
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams, sendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
}
// 发送就绪消息
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
@ -804,6 +919,59 @@ public class AiragChatServiceImpl implements IAiragChatService {
return emitter;
}
/**
* 生成图片
*
* @param emitter
* @param sendParams
* @param requestId
* @param messages
* @param chatConversation
* @param topicId
* @return
*/
private SseEmitter genImageChat(SseEmitter emitter, ChatSendParams sendParams, String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
AssertUtils.assertNotEmpty("请选择绘画模型", sendParams.getDrawModelId());
AIChatParams aiChatParams = new AIChatParams();
try {
List<String> images = sendParams.getImages();
List<Map<String, Object>> imageList = new ArrayList<>();
if(CollectionUtils.isEmpty(images)) {
//生成图片
imageList = aiChatHandler.imageGenerate(sendParams.getDrawModelId(), sendParams.getContent(), aiChatParams);
} else {
//图生图
imageList = aiChatHandler.imageEdit(sendParams.getDrawModelId(), sendParams.getContent(), images, aiChatParams);
}
// 记录历史消息
String imageMarkdown = imageList.stream().map(map -> {
String newUrl = this.uploadImage(map);
return "![](" + newUrl + ")";
}).collect(Collectors.joining("\n"));
AiMessage aiMessage = new AiMessage(imageMarkdown);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 处理绘画结果并通过SSE返回给客户端
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(imageMarkdown).build();
eventData.setData(messageEventData);
eventData.setRequestId(requestId);
sendMessage2Client(emitter, eventData);
// 保存会话
saveChatConversation(chatConversation, false, SpringContextUtils.getHttpServletRequest(), sendParams.getSessionType());
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
eventData.setRequestId(requestId);
sendMessage2Client(emitter, eventData);
} catch (Exception e) {
log.error("绘画AI调用异常", e);
EventData errorEventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message("绘画AI调用失败" + e.getMessage()).build();
errorEventData.setData(messageEventData);
errorEventData.setRequestId(requestId);
closeSSE(emitter, errorEventData);
}
return emitter;
}
/**
* 运行流程
*
@ -875,7 +1043,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
sendMessage2Client(emitter, msgEventData);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
saveChatConversation(chatConversation, false, httpRequest, sendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
}
}else{
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
@ -908,16 +1078,31 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @param chatConversation
* @param topicId
* @param sendParams
* @param flowId
* @param memoryId
* @return
* @author chenrui
* @date 2025/2/28 10:41
*/
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams, String flowId, String memoryId) {
AiragApp aiApp = chatConversation.getApp();
String modelId = aiApp.getModelId();
AssertUtils.assertNotEmpty("请先选择模型", modelId);
// AI应用提示词
String prompt = aiApp.getPrompt();
String username = "jeecg";
try {
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
username = JwtUtil.getUserNameByToken(req);
} catch (Exception e) {
log.error(e.getMessage());
}
//将变量中的题试题替换并追加
if(oConvertUtils.isObjectNotEmpty(aiApp.getVariables())) {
prompt = airagVariableService.additionalPrompt(username, aiApp);
}
if (oConvertUtils.isNotEmpty(prompt)) {
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
}
@ -943,6 +1128,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (metadata.containsKey("maxTokens")) {
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
}
if (metadata.containsKey(FlowConsts.FLOW_NODE_OPTION_TIME_OUT)) {
aiChatParams.setTimeout(oConvertUtils.getInt(metadata.getInteger(FlowConsts.FLOW_NODE_OPTION_TIME_OUT), 300));
}
}
}
@ -964,16 +1152,70 @@ public class AiragChatServiceImpl implements IAiragChatService {
aiChatParams.setPluginIds(pluginIds);
}
}
//流程不为空,构建插件
if(oConvertUtils.isNotEmpty(flowId)){
Map<String, Object> result = airagFlowPluginService.getFlowsToPlugin(flowId);
this.addPluginToParams(aiChatParams, result);
}
// 设置网络搜索参数(如果前端传递了)
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
}
// 设置深度思考参数(如果前端传递了)
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
aiChatParams.setReturnThinking(sendParams.getEnableThink());
}
// 设置记忆库的插件
if(sendParams != null && oConvertUtils.isNotEmpty(memoryId)){
//开启记忆
if(null == aiApp.getIzOpenMemory() || AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())){
Map<String, Object> pluginMemory = airagKnowledgeService.getPluginMemory(memoryId);
this.addPluginToParams(aiChatParams, pluginMemory);
}
}
//设置变量的插件
// 添加系统级工具:变量更新
if (oConvertUtils.isNotEmpty(aiApp.getId())) {
airagVariableService.addUpdateVariableTool(aiApp,username,aiChatParams);
}
// 打印流程耗时日志
printChatDuration(requestId, "构造应用自定义参数完成");
// 发消息
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams, sendParams.getSessionType());
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
}
/**
* 添加插件到参数中
*
* @param aiChatParams
* @param result
*/
private void addPluginToParams(AIChatParams aiChatParams, Map<String, Object> result) {
if (result == null) {
return;
}
Map<ToolSpecification, ToolExecutor> flowsToPlugin = (Map<ToolSpecification, ToolExecutor>) result.get("pluginTool");
String pluginId = (String) result.get("pluginId");
if (aiChatParams.getTools() == null) {
aiChatParams.setTools(new HashMap<>());
}
if (flowsToPlugin != null) {
aiChatParams.getTools().putAll(flowsToPlugin);
}
if (aiChatParams.getPluginIds() == null) {
aiChatParams.setPluginIds(new ArrayList<>());
}
if (oConvertUtils.isNotEmpty(pluginId)) {
aiChatParams.getPluginIds().add(pluginId);
}
}
/**
@ -984,11 +1226,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @param topicId
* @param modelId
* @param messages
* @param sessionType
* @return
* @author chenrui
* @date 2025/2/25 19:24
*/
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams, String sessionType) {
// 调用ai聊天
if (null == aiChatParams) {
aiChatParams = new AIChatParams();
@ -997,11 +1240,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
}
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
} else {
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
}
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
aiChatParams.setReturnThinking(true);
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
// for [QQYUN-9234] MCP服务连接关闭 - 保存参数引用用于在回调中关闭MCP连接
final AIChatParams finalAiChatParams = aiChatParams;
TokenStream chatStream;
try {
// 打印流程耗时日志
@ -1013,6 +1261,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
} catch (Exception e) {
log.error(e.getMessage(), e);
// for [QQYUN-9234] MCP服务连接关闭 - 异常时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
@ -1098,6 +1348,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息完成");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// for [QQYUN-9234] MCP服务连接关闭 - 聊天完成时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
@ -1113,7 +1365,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
saveChatConversation(chatConversation, false, httpRequest, sessionType);
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
closeSSE(emitter, eventData);
} else if (FinishReason.LENGTH.equals(finishReason)) {
// 上下文长度超过限制
@ -1137,6 +1391,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息异常");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// for [QQYUN-9234] MCP服务连接关闭 - 聊天异常时关闭MCP连接
finalAiChatParams.closeMcpConnections();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
@ -1201,7 +1457,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
*/
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
try {
log.info("发送消息:{}", eventData.getRequestId());
log.debug("发送消息:{}", eventData.getRequestId());
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
@ -1251,7 +1507,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isEmpty(chatConversation.getId())) {
return;
}
String key = getConversationCacheKey(chatConversation.getId(), null);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
String key = getConversationCacheKey(chatConversation.getId(), null,"");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
if (oConvertUtils.isEmpty(key)) {
return;
}
@ -1281,10 +1539,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
if (oConvertUtils.isNotEmpty(summaryTitle)) {
cachedConversation.setTitle(summaryTitle);
} else {
cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
cachedConversation.setTitle(question.length() > maxLength ? question.substring(0, maxLength) : question);
}
//保存会话
saveChatConversation(cachedConversation);
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
saveChatConversation(cachedConversation,"");
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
}
});
}
@ -1329,4 +1590,296 @@ public class AiragChatServiceImpl implements IAiragChatService {
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
}
}
}
/**
* 根据会话类型获取会话信息
*
* @param sessionType
* @return
*/
@Override
public Result<?> getConversationsByType(String sessionType) {
String key = getConversationDirCacheKey(null);
key = key + ":" + sessionType + ":*";
List<String> keys = redisUtil.scan(key);
// 如果键集合为空,返回空列表
if (keys.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 遍历键集合,获取对应的 ChatConversation 对象
List<ChatConversation> conversations = new ArrayList<>();
for (Object k : keys) {
ChatConversation conversation = (ChatConversation) redisTemplate.boundValueOps(k).get();
if (conversation != null) {
AiragApp app = conversation.getApp();
if (null == app) {
continue;
}
conversation.setApp(null);
conversation.setMessages(null);
conversations.add(conversation);
}
}
// 对会话列表按创建时间降序排序
conversations.sort((o1, o2) -> {
Date date1 = o1.getCreateTime();
Date date2 = o2.getCreateTime();
if (date1 == null && date2 == null) {
return 0;
}
if (date1 == null) {
return 1;
}
if (date2 == null) {
return -1;
}
return date2.compareTo(date1);
});
// 返回结果
return Result.ok(conversations);
}
//================================================= begin 【QQYUN-14269】【AI】支持变量 ========================================
/**
* 初始化变量(仅不存在时设置)
*/
private void saveVariables(AiragApp app) {
if(null == app){
return;
}
if(!AiAppConsts.IZ_OPEN_MEMORY.equals(app.getIzOpenMemory())){
return;
}
if (oConvertUtils.isObjectNotEmpty(app.getVariables())) {
// 变量替换
String username = "jeecg";
try {
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
username = JwtUtil.getUserNameByToken(req);
} catch (Exception e) {
log.error(e.getMessage());
}
if (oConvertUtils.isNotEmpty(username) && oConvertUtils.isNotEmpty(app.getId())) {
String variables = app.getVariables();
JSONArray objects = JSONArray.parseArray(variables);
for (int i = 0; i < objects.size(); i++) {
JSONObject jsonObject = objects.getJSONObject(i);
String name = jsonObject.getString("name");
String defaultValue = jsonObject.getString("defaultValue");
if (oConvertUtils.isNotEmpty(name)) {
airagVariableService.initVariable(username, app.getId(), name, defaultValue);
}
}
}
}
}
//================================================= end 【QQYUN-14269】【AI】支持变量 ========================================
/**
* ai海报生成
*
* @param chatSendParams
* @return
*/
@Override
public String genAiPoster(ChatSendParams chatSendParams) {
AssertUtils.assertNotEmpty("请选择绘画模型", chatSendParams.getDrawModelId());
AssertUtils.assertNotEmpty("请填写提示词", chatSendParams.getContent());
AIChatParams aiChatParams = new AIChatParams();
if(oConvertUtils.isNotEmpty(chatSendParams.getImageSize())){
aiChatParams.setImageSize(chatSendParams.getImageSize());
}
String image= chatSendParams.getImageUrl();
List<Map<String, Object>> imageList = new ArrayList<>();
if(oConvertUtils.isEmpty(image)) {
//生成图片
imageList = aiChatHandler.imageGenerate(chatSendParams.getDrawModelId(), chatSendParams.getContent(), aiChatParams);
} else {
//图生图
imageList = aiChatHandler.imageEdit(chatSendParams.getDrawModelId(), chatSendParams.getContent(), Arrays.asList(image.split(SymbolConstant.COMMA)), aiChatParams);
}
return imageList.stream().map(this::uploadImage).collect(Collectors.joining("\n"));
}
/**
* 上传图片
*
* @param map
* @return
*/
private String uploadImage(Map<String, Object> map) {
if (null == map || map.isEmpty()) {
return "";
}
try {
String type = String.valueOf(map.get("type"));
String value = String.valueOf(map.get("value"));
byte[] data = new byte[1024];
// 判断是否是base64
if ("base64".equals(type)) {
if(value.startsWith("data:image")){
value = value.substring(value.indexOf(",") + 1);
}
data = Base64.getDecoder().decode(value);
} else {
//下载网络图片
InputStream inputStream = FileDownloadUtils.getDownInputStream(value, "");
if (inputStream != null) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] inpByte = new byte[1024]; // 1KB缓冲区
int nRead;
while ((nRead = inputStream.read(inpByte, 0, data.length)) != -1) {
buffer.write(inpByte, 0, nRead);
}
data = buffer.toByteArray();
}
}
if (data != null) {
Path path = jeecgBaseConfig.getPath();
String bizPath = "chat";
String url = CommonUtils.uploadOnlineImage(data, path.getUpload(), bizPath, jeecgBaseConfig.getUploadType());
if("local".equals(jeecgBaseConfig.getUploadType())){
url = "#{domainURL}/" + url;
}
return url;
}
} catch (Exception e) {
log.error("上传图片失败", e);
}
return "";
}
//================================================= begin【QQYUN-14261】【AI】AI助手支持多模态能力- 文档========================================
/**
* 构建文件内容
*
* @param content
* @param files
* @return
*/
private String buildContentWithFiles(String content, List<String> files) {
String filesText = parseFilesToText(files);
if (oConvertUtils.isEmpty(content)) {
content = "请基于我提供的附件内容回答问题。";
}else{
content = content + "\n\n请基于我提供的附件内容回答问题。";
}
if (oConvertUtils.isNotEmpty(filesText)) {
if (oConvertUtils.isNotEmpty(content)) {
content = content + "\n\n" + filesText;
} else {
content = filesText;
}
}
return content;
}
/**
* 将文件转换成text
*
* @param files
* @return
*/
private String parseFilesToText(List<String> files) {
if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(files)) {
return "";
}
StringBuilder sb = new StringBuilder();
TikaDocumentParser parser = new TikaDocumentParser(AutoDetectParser::new, null, null, null);
int parsedCount = 0;
for (String fileRef : files) {
if (parsedCount >= LLMConsts.CHAT_FILE_MAX_COUNT) {
break;
}
if (oConvertUtils.isEmpty(fileRef)) {
continue;
}
String fileRefWithoutQuery = fileRef;
if (fileRefWithoutQuery.contains("?")) {
fileRefWithoutQuery = fileRefWithoutQuery.substring(0, fileRefWithoutQuery.indexOf("?"));
}
String fileName = FilenameUtils.getName(fileRefWithoutQuery);
String ext = FilenameUtils.getExtension(fileName);
if (oConvertUtils.isEmpty(ext) || !LLMConsts.CHAT_FILE_EXT_WHITELIST.contains(ext.toLowerCase())) {
continue;
}
try {
File file = ensureLocalFile(fileRef, fileName);
if (file == null || !file.exists() || !file.isFile()) {
continue;
}
Document document = parser.parse(file);
if (document == null || oConvertUtils.isEmpty(document.text())) {
continue;
}
String text = document.text().trim();
if (text.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
text = text.substring(0, LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH);
}
sb.append("附件[").append(fileName).append("]内容:\n").append(text).append("\n\n");
parsedCount++;
if (sb.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
break;
}
} catch (Exception e) {
log.warn("附件解析失败: {}, {}", fileRef, e.getMessage());
}
}
return sb.toString().trim();
}
/**
* 获取本地文件
*
* @param fileRef
* @param fileName
* @return
* @throws IOException
*/
private File ensureLocalFile(String fileRef, String fileName) {
String uploadpath = jeecgBaseConfig.getPath().getUpload();
if (LLMConsts.WEB_PATTERN.matcher(fileRef).matches()) {
String tempDir = uploadpath + File.separator + "chat" + File.separator + UUID.randomUUID() + File.separator;
File dir = new File(tempDir);
if (!dir.exists() && !dir.mkdirs()) {
return null;
}
String tempFilePath = tempDir + fileName;
FileDownloadUtils.download2DiskFromNet(fileRef, tempFilePath);
return new File(tempFilePath);
}
return new File(uploadpath + File.separator + fileRef);
}
//================================================= end【QQYUN-14261】【AI】AI助手支持多模态能力- 文档========================================
/**
* ai创作
*
* @param aiWriteGenerateVo
* @return
*/
@Override
public SseEmitter genAiWriter(AiWriteGenerateVo aiWriteGenerateVo) {
String activeMode = "compose";
ChatSendParams sendParams = new ChatSendParams();
sendParams.setAppId(AiAppConsts.WRITER_APP_ID);
String content = "";
//写作
if (activeMode.equals(aiWriteGenerateVo.getActiveMode())) {
content = StrUtil.format(Prompts.AI_WRITER_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
} else {
//回复
content = StrUtil.format(Prompts.AI_REPLY_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getOriginalContent(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
}
sendParams.setContent(content);
sendParams.setIzSaveSession(false);
return this.send(sendParams);
}
}

View File

@ -0,0 +1,194 @@
package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.service.tool.ToolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.service.IAiragVariableService;
import org.jeecg.modules.airag.app.vo.AppVariableVo;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: AI应用变量服务实现
* @Author: jeecg-boot
* @Date: 2025-02-26
* @Version: V1.0
*/
@Service
@Slf4j
public class AiragVariableServiceImpl implements IAiragVariableService {
@Autowired
private RedisTemplate redisTemplate;
private static final String CACHE_PREFIX = "airag:app:var:";
/**
* 初始化变量(仅不存在时设置)
*
* @param username
* @param appId
* @param name
* @param defaultValue
*/
@Override
public void initVariable(String username, String appId, String name, String defaultValue) {
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
return;
}
String key = CACHE_PREFIX + appId + ":" + username;
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
}
/**
* 追加提示词
*
* @param username
* @param app
* @return
*/
@Override
public String additionalPrompt(String username, AiragApp app) {
String memoryPrompt = app.getMemoryPrompt();
String prompt = app.getPrompt();
if (oConvertUtils.isEmpty(memoryPrompt)) {
return prompt;
}
String variablesStr = app.getVariables();
if (oConvertUtils.isEmpty(variablesStr)) {
return prompt;
}
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
if (variableList == null || variableList.isEmpty()) {
return prompt;
}
String key = CACHE_PREFIX + app.getId() + ":" + username;
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
for (AppVariableVo variable : variableList) {
if (variable.getEnable() != null && !variable.getEnable()) {
continue;
}
String name = variable.getName();
String value = variable.getDefaultValue();
// 优先使用Redis中的值
if (savedValues.containsKey(name)) {
Object savedVal = savedValues.get(name);
if (savedVal != null) {
value = String.valueOf(savedVal);
}
}
if (value == null) {
value = "";
}
// 替换 {{name}}
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
}
return prompt + "\n" + memoryPrompt;
}
/**
* 更新变量值
*
* @param userId
* @param appId
* @param name
* @param value
*/
@Override
public void updateVariable(String userId, String appId, String name, String value) {
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
return;
}
String key = CACHE_PREFIX + appId + ":" + userId;
redisTemplate.opsForHash().put(key, name, value);
}
/**
* 添加变量更新工具
*
* @param params
* @param aiApp
* @param username
*/
@Override
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
if (params.getTools() == null) {
params.setTools(new HashMap<>());
}
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
return;
}
// 构建变量描述信息
String variablesStr = aiApp.getVariables();
List<AppVariableVo> variableList = null;
if (oConvertUtils.isNotEmpty(variablesStr)) {
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
}
//工具描述
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
if (variableList != null && !variableList.isEmpty()) {
descriptionBuilder.append("\n\n可用变量列表");
for (AppVariableVo var : variableList) {
if (var.getEnable() != null && !var.getEnable()) {
continue;
}
descriptionBuilder.append("\n- ").append(var.getName());
if (oConvertUtils.isNotEmpty(var.getDescription())) {
descriptionBuilder.append(": ").append(var.getDescription());
}
}
descriptionBuilder.append("\n\n注意variableName必须是上述列表中的名称之一。");
}
//构建更新变量的工具
ToolSpecification spec = ToolSpecification.builder()
.name("update_variable")
.description(descriptionBuilder.toString())
.parameters(JsonObjectSchema.builder()
.addStringProperty("variableName", "变量名称")
.addStringProperty("value", "变量值")
.required("variableName", "value")
.build())
.build();
//监听工具的调用
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
try {
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
String name = args.getString("variableName");
String value = args.getString("value");
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
//更新变量值
variableService.updateVariable(username, aiApp.getId(), name, value);
return "变量 " + name + " 已更新为: " + value;
} catch (Exception e) {
log.error("更新变量失败", e);
return "更新变量失败: " + e.getMessage();
}
};
params.getTools().put(spec, executor);
}
}

View File

@ -0,0 +1,48 @@
package org.jeecg.modules.airag.app.vo;
import lombok.Data;
/**
* @Description: ai写作生成实体类
*
* @author: wangshuai
* @date: 2026/1/12 15:59
*/
@Data
public class AiWriteGenerateVo {
/**
* 写作类型
*/
private String activeMode;
/**
* 写作内容提示
*/
private String prompt;
/**
* 原文
*/
private String originalContent;
/**
* 长度
*/
private String length;
/**
* 格式
*/
private String format;
/**
* 语气
*/
private String tone;
/**
* 语言
*/
private String language;
}

View File

@ -0,0 +1,46 @@
package org.jeecg.modules.airag.app.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @Description: 应用变量配置
* @Author: jeecg-boot
* @Date: 2025-02-26
* @Version: V1.0
*/
@Data
public class AppVariableVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 变量名
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 默认值
*/
private String defaultValue;
/**
* 是否启用
*/
private Boolean enable;
/**
* 动作
*/
private String action;
/**
* 排序
*/
private Integer orderNum;
}

View File

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

View File

@ -47,6 +47,11 @@ public class ChatSendParams {
*/
private List<String> images;
/**
* 文件列表
*/
private List<String> files;
/**
* 工作流额外入参配置
* key: 参数field, value: 参数值
@ -59,4 +64,39 @@ public class ChatSendParams {
*/
private Boolean enableSearch;
/**
* 是否开启深度思考
*/
private Boolean enableThink;
/**
* 会话类型: portal 应用门户
*/
private String sessionType;
/**
* 是否开启生成绘画
*/
private Boolean enableDraw;
/**
* 绘画模型的id
*/
private String drawModelId;
/**
* 图片尺寸
*/
private String imageSize;
/**
* 一张图片
*/
private String imageUrl;
/**
* 是否保存会话
*/
private Boolean izSaveSession;
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.demo;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.springframework.stereotype.Component;
@ -11,12 +12,15 @@ import java.util.Map;
* @Author: chenrui
* @Date: 2025/3/6 11:42
*/
@Slf4j
@Component("testAiragEnhance")
public class TestAiragEnhance implements IAiRagEnhanceJava {
@Override
public Map<String, Object> process(Map<String, Object> inputParams) {
Object arg1 = inputParams.get("arg1");
Object arg2 = inputParams.get("arg2");
Object index = inputParams.get("index");
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
}
}

View File

@ -0,0 +1,209 @@
package org.jeecg.modules.airag.llm.consts;
/**
* @Description: 流程插件常量
*
* @author: wangshuai
* @date: 2025/12/23 19:37
*/
public interface FlowPluginContent {
/**
* 名称
*/
String NAME = "name";
/**
* 描述
*/
String DESCRIPTION = "description";
/**
* 响应
*/
String RESPONSES = "responses";
/**
* 类型
*/
String TYPE = "type";
/**
* 参数
*/
String PARAMETERS = "parameters";
/**
* 是否必须
*/
String REQUIRED = "required";
/**
* 默认值
*/
String DEFAULT_VALUE = "defaultValue";
/**
* 路径
*/
String PATH = "path";
/**
* 方法
*/
String METHOD = "method";
/**
* 位置
*/
String LOCATION = "location";
/**
* 认证类型
*/
String AUTH_TYPE = "authType";
/**
* token参数名称
*/
String TOKEN_PARAM_NAME = "tokenParamName";
/**
* token参数值
*/
String TOKEN_PARAM_VALUE = "tokenParamValue";
/**
* token
*/
String TOKEN = "token";
/**
* Path位置
*/
String LOCATION_PATH = "Path";
/**
* Header位置
*/
String LOCATION_HEADER = "Header";
/**
* Query位置
*/
String LOCATION_QUERY = "Query";
/**
* Body位置
*/
String LOCATION_BODY = "Body";
/**
* Form-Data位置
*/
String LOCATION_FORM_DATA = "Form-Data";
/**
* String类型
*/
String TYPE_STRING = "String";
/**
* string类型
*/
String TYPE_STRING_LOWER = "string";
/**
* Number类型
*/
String TYPE_NUMBER = "Number";
/**
* number类型
*/
String TYPE_NUMBER_LOWER = "number";
/**
* Integer类型
*/
String TYPE_INTEGER = "Integer";
/**
* integer类型
*/
String TYPE_INTEGER_LOWER = "integer";
/**
* Boolean类型
*/
String TYPE_BOOLEAN = "Boolean";
/**
* boolean类型
*/
String TYPE_BOOLEAN_LOWER = "boolean";
/**
* 工具数量
*/
String TOOL_COUNT = "tool_count";
/**
* 是否启用
*/
String ENABLED = "enabled";
/**
* 输入
*/
String INPUTS = "inputs";
/**
* 输出
*/
String OUTPUTS = "outputs";
/**
* POST请求
*/
String POST = "POST";
/**
* token名称
*/
String X_ACCESS_TOKEN = "X-Access-Token";
/**
* 插件名称
*/
String PLUGIN_NAME = "流程调用";
/**
* 插件描述
*/
String PLUGIN_DESC = "调用工作流";
/**
* 插件请求地址
*/
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
/**
* 记忆库插件名称
*/
String PLUGIN_MEMORY_NAME = "记忆库";
/**
* 记忆库插件描述
*/
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
/**
* 添加记忆路径
*/
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
/**
* 查询记忆路径
*/
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
}

View File

@ -1,5 +1,8 @@
package org.jeecg.modules.airag.llm.consts;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
/**
@ -35,6 +38,11 @@ public class LLMConsts {
*/
public static final String MODEL_TYPE_LLM = "LLM";
/**
* 模型类型: 图像生成
*/
public static final String MODEL_TYPE_IMAGE = "IMAGE";
/**
* 向量模型:默认维度
*/
@ -85,4 +93,29 @@ public class LLMConsts {
*/
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
/**
* 知识库类型:知识库
*/
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
/**
* 知识库类型:记忆库
*/
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
/**
* 支持文件的后缀
*/
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
/**
* 文件内容最大长度
*/
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
/**
* 上传文件对打数量
*/
public static final int CHAT_FILE_MAX_COUNT = 3;
}

View File

@ -0,0 +1,31 @@
package org.jeecg.modules.airag.llm.controller;
import org.jeecg.common.airag.api.IAiragBaseApi;
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* airag baseAPI Controller
*
* @author sjlei
* @date 2025-12-30
*/
@RestController("airagBaseApiController")
public class AiragBaseApiController implements IAiragBaseApi {
@Autowired
AiragBaseApiImpl airagBaseApi;
@PostMapping("/airag/api/knowledgeWriteTextDocument")
public String knowledgeWriteTextDocument(
@RequestParam("knowledgeId") String knowledgeId,
@RequestParam("title") String title,
@RequestParam("content") String content
) {
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
}
}

View File

@ -1,14 +1,18 @@
package org.jeecg.modules.airag.llm.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
@ -22,7 +26,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -80,6 +83,9 @@ public class AiragKnowledgeController {
@RequiresPermissions("airag:knowledge:add")
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
if(oConvertUtils.isEmpty(airagKnowledge.getType())) {
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
}
airagKnowledgeService.save(airagKnowledge);
return Result.OK("添加成功!");
}
@ -101,6 +107,9 @@ public class AiragKnowledgeController {
return Result.error("未找到对应数据");
}
String oldEmbedId = airagKnowledgeEntity.getEmbedId();
if(oConvertUtils.isEmpty(airagKnowledgeEntity.getType())) {
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
}
airagKnowledgeService.updateById(airagKnowledge);
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
// 更新了模型,重建文档
@ -357,5 +366,62 @@ public class AiragKnowledgeController {
List<AiragKnowledge> airagKnowledges = airagKnowledgeService.listByIds(idList);
return Result.OK(airagKnowledges);
}
/**
* 添加记忆
*
* @param airagKnowledgeDoc
* @return
*/
@Operation(summary = "添加记忆")
@PostMapping(value = "/plugin/add")
public Result<?> add(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc, HttpServletRequest request) {
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getKnowledgeId())) {
return Result.error("知识库ID不能为空");
}
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getContent())) {
return Result.error("内容不能为空");
}
// 设置默认值
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getTitle())) {
// 取内容前20个字作为标题
String content = airagKnowledgeDoc.getContent();
String title = content.length() > 20 ? content.substring(0, 20) : content;
airagKnowledgeDoc.setTitle(title);
}
airagKnowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
// 保存并构建向量
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
}
/**
* 查询记忆
*
* @param params
* @return
*/
@Operation(summary = "查询记忆")
@PostMapping(value = "/plugin/query")
public Result<?> pluginQuery(@RequestBody Map<String, Object> params, HttpServletRequest request) {
String knowId = (String) params.get("knowledgeId");
String queryText = (String) params.get("queryText");
if (oConvertUtils.isEmpty(knowId)) {
return Result.error("知识库ID不能为空");
}
if (oConvertUtils.isEmpty(queryText)) {
return Result.error("查询内容不能为空");
}
LambdaQueryWrapper<AiragKnowledgeDoc> queryWrapper = new LambdaQueryWrapper<AiragKnowledgeDoc>();
queryWrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
long count = airagKnowledgeDocService.count(queryWrapper);
if(count == 0){
return Result.ok("");
}
// 默认查询前5条
KnowledgeSearchResult searchResp = embeddingHandler.embeddingSearch(Collections.singletonList(knowId), queryText, (int) count, null);
return Result.ok(searchResp);
}
}

View File

@ -17,6 +17,7 @@ import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
@ -172,11 +173,16 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
try {
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
}else{
}else if(LLMConsts.MODEL_TYPE_EMBED.equals(airagModel.getModelType())){
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
embeddingModel.embed("test text");
//update-begin---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---=
}else if(LLMConsts.MODEL_TYPE_IMAGE.equals(airagModel.getModelType())){
AIChatParams aiChatParams = new AIChatParams();
aiChatHandler.imageGenerate(airagModel, "To test whether it can be successfully called, simply return success", aiChatParams);
}
//update-end---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---
}catch (Exception e){
log.error("测试模型连接失败", e);
return Result.error("测试模型连接失败,请检查模型配置是否正确!");

View File

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

View File

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

View File

@ -12,13 +12,16 @@ 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;
@ -54,6 +57,8 @@ public class AIChatHandler implements IAIChatHandler {
@Autowired
LLMHandler llmHandler;
@Autowired
AiRagConfigBean aiRagConfigBean;
@Value(value = "${jeecg.path.upload:}")
private String uploadpath;
@ -285,7 +290,7 @@ public class AIChatHandler implements IAIChatHandler {
// 默认超时时间
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
params.setTimeout(60);
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT);
}
//deepseek-reasoner 推理模型不支持插件tool
@ -301,6 +306,7 @@ public class AIChatHandler implements IAIChatHandler {
/**
* 构造插件和MCP工具
* for [QQYUN-12453]【AI】支持插件
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
* @param params
* @author chenrui
* @date 2025/10/31 14:04
@ -310,6 +316,7 @@ public class AIChatHandler implements IAIChatHandler {
if(oConvertUtils.isObjectNotEmpty(pluginIds)){
List<McpToolProvider> mcpToolProviders = new ArrayList<>();
List<McpToolProviderWrapper> mcpToolProviderWrappers = new ArrayList<>();
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
@ -325,15 +332,18 @@ public class AIChatHandler implements IAIChatHandler {
}
if ("mcp".equalsIgnoreCase(category)) {
// MCP类型构建McpToolProvider
McpToolProvider mcpToolProvider = buildMcpToolProvider(
// MCP类型构建McpToolProviderWrapper包含连接引用用于后续关闭
// for [QQYUN-9234] MCP服务连接关闭
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
airagMcp.getName(),
airagMcp.getType(),
airagMcp.getEndpoint(),
airagMcp.getHeaders()
airagMcp.getHeaders(),
aiRagConfigBean.getAllowSensitiveNodes()
);
if (mcpToolProvider != null) {
mcpToolProviders.add(mcpToolProvider);
if (wrapper != null) {
mcpToolProviders.add(wrapper.getMcpToolProvider());
mcpToolProviderWrappers.add(wrapper);
}
} else if ("plugin".equalsIgnoreCase(category)) {
// 插件类型构建ToolSpecification和ToolExecutor
@ -348,6 +358,12 @@ public class AIChatHandler implements IAIChatHandler {
if (!mcpToolProviders.isEmpty()) {
params.setMcpToolProviders(mcpToolProviders);
}
// 保存MCP连接包装器用于后续关闭
// for [QQYUN-9234] MCP服务连接关闭
if (!mcpToolProviderWrappers.isEmpty()) {
params.setMcpToolProviderWrappers(mcpToolProviderWrappers);
}
// 设置插件工具
if (!pluginTools.isEmpty()) {
@ -401,5 +417,129 @@ 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 绘画创作 ========================================
}

View File

@ -17,13 +17,17 @@ import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.filter.Filter;
import dev.langchain4j.store.embedding.filter.logical.And;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.tika.parser.AutoDetectParser;
import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*;
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
@ -94,11 +98,21 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
private static final int DEFAULT_OVERLAP_SIZE = 50;
/**
* 最大输出长度
*/
private static final int DEFAULT_MAX_OUTPUT_CHARS = 4000;
/**
* 向量存储元数据:knowledgeId
*/
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
/**
* 向量存储元数据: 用户账号
*/
public static final String EMBED_STORE_METADATA_USER_NAME = "username";
/**
* 向量存储元数据:docId
*/
@ -109,6 +123,11 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
/**
* 向量存储元数据:创建时间
*/
public static final String EMBED_STORE_CREATE_TIME = "createTime";
/**
* 向量存储缓存
*/
@ -175,7 +194,26 @@ public class EmbeddingHandler implements IEmbeddingHandler {
.build();
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()));
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()))
//初始化记忆库的时候添加创建时间选项
.put(EMBED_STORE_CREATE_TIME, String.valueOf(doc.getCreateTime() != null ? doc.getCreateTime().getTime() : System.currentTimeMillis()));
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
//添加用户名字到元数据里面,用于记忆库中数据隔离
String username = doc.getCreateBy();
if (oConvertUtils.isEmpty(username)) {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String token = TokenUtils.getTokenByRequest(request);
username = JwtUtil.getUsername(token);
} catch (Exception e) {
// ignoretoken获取不到默认为admin
username = "admin";
}
}
if (oConvertUtils.isNotEmpty(username)) {
metadata.put(EMBED_STORE_METADATA_USER_NAME, username);
}
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
Document from = Document.from(content, metadata);
ingestor.ingest(from);
return metadata.toMap();
@ -208,16 +246,47 @@ public class EmbeddingHandler implements IEmbeddingHandler {
}
}
//命中的文档内容
StringBuilder data = new StringBuilder();
// 对documents按score降序排序并取前topNumber个
List<Map<String, Object>> sortedDocuments = documents.stream()
.sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
.limit(topNumber)
.peek(doc -> data.append(doc.get("content")).append("\n"))
//update-begin---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
//是否为记忆库
boolean memoryMode = false;
//记忆库只有一个
if (knowIds.size() == 1) {
String firstId = knowIds.get(0);
if (oConvertUtils.isNotEmpty(firstId)) {
AiragKnowledge k = airagKnowledgeMapper.getByIdIgnoreTenant(firstId);
memoryMode = (k != null && LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(k.getType()));
}
}
//如果是记忆库按照创建时间排序如果不是按照score分值进行排序
List<Map<String, Object>> prepared = documents.stream()
.sorted(memoryMode
? Comparator.comparingLong((Map<String, Object> doc) -> oConvertUtils.getLong(doc.get(EMBED_STORE_CREATE_TIME), 0L)).reversed()
: Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
.collect(Collectors.toList());
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
List<Map<String, Object>> limited = new ArrayList<>();
//将返回的结果按照最大的token进行长度限制
for (Map<String, Object> doc : prepared) {
if (limited.size() >= topNumber) {
break;
}
String content = oConvertUtils.getString(doc.get("content"), "");
int remain = DEFAULT_MAX_OUTPUT_CHARS - data.length();
if (remain <= 0) {
break;
}
//数据库中文本的长度和已经拼接的长度
if (content.length() <= remain) {
data.append(content).append("\n");
limited.add(doc);
} else {
data.append(content, 0, remain);
limited.add(doc);
break;
}
}
return new KnowledgeSearchResult(data.toString(), limited);
//update-end---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
}
/**
@ -244,11 +313,31 @@ public class EmbeddingHandler implements IEmbeddingHandler {
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
// 记忆库的时候需要根据用户隔离
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String token = TokenUtils.getTokenByRequest(request);
String username = JwtUtil.getUsername(token);
if (oConvertUtils.isNotEmpty(username)) {
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
}
} catch (Exception e) {
// ignore
log.info("构建过滤器异常,{}",e.getMessage());
}
}
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(topNumber)
.minScore(similarity)
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.filter(filter)
.build();
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
@ -262,6 +351,9 @@ public class EmbeddingHandler implements IEmbeddingHandler {
Metadata metadata = matchRes.embedded().metadata();
data.put("chunk", metadata.getInteger("index"));
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
//查询返回的时候增加创建时间,用于排序
String ct = metadata.getString(EMBED_STORE_CREATE_TIME);
data.put(EMBED_STORE_CREATE_TIME, ct);
return data;
}).collect(Collectors.toList());
}
@ -295,13 +387,32 @@ public class EmbeddingHandler implements IEmbeddingHandler {
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
topNumber = oConvertUtils.getInteger(topNumber, 5);
similarity = oConvertUtils.getDou(similarity, 0.75);
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
// 记忆库的时候需要根据用户隔离
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String token = TokenUtils.getTokenByRequest(request);
String username = JwtUtil.getUsername(token);
if (oConvertUtils.isNotEmpty(username)) {
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
}
} catch (Exception e) {
// ignore
log.info("构建过滤器异常,{}",e.getMessage());
}
}
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(topNumber)
.minScore(similarity)
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.filter(filter)
.build();
retrievers.add(contentRetriever);
}

View File

@ -12,6 +12,8 @@ import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.flow.component.ToolsNode;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -51,8 +53,9 @@ public class PluginToolBuilder {
}
String baseUrl = airagMcp.getEndpoint();
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
// 如果baseUrl为空使用当前系统地址
if (oConvertUtils.isEmpty(baseUrl)) {
if (isEmptyBaseUrl) {
if (currentHttpRequest != null) {
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
log.info("插件[{}]的BaseURL为空使用系统地址: {}", airagMcp.getName(), baseUrl);
@ -64,7 +67,10 @@ public class PluginToolBuilder {
// 解析headers
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
// 判断是否需要加签
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
// 解析并应用授权配置从metadata中读取
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
@ -76,7 +82,7 @@ public class PluginToolBuilder {
try {
ToolSpecification spec = buildToolSpecification(toolConfig);
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign);
if (spec != null && executor != null) {
tools.put(spec, executor);
}
@ -187,7 +193,7 @@ public class PluginToolBuilder {
/**
* 构建ToolExecutor
*/
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders) {
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders, boolean isNeedSign) {
String path = toolConfig.getString("path");
String method = toolConfig.getString("method");
JSONArray parameters = toolConfig.getJSONArray("parameters");
@ -215,8 +221,13 @@ public class PluginToolBuilder {
JSONObject urlVariables = buildUrlVariables(parameters, args);
Object body = buildRequestBody(parameters, args, httpHeaders);
// 发送HTTP请求
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
if (isNeedSign) {
// 发送请求前加签
ToolsNode.Helper.applySignature(url, httpHeaders, urlVariables, body);
}
// 发送HTTP请求,增加超时时间
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class, AiragConsts.DEFAULT_TIMEOUT * 1000);
// 直接返回原始响应字符串,不进行解析
return response.getBody() != null ? response.getBody() : "";
@ -335,7 +346,13 @@ public class PluginToolBuilder {
if (isQueryParam || !isOtherType) {
Object value = args.get(paramName);
if (value != null) {
urlVariables.put(paramName, value);
//如果是知识库的id赋值默认值
if ("knowledgeId".equalsIgnoreCase(paramName)) {
String defaultValue = param.getString("defaultValue");
urlVariables.put(paramName, defaultValue);
} else {
urlVariables.put(paramName, value);
}
}
}
}
@ -392,7 +409,13 @@ public class PluginToolBuilder {
Object value = args.get(paramName);
if (value != null) {
body.put(paramName, value);
//如果是知识库的id赋值默认值
if ("knowledgeId".equalsIgnoreCase(paramName)) {
String defaultValue = param.getString("defaultValue");
body.put(paramName, defaultValue);
} else {
body.put(paramName, value);
}
} else {
// 检查是否有默认值
String defaultValue = param.getString("defaultValue");

View File

@ -0,0 +1,19 @@
package org.jeecg.modules.airag.llm.service;
import java.util.Map;
/**
* @Description: 获取流程mcp服务
* @Author: wangshuai
* @Date: 2025-12-22 15:34:20
* @Version: V1.0
*/
public interface IAiragFlowPluginService {
/**
* 同步所有启用的流程到MCP插件配置
*
* @param flowIds 多个流程id
*/
Map<String, Object> getFlowsToPlugin(String flowIds);
}

View File

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

View File

@ -0,0 +1,45 @@
package org.jeecg.modules.airag.llm.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.airag.api.IAiragBaseApi;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
/**
* airag baseAPI 实现类
*/
@Slf4j
@Primary
@Service("airagBaseApiImpl")
public class AiragBaseApiImpl implements IAiragBaseApi {
@Autowired
private IAiragKnowledgeDocService airagKnowledgeDocService;
@Override
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
AssertUtils.assertNotEmpty("知识库ID不能为空", knowledgeId);
AssertUtils.assertNotEmpty("写入内容不能为空", content);
AiragKnowledgeDoc knowledgeDoc = new AiragKnowledgeDoc();
knowledgeDoc.setKnowledgeId(knowledgeId);
knowledgeDoc.setTitle(title);
knowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
knowledgeDoc.setContent(content);
Result<?> result = airagKnowledgeDocService.editDocument(knowledgeDoc);
if (!result.isSuccess()) {
throw new JeecgBootBizTipException(result.getMessage());
}
if (knowledgeDoc.getId() == null) {
throw new JeecgBootBizTipException("知识库文档ID为空");
}
log.info("[AI-KNOWLEDGE] 文档写入完成,知识库:{}, 文档ID:{}", knowledgeId, knowledgeDoc.getId());
return knowledgeDoc.getId();
}
}

View File

@ -0,0 +1,231 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* @Description: 流程同步到MCP服务实现类
* @Author: wangshuai
* @Date: 2025-12-22
* @Version: V1.0
*/
@Service
@Slf4j
public class AiragFlowPluginServiceImpl implements IAiragFlowPluginService {
@Autowired
private IAiragFlowService airagFlowService;
@Override
public Map<String, Object> getFlowsToPlugin(String flowIds) {
log.info("开始构建流程插件");
// 1. 查询所有启用的流程
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AiragFlow::getStatus, FlowConsts.FLOW_STATUS_ENABLE);
queryWrapper.in(AiragFlow::getId, Arrays.asList(flowIds.split(SymbolConstant.COMMA)));
List<AiragFlow> flows = airagFlowService.list(queryWrapper);
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
if (flows.isEmpty()) {
log.info("当前应用所选流程没有启用的流程");
return null;
}
//返回数据
Map<String, Object> result = new HashMap<>();
//插件
//插件id
AiragMcp tool = new AiragMcp();
// 2. 构建插件
String id = UUID.randomUUID().toString().replace("-", "");
tool.setId(id);
// 插件名称
tool.setName(FlowPluginContent.PLUGIN_NAME);
// 描述
tool.setDescr(FlowPluginContent.PLUGIN_DESC);
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
tool.setSynced(CommonConstant.STATUS_1_INT);
tool.setCategory("plugin");
tool.setEndpoint("");
int toolCount = 0;
//构建拆件工具
for (AiragFlow flow : flows) {
try {
SubFlowResult subFlow = new SubFlowResult(flow);
// 获取入参参数
JSONArray parameter = getInputParameter(flow, subFlow);
// 获取出参参数
JSONArray outParams = getOutputParameter(flow, subFlow);
// name必须符合 ^[a-zA-Z0-9_-]+$
String validToolName = "flow_" + flow.getId();
// 将原始名称拼接到描述中
String description = flow.getName();
if (oConvertUtils.isNotEmpty(flow.getDescr())) {
description += " : " + flow.getDescr();
}
//构造工具参数
String flowTool = buildParameter(parameter, outParams, flow.getId(), tool.getTools(), validToolName, description);
tool.setTools(flowTool);
toolCount++;
} catch (Exception e) {
log.error("处理流程[{}]转换插件失败: {}", flow.getName(), e.getMessage());
}
}
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
//构建元数据(请求头)
String meataData = buildMetadata(toolCount, tenantId);
tool.setMetadata(meataData);
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
result.put("pluginTool", tools);
result.put("pluginId", id);
log.info("构建流程插件结束");
return result;
}
/**
* 构建元数据
*
* @param toolCount
* @param tenantId
*/
private String buildMetadata(int toolCount, String tenantId) {
JSONObject jsonObject = new JSONObject();
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
jsonObject.put(FlowPluginContent.TOOL_COUNT, toolCount);
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
return jsonObject.toJSONString();
}
/**
* 构建参数
*
* @param parameter
* @param outParams
* @param flowId
* @param tools
* @param description
* @param name
*/
private String buildParameter(JSONArray parameter, JSONArray outParams, String flowId, String tools, String name, String description) {
JSONArray paramArray = new JSONArray();
JSONObject parameterObject = new JSONObject();
parameterObject.put(FlowPluginContent.NAME, name);
parameterObject.put(FlowPluginContent.DESCRIPTION, description);
parameterObject.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_REQUEST_URL + flowId);
parameterObject.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
parameterObject.put(FlowPluginContent.ENABLED, true);
parameterObject.put(FlowPluginContent.PARAMETERS, parameter);
parameterObject.put(FlowPluginContent.RESPONSES, outParams);
if (oConvertUtils.isNotEmpty(tools)) {
paramArray = JSONArray.parseArray(tools);
paramArray.add(parameterObject);
} else {
paramArray.add(parameterObject);
}
return paramArray.toJSONString();
}
/**
* 获取参数
*
* @param flow
* @param subFlow
*/
private JSONArray getInputParameter(AiragFlow flow, SubFlowResult subFlow) {
JSONArray parameters = new JSONArray();
String metadata = flow.getMetadata();
if (oConvertUtils.isNotEmpty(metadata)) {
JSONObject jsonObject = JSONObject.parseObject(metadata);
if (jsonObject.containsKey(FlowPluginContent.INPUTS)) {
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.INPUTS);
jsonArray.forEach(item -> {
if (oConvertUtils.isNotEmpty(item.toString())) {
JSONObject json = JSONObject.parseObject(item.toString());
json.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
}
});
parameters.addAll(jsonArray);
}
}
//需要获取子流程的参数,子流程的参数是单独封装的,否则在流程执行的时候会报错缺少参数
List<FlowNodeConfig.NodeParam> inputParams = subFlow.getInputParams();
if (inputParams != null) {
for (FlowNodeConfig.NodeParam param : inputParams) {
JSONObject p = new JSONObject();
// 参数名
p.put(FlowPluginContent.NAME, param.getField());
String paramDesc = param.getName();
if (oConvertUtils.isEmpty(paramDesc)) {
paramDesc = param.getField();
}
// 参数描述
p.put(FlowPluginContent.DESCRIPTION, paramDesc);
// 类型
p.put(FlowPluginContent.TYPE, oConvertUtils.getString(param.getType(), FlowPluginContent.TYPE_STRING));
// 所有参数都在Body中
p.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
boolean required = param.getRequired() != null && param.getRequired();
p.put(FlowPluginContent.REQUIRED, required);
parameters.add(p);
}
}
return parameters;
}
/**
* 构建返回值
*/
private JSONArray getOutputParameter(AiragFlow flow, SubFlowResult subFlow) {
JSONArray parameters = new JSONArray();
String metadata = flow.getMetadata();
if (oConvertUtils.isNotEmpty(metadata)) {
JSONObject jsonObject = JSONObject.parseObject(metadata);
if (jsonObject.containsKey(FlowPluginContent.OUTPUTS)) {
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.OUTPUTS);
parameters.addAll(jsonArray);
}
}
// List<FlowNodeConfig.NodeParam> outputParams = subFlow.getOutputParams();
// if (outputParams != null) {
// for (FlowNodeConfig.NodeParam param : outputParams) {
// JSONObject p = new JSONObject();
// // 参数名
// p.put("name", param.getField());
// String paramDesc = param.getName();
// if (oConvertUtils.isEmpty(paramDesc)) {
// paramDesc = param.getField();
// }
// // 参数描述
// p.put("description", paramDesc);
// // 类型
// p.put("type", oConvertUtils.getString(param.getType(), "String"));
// parameters.add(p);
// }
// }
return parameters;
}
}

View File

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

View File

@ -1,18 +1,215 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.service.tool.ToolExecutor;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: AIRag知识库
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
@Slf4j
@Service
public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService {
@Override
public Map<String, Object> getPluginMemory(String memoryId) {
//step 1获取知识库
AiragKnowledge airagKnowledge = this.baseMapper.selectById(memoryId);
if(airagKnowledge == null){
return null;
}
return this.getKnowledgeToPlugin(memoryId,airagKnowledge.getDescr());
}
/**
* 获取插件信息
*
* @param knowledgeId
* @param descr
* @return
*/
public Map<String, Object> getKnowledgeToPlugin(String knowledgeId, String descr) {
//step1 构建插件
log.info("开始构建记忆库插件");
if (oConvertUtils.isEmpty(knowledgeId)) {
return null;
}
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
//返回数据
Map<String, Object> result = new HashMap<>();
//插件
//插件id
AiragMcp tool = new AiragMcp();
// 2. 构建插件
tool.setId(knowledgeId);
// 插件名称
tool.setName(FlowPluginContent.PLUGIN_MEMORY_NAME);
// 描述
tool.setDescr(FlowPluginContent.PLUGIN_MEMORY_DESC);
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
tool.setSynced(CommonConstant.STATUS_1_INT);
tool.setCategory("plugin");
tool.setEndpoint("");
JSONArray toolsArray = new JSONArray();
// 添加记忆
toolsArray.add(buildAddMemoryTool(knowledgeId,descr));
// 查询记忆
toolsArray.add(buildQueryMemoryTool(knowledgeId,descr));
tool.setTools(toolsArray.toJSONString());
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
//构建元数据(请求头)
String meataData = buildMetadata(tenantId);
tool.setMetadata(meataData);
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
result.put("pluginTool", tools);
result.put("pluginId", knowledgeId);
log.info("构建记忆库插件结束");
return result;
}
/**
* 构建元数据
*
* @param tenantId
*/
private String buildMetadata(String tenantId) {
JSONObject jsonObject = new JSONObject();
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
return jsonObject.toJSONString();
}
/**
* 构建添加记忆工具
*
* @param knowId
* @param descr
* @return
*/
private JSONObject buildAddMemoryTool(String knowId, String descr) {
JSONObject tool = new JSONObject();
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
tool.put(FlowPluginContent.NAME, "add_memory");
String addDescPrefix = "【自动触发】向记忆库添加长期信息。范围:";
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_ADD_PATH);
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
tool.put(FlowPluginContent.ENABLED, true);
JSONArray parameters = new JSONArray();
// 知识库ID参数
JSONObject knowIdParam = new JSONObject();
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
knowIdParam.put(FlowPluginContent.REQUIRED, true);
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
parameters.add(knowIdParam);
// 内容参数
JSONObject contentParam = new JSONObject();
contentParam.put(FlowPluginContent.NAME, "content");
contentParam.put(FlowPluginContent.DESCRIPTION, "记忆内容。当前时间为:" + DateUtils.now() + "。格式要求:'在yyyy年MM月dd日 HH:mm分用户[用户的行为/问题]assistant[助手的回答/反应]。'");
contentParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
contentParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
contentParam.put(FlowPluginContent.REQUIRED, true);
parameters.add(contentParam);
// 标题参数
JSONObject titleParam = new JSONObject();
titleParam.put(FlowPluginContent.NAME, "title");
titleParam.put(FlowPluginContent.DESCRIPTION, "记忆标题");
titleParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
titleParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
titleParam.put(FlowPluginContent.REQUIRED, false);
parameters.add(titleParam);
tool.put(FlowPluginContent.PARAMETERS, parameters);
// 响应
JSONArray responses = new JSONArray();
tool.put(FlowPluginContent.RESPONSES, responses);
return tool;
}
/**
* 构建查询记忆工具
*
* @param knowId
* @param descr
* @return
*/
private JSONObject buildQueryMemoryTool(String knowId, String descr) {
JSONObject tool = new JSONObject();
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
String addDescPrefix = "【自动触发】向记忆库检索信息。范围:";
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
tool.put(FlowPluginContent.NAME, "query_memory");
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_QUERY_PATH);
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
tool.put(FlowPluginContent.ENABLED, true);
JSONArray parameters = new JSONArray();
// 知识库ID参数
JSONObject knowIdParam = new JSONObject();
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
knowIdParam.put(FlowPluginContent.REQUIRED, true);
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
parameters.add(knowIdParam);
// 查询内容参数
JSONObject queryTextParam = new JSONObject();
queryTextParam.put(FlowPluginContent.NAME, "queryText");
queryTextParam.put(FlowPluginContent.DESCRIPTION, "查询内容");
queryTextParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
queryTextParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
queryTextParam.put(FlowPluginContent.REQUIRED, true);
parameters.add(queryTextParam);
tool.put(FlowPluginContent.PARAMETERS, parameters);
// 响应
JSONArray responses = new JSONArray();
tool.put(FlowPluginContent.RESPONSES, responses);
return tool;
}
}

View File

@ -10,11 +10,14 @@ import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import dev.langchain4j.model.chat.request.json.*;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.AiRagConfigBean;
import org.jeecg.modules.airag.llm.entity.AiragMcp;
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
@ -22,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
@ -31,12 +35,15 @@ import java.util.stream.Collectors;
* @Date: 2025-10-20
* @Version: V1.0
*/
@Service("airagMcpServiceImpl")
@Slf4j
@SuppressWarnings("removal")
@Service("airagMcpServiceImpl")
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
@Autowired
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
@Autowired
private AiRagConfigBean aiRagConfigBean;
/**
* 新增或编辑Mcpserver
@ -125,7 +132,7 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
Map<String, String> headers = null;
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
try {
headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
headers = JSONObject.parseObject(mcp.getHeaders(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {});
} catch (JSONException e) {
headers = null;
}
@ -136,19 +143,53 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
McpClient mcpClient = null;
try {
if ("sse".equalsIgnoreCase(type)) {
HttpMcpTransport.Builder builder = new HttpMcpTransport.Builder()
//TODO 1.4.0-beta10被弃用,推荐使用http
log.info("[MCP]使用SSE协议(HttpMcpTransport), endpoint:{}", endpoint);
HttpMcpTransport.Builder builder = HttpMcpTransport.builder()
.sseUrl(endpoint)
.logRequests(true)
.logResponses(true);
if (headers != null && !headers.isEmpty()) {
builder.customHeaders(headers);
}
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
} else if ("stdio".equalsIgnoreCase(type)) {
// stdio 类型endpoint 可能是一个命令行,需要拆分为命令列表
// List<String> cmdParts = Arrays.asList(endpoint.trim().split("\\s+"));
// StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
// .command(cmdParts)
// .environment(headers);
// mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
return Result.error("不支持的MCP类型:" + type);
//update-begin---author:wangshuai---date:2025-12-18---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
String openSafe = aiRagConfigBean.getAllowSensitiveNodes();
if(oConvertUtils.isNotEmpty(openSafe) && openSafe.toLowerCase().contains("stdio")) {
log.info("[MCP]使用STDIO协议(StdioMcpTransport), endpoint:{}", endpoint);
// stdio 类型endpoint 可能是一个命令行
// Windows 下需要通过 cmd.exe /c 来执行命令,否则找不到 npx 等程序
List<String> cmdParts;
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
// Windows: 使用 cmd.exe /c 执行
cmdParts = new ArrayList<>();
cmdParts.add("cmd.exe");
cmdParts.add("/c");
cmdParts.add(endpoint.trim());
} else {
// Linux/Mac: 使用 sh -c 执行
cmdParts = new ArrayList<>();
cmdParts.add("sh");
cmdParts.add("-c");
cmdParts.add(endpoint.trim());
}
log.info("[MCP]执行stdio命令: {}", cmdParts);
StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
.command(cmdParts)
.environment(headers);
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
} else {
String disabledMsg = "stdio 功能已禁用。若需启用,请在 yml 的 jeecg.airag.allow-sensitive-nodes 中加入 stdio。";
log.warn("[MCP]{}", disabledMsg);
return Result.error(disabledMsg);
}
//update-end---author:wangshuai---date:2025-12-19---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
}else if("http".equalsIgnoreCase(type)){
log.info("[MCP]使用HTTP协议(StreamableHttpMcpTransport), endpoint:{}", endpoint);
//增加http选项
mcpClient = mcpHttpCreate(endpoint,headers);
} else {
return Result.error("不支持的MCP类型:" + type);
}
@ -204,6 +245,29 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
}
}
/**
* mcp插件http创建
*
* @param endpoint
* @param headers
* @return
*/
private McpClient mcpHttpCreate(String endpoint, Map<String, String> headers) {
StreamableHttpMcpTransport.Builder builder = new StreamableHttpMcpTransport.Builder()
.url(endpoint)
.timeout(Duration.ofMinutes(60))
.logRequests(true)
.logResponses(true);
if (headers != null && !headers.isEmpty()) {
builder.customHeaders(headers);
}
return new DefaultMcpClient.Builder()
.transport(builder.build())
.build();
}
// 安全序列化单个对象为 JSON 字符串
private String safeWriteJson(Object obj) {
try {

View File

@ -0,0 +1,30 @@
package org.jeecg.modules.airag.prompts.consts;
/**
* AI提示词常量类
*
*/
public class AiPromptsConsts {
/**
* 状态:进行中
*/
public static final String STATUS_RUNNING = "run";
/**
* 状态:完成
*/
public static final String STATUS_COMPLETED = "completed";
/**
* 状态:失败
*/
public static final String STATUS_FAILED = "failed";
/**
* 业务类型:评估器
*/
public static final String BIZ_TYPE_EVALUATOR = "evaluator";
/**
* 业务类型:轨迹
*/
public static final String BIZ_TYPE_TRACK = "track";
}

View File

@ -0,0 +1,213 @@
package org.jeecg.modules.airag.prompts.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
import java.util.List;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-24
* @Version: V1.0
*/
@Tag(name="airag_ext_data")
@RestController
@RequestMapping("/airag/extData")
@Slf4j
public class AiragExtDataController extends JeecgController<AiragExtData, IAiragExtDataService> {
@Autowired
private IAiragExtDataService airagExtDataService;
/**
* 分页列表查询
*
* @param airagExtData
* @param pageNo
* @param pageSize
* @param req
* @return
*/
//@AutoLog(value = "airag_ext_data-分页列表查询")
@Operation(summary="airag_ext_data-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<AiragExtData>> queryPageList(AiragExtData airagExtData,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_EVALUATOR);
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 调用轨迹列表查询
*
* @param airagExtData
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@Operation(summary="airag_ext_data-分页列表查询")
@GetMapping(value = "/getTrackList")
public Result<IPage<AiragExtData>> getTrackList(AiragExtData airagExtData,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_TRACK);
String metadata = airagExtData.getMetadata();
if(oConvertUtils.isEmpty(metadata)){
return Result.OK();
}
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param airagExtData
* @return
*/
@AutoLog(value = "airag_ext_data-添加")
@Operation(summary="airag_ext_data-添加")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody AiragExtData airagExtData) {
airagExtData.setBizType(AiPromptsConsts.BIZ_TYPE_EVALUATOR);
airagExtDataService.save(airagExtData);
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param airagExtData
* @return
*/
@AutoLog(value = "airag_ext_data-编辑")
@Operation(summary="airag_ext_data-编辑")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody AiragExtData airagExtData) {
airagExtDataService.updateById(airagExtData);
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "airag_ext_data-通过id删除")
@Operation(summary="airag_ext_data-通过id删除")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
airagExtDataService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "airag_ext_data-批量删除")
@Operation(summary="airag_ext_data-批量删除")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.airagExtDataService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "airag_ext_data-通过id查询")
@Operation(summary="airag_ext_data-通过id查询")
@GetMapping(value = "/queryById")
public Result<AiragExtData> queryById(@RequestParam(name="id",required=true) String id) {
AiragExtData airagExtData = airagExtDataService.getById(id);
if(airagExtData==null) {
return Result.error("未找到对应数据");
}
return Result.OK(airagExtData);
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "airag_ext_data-通过id查询")
@Operation(summary="airag_ext_data-通过id查询")
@GetMapping(value = "/queryTrackById")
public Result<List<AiragExtData>> queryTrackById(@RequestParam(name="id",required=true) String id) {
AiragExtData airagExtData = airagExtDataService.getById(id);
String status = airagExtData.getStatus();
if(AiPromptsConsts.STATUS_RUNNING.equals(status)) {
return Result.error("处理中,请稍后刷新");
}
List<AiragExtData> trackList = airagExtDataService.queryTrackById(id);
return Result.OK(trackList);
}
/**
* 构造器调试
*
* @param debugVo
* @return
*/
@PostMapping(value = "/evaluator/debug")
public Result<?> debugEvaluator(@RequestBody AiragDebugVo debugVo) {
return airagExtDataService.debugEvaluator(debugVo);
}
/**
* 导出excel
*
* @param request
* @param airagExtData
*/
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragExtData airagExtData) {
return super.exportXls(request, airagExtData, AiragExtData.class, "airag_ext_data");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragExtData.class);
}
}

View File

@ -0,0 +1,167 @@
package org.jeecg.modules.airag.prompts.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-24
* @Version: V1.0
*/
@Tag(name="airag_prompts")
@RestController
@RequestMapping("/airag/prompts")
@Slf4j
public class AiragPromptsController extends JeecgController<AiragPrompts, IAiragPromptsService> {
@Autowired
private IAiragPromptsService airagPromptsService;
/**
* 分页列表查询
*
* @param airagPrompts
* @param pageNo
* @param pageSize
* @param req
* @return
*/
//@AutoLog(value = "airag_prompts-分页列表查询")
@Operation(summary="airag_prompts-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<AiragPrompts>> queryPageList(AiragPrompts airagPrompts,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragPrompts> queryWrapper = QueryGenerator.initQueryWrapper(airagPrompts, req.getParameterMap());
Page<AiragPrompts> page = new Page<AiragPrompts>(pageNo, pageSize);
IPage<AiragPrompts> pageList = airagPromptsService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param airagPrompts
* @return
*/
@AutoLog(value = "airag_prompts-添加")
@Operation(summary="airag_prompts-添加")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody AiragPrompts airagPrompts) {
airagPrompts.setDelFlag(CommonConstant.DEL_FLAG_0);
airagPrompts.setStatus("0");
airagPromptsService.save(airagPrompts);
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param airagPrompts
* @return
*/
@AutoLog(value = "airag_prompts-编辑")
@Operation(summary="airag_prompts-编辑")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody AiragPrompts airagPrompts) {
airagPromptsService.updateById(airagPrompts);
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "airag_prompts-通过id删除")
@Operation(summary="airag_prompts-通过id删除")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
airagPromptsService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "airag_prompts-批量删除")
@Operation(summary="airag_prompts-批量删除")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.airagPromptsService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "airag_prompts-通过id查询")
@Operation(summary="airag_prompts-通过id查询")
@GetMapping(value = "/queryById")
public Result<AiragPrompts> queryById(@RequestParam(name="id",required=true) String id) {
AiragPrompts airagPrompts = airagPromptsService.getById(id);
if(airagPrompts==null) {
return Result.error("未找到对应数据");
}
return Result.OK(airagPrompts);
}
/**
* 构造器调试
*
* @param experimentVo
* @return
*/
@PostMapping(value = "/experiment")
public Result<?> promptExperiment(@RequestBody AiragExperimentVo experimentVo, HttpServletRequest request) {
return airagPromptsService.promptExperiment(experimentVo,request);
}
/**
* 导出excel
*
* @param request
* @param airagPrompts
*/
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragPrompts airagPrompts) {
return super.exportXls(request, airagPrompts, AiragPrompts.class, "airag_prompts");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragPrompts.class);
}
}

View File

@ -0,0 +1,99 @@
package org.jeecg.modules.airag.prompts.entity;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableLogic;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecg.common.aspect.annotation.Dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Data
@TableName("airag_ext_data")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="airag_ext_data")
public class AiragExtData implements Serializable {
private static final long serialVersionUID = 1L;
/**主键ID*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private java.lang.String id;
/**业务类型标识(evaluator:评估器track:测试追踪)*/
@Excel(name = "业务类型标识(evaluator:评估器track:测试追踪)", width = 15)
@Schema(description = "业务类型标识(evaluator:评估器track:测试追踪)")
private java.lang.String bizType;
/**名称*/
@Excel(name = "名称", width = 15)
@Schema(description = "名称")
private java.lang.String name;
/**描述信息*/
@Excel(name = "描述信息", width = 15)
@Schema(description = "描述信息")
private java.lang.String descr;
/**标签,多个用逗号分隔*/
@Excel(name = "标签,多个用逗号分隔", width = 15)
@Schema(description = "标签,多个用逗号分隔")
private java.lang.String tags;
/**实际存储内容json*/
@Excel(name = "实际存储内容json", width = 15)
@Schema(description = "实际存储内容json")
private java.lang.String dataValue;
/**元数据,用于存储补充业务数据信息*/
@Excel(name = "元数据,用于存储补充业务数据信息", width = 15)
@Schema(description = "元数据,用于存储补充业务数据信息")
private java.lang.String metadata;
/**评测集数据*/
@Excel(name = "评测集数据", width = 15)
@Schema(description = "评测集数据")
private java.lang.String datasetValue;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private java.util.Date createTime;
/**修改人*/
@Schema(description = "修改人")
private java.lang.String updateBy;
/**修改时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "修改时间")
private java.util.Date updateTime;
/**所属部门*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**租户id*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private java.lang.String tenantId;
/**状态*/
@Excel(name = "状态run:进行中 completed已完成", width = 15)
@Schema(description = "状态run:进行中 completed已完成")
private java.lang.String status;
/**版本*/
@Excel(name = "版本", width = 15)
@Schema(description = "版本")
private java.lang.Integer version;
}

View File

@ -0,0 +1,108 @@
package org.jeecg.modules.airag.prompts.entity;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableLogic;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecg.common.aspect.annotation.Dict;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Data
@TableName("airag_prompts")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="airag_prompts")
public class AiragPrompts implements Serializable {
private static final long serialVersionUID = 1L;
/**主键ID*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private java.lang.String id;
/**提示词名称*/
@Excel(name = "提示词名称", width = 15)
@Schema(description = "提示词名称")
private java.lang.String name;
/**提示词名称*/
@Excel(name = "提示key", width = 15)
@Schema(description = "提示key")
private java.lang.String promptKey;
/**提示词功能描述*/
@Excel(name = "提示词功能描述", width = 15)
@Schema(description = "提示词功能描述")
private java.lang.String description;
/**提示词模板内容,支持变量占位符如 {{variable}}*/
@Excel(name = "提示词模板内容,支持变量占位符如 {{variable}}", width = 15)
@Schema(description = "提示词模板内容,支持变量占位符如 {{variable}}")
private java.lang.String content;
/**提示词分类*/
@Excel(name = "提示词分类", width = 15)
@Schema(description = "提示词分类")
private java.lang.String category;
/**标签,多个逗号分割*/
@Excel(name = "标签,多个逗号分割", width = 15)
@Schema(description = "标签,多个逗号分割")
private java.lang.String tags;
/**适配的大模型ID*/
@Excel(name = "适配的大模型ID", width = 15)
@Schema(description = "适配的大模型ID")
private java.lang.String modelId;
/**大模型的参数配置*/
@Excel(name = "大模型的参数配置", width = 15)
@Schema(description = "大模型的参数配置")
private java.lang.String modelParam;
/**状态0:未发布 1:已发布)*/
@Excel(name = "状态0:未发布 1:已发布)", width = 15)
@Schema(description = "状态0:未发布 1:已发布)")
private java.lang.String status;
/**版本号(格式 0.0.1)*/
@Excel(name = "版本号(格式 0.0.1)", width = 15)
@Schema(description = "版本号(格式 0.0.1)")
private java.lang.String version;
/**删除状态0未删除 1已删除*/
@Excel(name = "删除状态0未删除 1已删除", width = 15)
@Schema(description = "删除状态0未删除 1已删除")
@TableLogic
private java.lang.Integer delFlag;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private java.util.Date createTime;
/**更新人*/
@Schema(description = "更新人")
private java.lang.String updateBy;
/**更新日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private java.util.Date updateTime;
/**所属部门*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**租户id*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private java.lang.String tenantId;
}

View File

@ -0,0 +1,17 @@
package org.jeecg.modules.airag.prompts.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
public interface AiragExtDataMapper extends BaseMapper<AiragExtData> {
}

View File

@ -0,0 +1,17 @@
package org.jeecg.modules.airag.prompts.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
public interface AiragPromptsMapper extends BaseMapper<AiragPrompts> {
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper">
</mapper>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper">
</mapper>

View File

@ -0,0 +1,21 @@
package org.jeecg.modules.airag.prompts.service;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
import java.util.List;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
public interface IAiragExtDataService extends IService<AiragExtData> {
Result debugEvaluator(AiragDebugVo debugVo);
List<AiragExtData> queryTrackById(String id);
}

View File

@ -0,0 +1,18 @@
package org.jeecg.modules.airag.prompts.service;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
public interface IAiragPromptsService extends IService<AiragPrompts> {
Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request);
}

View File

@ -0,0 +1,98 @@
package org.jeecg.modules.airag.prompts.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
import org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper;
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
/**
* @Description: airag_ext_data
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Service("airagExtDataServiceImpl")
public class AiragExtDataServiceImpl extends ServiceImpl<AiragExtDataMapper, AiragExtData> implements IAiragExtDataService {
@Autowired
IAIChatHandler aiChatHandler;
@Override
public Result debugEvaluator(AiragDebugVo debugVo) {
//1.提示词
String prompt = debugVo.getPrompts();
AssertUtils.assertNotEmpty("请输入提示词", prompt);
//2.测试内容
String content = debugVo.getContent();
AssertUtils.assertNotEmpty("请输入测试内容", content);
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(content));
//3.模型数据
String modelId = debugVo.getModelId();
AssertUtils.assertNotEmpty("请选择模型", modelId);
//4.模型参数
String modelParam = debugVo.getModelParam();
// 默认大模型参数
AIChatParams params = new AIChatParams();
params.setTemperature(0.8);
params.setTopP(0.9);
params.setPresencePenalty(0.1);
params.setFrequencyPenalty(0.1);
if(oConvertUtils.isNotEmpty(modelParam)){
JSONObject param = JSON.parseObject(modelParam);
if(param.containsKey("temperature")){
params.setTemperature(param.getDoubleValue("temperature"));
}
if(param.containsKey("topP")){
params.setTemperature(param.getDoubleValue("topP"));
}
if(param.containsKey("presencePenalty")){
params.setTemperature(param.getDoubleValue("presencePenalty"));
}
if(param.containsKey("frequencyPenalty")){
params.setTemperature(param.getDoubleValue("frequencyPenalty"));
}
}
//5.AI问答
String promptValue = aiChatHandler.completions(modelId,messages, params);
if (promptValue == null || promptValue.isEmpty()) {
return Result.error("生成失败");
}
return Result.OK("success", promptValue);
}
/**
* 查询AI问答记录
* @param id
* @return
*/
@Override
public List<AiragExtData> queryTrackById(String id) {
LambdaQueryWrapper<AiragExtData> lqw = new LambdaQueryWrapper<AiragExtData>()
.eq(AiragExtData::getMetadata, id)
.orderByDesc(AiragExtData::getVersion)
.orderByDesc(AiragExtData::getCreateTime);
List<AiragExtData> list = this.baseMapper.selectList(lqw);
return list;
}
}

View File

@ -0,0 +1,394 @@
package org.jeecg.modules.airag.prompts.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
import org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper;
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* @Description: airag_prompts
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Slf4j
@Service("airagPromptsServiceImpl")
public class AiragPromptsServiceImpl extends ServiceImpl<AiragPromptsMapper, AiragPrompts> implements IAiragPromptsService {
@Autowired
IAIChatHandler aiChatHandler;
@Autowired
IAiragExtDataService airagExtDataService;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
// 创建静态线程池,确保整个应用生命周期中只有一个实例
private static final ExecutorService executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 防止内存溢出
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
/**
* 提示词实验
* @param experimentVo
* @return
*/
@Override
public Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request) {
log.info("开始执行提示词实验,参数:{}", JSON.toJSONString(experimentVo));
// 参数验证
String promptKey = experimentVo.getPromptKey();
AssertUtils.assertNotEmpty("请选择提示词", promptKey);
String dataId = experimentVo.getExtDataId();
AssertUtils.assertNotEmpty("请选择数据集", dataId);
Map<String, String> fieldMappings = experimentVo.getMappings();
AssertUtils.assertNotEmpty("请配置字段映射", fieldMappings);
try {
//1.查询提示词
AiragPrompts airagPrompts = this.baseMapper.selectOne(new LambdaQueryWrapper<AiragPrompts>().eq(AiragPrompts::getPromptKey, promptKey));
AssertUtils.assertNotEmpty("未找到指定的提示词", airagPrompts);
String modelParam = airagPrompts.getModelParam();
// 过滤提示词变量
JSONArray promptVariables;
if(oConvertUtils.isNotEmpty(modelParam)){
JSONObject airagPromptsParams = JSON.parseObject(modelParam);
if(airagPromptsParams.containsKey("promptVariables")){
promptVariables = airagPromptsParams.getJSONArray("promptVariables");
} else {
promptVariables = null;
}
} else {
promptVariables = null;
}
//2.查询数据集
AiragExtData airagExtData = airagExtDataService.getById(dataId);
AssertUtils.assertNotEmpty("未找到指定的数据集", airagExtData);
String datasetValue = airagExtData.getDatasetValue();
if(oConvertUtils.isEmpty(datasetValue)){
return Result.error("评测集不能为空!");
}
//3.异步调用 根据映射字段,调用评估器测评
JSONObject datasetObj = JSONObject.parseObject(datasetValue);
//评测列配置
JSONArray columns = datasetObj.getJSONArray("columns");
//评测题库
JSONArray datasetArray = datasetObj.getJSONArray("dataSource");
AssertUtils.assertNotEmpty("数据集中没有找到数据源", datasetArray);
AssertUtils.assertTrue("数据源为空", datasetArray.size() > 0);
//测评结果集 - 使用线程安全的CopyOnWriteArrayList
List<JSONObject> scoreResult = new CopyOnWriteArrayList<>();
// 批量提交任务
List<CompletableFuture<Void>> futures = IntStream.range(0, datasetArray.size())
.mapToObj(i -> CompletableFuture.runAsync(() -> {
try {
log.info("开始处理第{}条数据", i + 1);
//定义返回结果
JSONObject result = new JSONObject();
//评测数据
JSONObject dataset = datasetArray.getJSONObject(i);
result.putAll(dataset);
//用户问题
String userQuery = dataset.getString(fieldMappings.get("user_query"));
result.put("userQuery", userQuery);
//变量处理
if(!CollectionUtils.isEmpty(promptVariables)){
String content = airagPrompts.getContent();
for (Object var : promptVariables){
JSONObject variable = JSONObject.parseObject(var.toString());
String name = dataset.getString(fieldMappings.get(variable.getString("name")));
//提示词默认变量值
String defaultValue = variable.getString("value");
// 获取目标类型
String dataType = findDataType(columns, variable);
if("FILE".equals(dataType)){
defaultValue = getFileAccessHttpUrl(request, defaultValue);
name = getFileAccessHttpUrl(request, name);
}
if(oConvertUtils.isNotEmpty(name)){
//提示词 评估集变量值替换
content = content.replaceAll(variable.getString("name"), name);
}else if(oConvertUtils.isNotEmpty(defaultValue)){
content = content.replaceAll(variable.getString("name"), defaultValue);
}
}
airagPrompts.setContent(content);
}
//提示词答案
String promptAnswer = getPromptAnswer(airagPrompts, dataset, fieldMappings);
result.put("promptAnswer", promptAnswer);
//评估器答案
String answerScore = getAnswerScore(promptAnswer, dataset, fieldMappings, airagExtData);
result.put("answerScore", answerScore);
scoreResult.add(result);
log.info("第{}条数据处理完成", i + 1);
} catch (Exception e) {
log.error("处理第{}条数据时发生异常", i + 1, e);
// 重新抛出异常让CompletableFuture捕获
throw new CompletionException(e);
}
}, executor))
.collect(Collectors.toList());
// 非阻塞方式处理完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("批量处理失败", ex);
// 更新状态为失败
airagExtData.setStatus(AiPromptsConsts.STATUS_FAILED);
} else {
log.info("所有数据处理完成,共处理{}条数据", scoreResult.size());
// 查询已存在的评测记录
List<AiragExtData> existingTracks = airagExtDataService.queryTrackById(dataId);
Integer version = 1;
if(!CollectionUtils.isEmpty(existingTracks)) {
version = existingTracks.stream()
.map(AiragExtData::getVersion)
.max(Integer::compareTo)
.orElse(0) + 1;
}
for (JSONObject item : scoreResult) {
// 保存结果
AiragExtData track = new AiragExtData();
//关联评估器ID
track.setMetadata(dataId);
//定义类型
track.setBizType(AiPromptsConsts.BIZ_TYPE_TRACK);
//定义版本
track.setVersion(version);
//定义状态
track.setStatus(AiPromptsConsts.STATUS_COMPLETED);
//定义评测结果
track.setDataValue(item.toJSONString());
airagExtDataService.save(track);
}
// 更新状态为完成
airagExtData.setStatus(AiPromptsConsts.STATUS_COMPLETED);
}
airagExtDataService.updateById(airagExtData);
});
//4.修改状态进行中
airagExtData.setStatus(AiPromptsConsts.STATUS_RUNNING);
airagExtDataService.updateById(airagExtData);
log.info("提示词实验已提交,共{}条数据待处理", datasetArray.size());
return Result.OK("实验已开始,正在处理数据");
} catch (Exception e) {
log.error("提示词实验执行失败", e);
return Result.error("实验执行失败:" + e.getMessage());
}
}
/**
* 提示词回答的结果
* @param airagPrompts
* @param questions
* @param fieldMappings
* @return
*/
public String getPromptAnswer(AiragPrompts airagPrompts, JSONObject questions, Map<String, String> fieldMappings) {
try {
//0.判断是否配置了判断fieldMappings的value值 是否包含actual_output
if (!fieldMappings.containsValue("actual_output")) {
log.warn("字段映射中没有配置actual_output");
return null;
}
//1.提示词
String prompt = airagPrompts.getContent();
AssertUtils.assertNotEmpty("请输入提示词", prompt);
String userQuery = questions.getString(fieldMappings.get("user_query"));
AssertUtils.assertNotEmpty("请输入测试内容", userQuery);
//2.ai问题组装
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
//3.模型数据
String modelId = airagPrompts.getModelId();
AssertUtils.assertNotEmpty("请选择模型", modelId);
//4.模型参数
String modelParam = airagPrompts.getModelParam();
// 默认大模型参数
AIChatParams params = new AIChatParams();
params.setTemperature(0.8);
params.setTopP(0.9);
params.setPresencePenalty(0.1);
params.setFrequencyPenalty(0.1);
if(oConvertUtils.isNotEmpty(modelParam)){
JSONObject param = JSON.parseObject(modelParam);
if(param.containsKey("temperature")){
params.setTemperature(param.getDoubleValue("temperature"));
}
if(param.containsKey("topP")){
params.setTopP(param.getDoubleValue("topP")); // 修复:设置到正确的字段
}
if(param.containsKey("presencePenalty")){
params.setPresencePenalty(param.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
}
if(param.containsKey("frequencyPenalty")){
params.setFrequencyPenalty(param.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
}
}
log.debug("调用AI模型模型ID{},参数:{}", modelId, JSON.toJSONString(params));
//5.AI问答
String promptAnswer = aiChatHandler.completions(modelId, messages, params);
log.debug("AI模型返回结果{}", promptAnswer);
return promptAnswer;
} catch (Exception e) {
log.error("获取提示词回答失败", e);
return null;
}
}
/**
* 评测答案分数
* @return
*/
public String getAnswerScore(String promptAnswer, JSONObject questions, Map<String, String> fieldMappings, AiragExtData airagExtData) {
try {
//1.提示词
String prompt = airagExtData.getDataValue();
AssertUtils.assertNotEmpty("请输入提示词", prompt);
prompt += "定义返回格式: 得分最终的得分必须输出一个数字表示满足Prompt中评分标准的程度。得分范围从 0.0 到 1.01.0 表示完全满足评分标准0.0 表示完全不满足评分标准。\n" +
"原因:{对得分的可读性的解释,说明打分原因}。最后,必须用一句话结束理由,该句话为:因此,应该给出的分数是<你评测的的得分>。请勿返回提问的问题、添加分析过程、解释说明等内容,只返回要求的格式内容";
String userQuery = "输入的内容:";
//2.拼接测试内容
for (Map.Entry<String, String> entry : fieldMappings.entrySet()) {
// 评估器中的key
String key = entry.getKey();
// 评估器中的映射的key
String value = entry.getValue();
String valueData;
if("actual_output".equalsIgnoreCase(value)){
valueData = promptAnswer;
}else{
valueData = questions.getString(value);
}
userQuery += (key + ":" + valueData + " ");
}
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
//3.模型数据
String metadata = airagExtData.getMetadata();
if(oConvertUtils.isNotEmpty(metadata)){
JSONObject modelParam = JSONObject.parseObject(metadata);
String modelId = modelParam.getString("modelId");
AssertUtils.assertNotEmpty("评估器模型ID不能为空", modelId);
// 默认大模型参数
AIChatParams params = new AIChatParams();
params.setTemperature(0.8);
params.setTopP(0.9);
params.setPresencePenalty(0.1);
params.setFrequencyPenalty(0.1);
if(oConvertUtils.isNotEmpty(modelParam)){
if(modelParam.containsKey("temperature")){
params.setTemperature(modelParam.getDoubleValue("temperature"));
}
if(modelParam.containsKey("topP")){
params.setTopP(modelParam.getDoubleValue("topP")); // 修复:设置到正确的字段
}
if(modelParam.containsKey("presencePenalty")){
params.setPresencePenalty(modelParam.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
}
if(modelParam.containsKey("frequencyPenalty")){
params.setFrequencyPenalty(modelParam.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
}
}
log.debug("调用评估器模型模型ID{},参数:{}", modelId, JSON.toJSONString(params));
//5.AI问答
String answerScore = aiChatHandler.completions(modelId, messages, params);
log.debug("评估器模型返回结果:{}", answerScore);
return answerScore;
}
return null;
} catch (Exception e) {
log.error("获取答案评分失败", e);
return null;
}
}
/**
*
* @param columns
* @param variable
* @return
*/
public static String findDataType(JSONArray columns, JSONObject variable) {
// 获取目标字段值
String targetName = variable.getString("name");
// 使用 Stream API 查找并获取 dataType
return columns.stream()
.map(obj -> JSONObject.parseObject(obj.toString()))
.filter(column -> targetName.equals(column.getString("name")))
.findFirst()
.map(column -> column.getString("dataType"))
.orElse(null); // 如果没有找到,返回 null
}
/**
* 获取图片地址
* @param request
* @param url
* @return
*/
private String getFileAccessHttpUrl(HttpServletRequest request,String url){
if(oConvertUtils.isNotEmpty(url) && url.startsWith("http")){
return url;
}else{
return CommonUtils.getBaseUrl(request) + "/sys/common/static/" + url;
}
}
}

View File

@ -0,0 +1,39 @@
package org.jeecg.modules.airag.prompts.vo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* @Description: AiragDebugVo
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Data
public class AiragDebugVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 提示词
*/
private String prompts;
/**
* 输入内容
*/
private String content;
/**适配的大模型ID*/
private String modelId;
/**大模型的参数配置*/
private String modelParam;
}

View File

@ -0,0 +1,30 @@
package org.jeecg.modules.airag.prompts.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* @Description: AiragExperimentVo
* @Author: jeecg-boot
* @Date: 2025-12-12
* @Version: V1.0
*/
@Data
public class AiragExperimentVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 提示词
*/
private String promptKey;
/**
* 输入内容
*/
private String extDataId;
/**
* 映射关系
*/
private Map<String,String> mappings;
}

View File

@ -0,0 +1,30 @@
package org.jeecg.modules.airag.wordtpl.consts;
import lombok.Getter;
/**
* @author chenrui
* @ClassName: TitleLevelEnum
* @Description: 标题级别
* @date 2024年5月4日07:38:30
*/
@Getter
public enum WordTitleEnum {
FIRST("first", "标题1"),
SECOND("second", "标题2"),
THIRD("third", "标题3"),
FOURTH("fourth", "标题4"),
FIFTH("fifth", "标题5"),
SIXTH("sixth", "标题6");
WordTitleEnum(String code, String name) {
this.code = code;
this.name = name;
}
final String code;
final String name;
}

View File

@ -0,0 +1,244 @@
package org.jeecg.modules.airag.wordtpl.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Arrays;
/**
* @Description: word模版管理
* @Author: jeecg-boot
* @Date: 2025-07-04
* @Version: V1.0
*/
@Tag(name = "word模版管理")
@RestController("eoaWordTemplateController")
@RequestMapping("/airag/word")
@Slf4j
public class EoaWordTemplateController extends JeecgController<EoaWordTemplate, IEoaWordTemplateService> {
@Autowired
private IEoaWordTemplateService eoaWordTemplateService;
@Autowired
WordTplUtils wordTplUtils;
/**
* 分页列表查询
*
* @param eoaWordTemplate
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@Operation(summary = "word模版管理-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<EoaWordTemplate>> queryPageList(EoaWordTemplate eoaWordTemplate,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<EoaWordTemplate> queryWrapper = QueryGenerator.initQueryWrapper(eoaWordTemplate, req.getParameterMap());
Page<EoaWordTemplate> page = new Page<EoaWordTemplate>(pageNo, pageSize);
IPage<EoaWordTemplate> pageList = eoaWordTemplateService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param eoaWordTemplate
* @return
*/
@AutoLog(value = "word模版管理-添加")
@Operation(summary = "word模版管理-添加")
// @RequiresPermissions("wordtpl:template:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody EoaWordTemplate eoaWordTemplate) {
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
boolean isCodeExists = eoaWordTemplateService.exists(Wrappers.lambdaQuery(EoaWordTemplate.class).eq(EoaWordTemplate::getCode, eoaWordTemplate.getCode()));
AssertUtils.assertFalse("模版编码已存在", isCodeExists);
eoaWordTemplateService.save(eoaWordTemplate);
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param eoaWordTemplate
* @return
*/
@AutoLog(value = "word模版管理-编辑")
@Operation(summary = "word模版管理-编辑")
// @RequiresPermissions("wordtpl:template:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody EoaWordTemplate eoaWordTemplate) {
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
// 避免编辑时修改编码
eoaWordTemplate.setCode(null);
eoaWordTemplateService.updateById(eoaWordTemplate);
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "word模版管理-通过id删除")
@Operation(summary = "word模版管理-通过id删除")
// @RequiresPermissions("wordtpl:template:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
eoaWordTemplateService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "word模版管理-批量删除")
@Operation(summary = "word模版管理-批量删除")
// @RequiresPermissions("wordtpl:template:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.eoaWordTemplateService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "word模版管理-通过id查询")
@Operation(summary = "word模版管理-通过id查询")
@GetMapping(value = "/queryById")
public Result<EoaWordTemplate> queryById(@RequestParam(name = "id", required = true) String id) {
EoaWordTemplate eoaWordTemplate = eoaWordTemplateService.getById(id);
if (eoaWordTemplate == null) {
return Result.error("未找到对应数据");
}
return Result.OK(eoaWordTemplate);
}
/**
* 下载word模版
* @param id
* @param response
* @return
* @author chenrui
* @date 2025/7/9 14:38
*/
@GetMapping(value = "/download")
public void downloadTemplate(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
AssertUtils.assertNotEmpty("请先选择模版", id);
EoaWordTemplate template = eoaWordTemplateService.getById(id);
try (ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
String fileName = template.getName();
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
response.addHeader("filename", encodedFileName + ".docx");
byte[] bytes = wordTemplateOut.toByteArray();
response.setHeader("Content-Length", String.valueOf(bytes.length));
bos.write(bytes);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new JeecgBootException("下载word模版失败: " + e.getMessage(), e);
}
}
/**
* 解析word模版文件
* @param file
* @param id
* @return
* @author chenrui
* @date 2025/7/9 14:38
*/
@PostMapping(value = "/parse/file")
public Result<?> parseWOrdFile(@RequestParam("file") MultipartFile file) {
try {
InputStream inputStream = file.getInputStream();
EoaWordTemplate eoaWordTemplate = wordTplUtils.parseWordFile(inputStream);
log.info("解析的模版信息: {}", eoaWordTemplate);
return Result.OK("解析成功", eoaWordTemplate);
} catch (Exception e) {
throw new RuntimeException("解析word模版失败: " + e.getMessage(), e);
}
}
/**
* 生成word文档
*
* @param wordTplGenDTO
* @param response
* @author chenrui
* @date 2025/7/10 15:39
*/
@PostMapping(value = "/generate/word")
public void generateWord(@RequestBody WordTplGenDTO wordTplGenDTO, HttpServletResponse response) {
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
EoaWordTemplate template ;
if (oConvertUtils.isNotEmpty(wordTplGenDTO.getTemplateId())) {
template = eoaWordTemplateService.getById(wordTplGenDTO.getTemplateId());
}else{
AssertUtils.assertNotEmpty("请先选择模版", wordTplGenDTO.getTemplateCode());
template = eoaWordTemplateService.getOne(Wrappers.lambdaQuery(EoaWordTemplate.class)
.eq(EoaWordTemplate::getCode, wordTplGenDTO.getTemplateCode()));
}
AssertUtils.assertNotEmpty("未找到对应的模版", template);
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
eoaWordTemplateService.generateWordFromTpl(wordTplGenDTO, outputStream);
String fileName = template.getName();
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
response.addHeader("filename", encodedFileName + ".docx");
byte[] bytes = outputStream.toByteArray();
response.setHeader("Content-Length", String.valueOf(bytes.length));
bos.write(bytes);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new JeecgBootException("生成word文档失败: " + e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,27 @@
package org.jeecg.modules.airag.wordtpl.dto;
import lombok.Data;
/**
* 合并列DTO
* @author chenrui
* @date 2025/7/4 18:36
*/
@Data
public class MergeColDTO {
/**
* 合并列的行号
*/
private int row;
/**
* 合并列的起始列号
*/
private int from;
/**
* 合并列的结束列号
*/
private int to;
}

View File

@ -0,0 +1,47 @@
package org.jeecg.modules.airag.wordtpl.dto;
import lombok.Data;
/**
* @ClassName: DocImageDto
* @Description: word文档图片用实体类
* @author chenrui
* @date 2024-10-02 09:17:59
*/
@Data
public class WordImageDTO {
/**
* @Fields type : 类型
* @author chenrui
* @date 2024-09-29 08:53:27
*/
private String type = "image";
/**
* @Fields value : 内容
* @author chenrui
* @date 2024-09-24 10:20:12
*/
private String value = "";
/**
* @Fields width : 图片宽度
* @author chenrui
* @date 2024-10-02 09:22:33
*/
private double width;
/**
* @Fields height : 图片高度
* @author chenrui
* @date 2024-10-02 09:22:40
*/
private double height;
/**
* @Fields rowFlex : 水平对齐方式默认left
* @author chenrui
* @date 2024-09-27 09:12:18
*/
private String rowFlex = "left";
}

View File

@ -0,0 +1,45 @@
package org.jeecg.modules.airag.wordtpl.dto;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@Data
public class WordTableCellDTO {
/**
* @Fields colspan : 合并列数
* @author chenrui
* @date 2024-09-26 09:37:27
*/
private int colspan;
/**
* @Fields rowspan : 合并行数
* @author chenrui
* @date 2024-09-26 09:38:22
*/
private int rowspan;
/**
* @Fields value : 单元格数据
* @author chenrui
* @date 2024-09-26 09:42:14
*/
private List<Object> value = new ArrayList<>();
/**
* @Fields verticalAlign : 垂直对齐方式默认top
* @author chenrui
* @date 2024-09-27 09:16:56
*/
private String verticalAlign = "top";
/**
* @Fields backgroundColor : 背景颜色
* @author chenrui
* @date 2024-11-18 09:56:28
*/
private String backgroundColor;
}

View File

@ -0,0 +1,24 @@
package org.jeecg.modules.airag.wordtpl.dto;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
@Data
public class WordTableDTO {
private String value = "";
private String type = "table";
private List<WordTableRowDTO> trList;
private int width;
private int height;
private List<JSONObject> colgroup = new ArrayList<>();
}

View File

@ -0,0 +1,30 @@
package org.jeecg.modules.airag.wordtpl.dto;
import java.util.List;
import lombok.Data;
@Data
public class WordTableRowDTO {
/**
* @Fields height : 行高
* @author chenrui
* @date 2024-09-26 09:45:30
*/
private Integer height;
/**
* @Fields minHeight : 行最小高度
* @author chenrui
* @date 2024-09-26 09:47:28
*/
private int minHeight = 42;
/**
* @Fields tdList : 行数据
* @author chenrui
* @date 2024-09-26 09:46:02
*/
private List<WordTableCellDTO> tdList;
}

View File

@ -0,0 +1,94 @@
package org.jeecg.modules.airag.wordtpl.dto;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* @author chenrui
* @ClassName: DocTextDto
* @Description: word文本实体类
* @date 2024-09-24 10:19:57
*/
@Data
public class WordTextDTO {
/**
* @Fields type : 类型
* @author chenrui
* @date 2024-09-29 08:53:27
*/
private String type;
/**
* @Fields value : 内容
* @author chenrui
* @date 2024-09-24 10:20:12
*/
private String value = "";
/**
* @Fields bold : 是否加粗 默认false
* @author chenrui
* @date 2024-09-24 10:20:33
*/
private boolean bold = false;
/**
* @Fields color : 字体颜色
* @author chenrui
* @date 2024-09-24 10:21:08
*/
private String color;
/**
* @Fields italic : 是否斜体 默认false
* @author chenrui
* @date 2024-09-24 10:21:25
*/
private boolean italic = false;
/**
* @Fields underline : 是否下划线 默认false
* @author chenrui
* @date 2024-09-24 10:21:47
*/
private boolean underline = false;
/**
* @Fields strikeout : 删除线 默认false
* @author chenrui
* @date 2024-09-24 10:22:06
*/
private boolean strikeout = false;
/**
* @Fields size : 字号大小
* @author chenrui
* @date 2024-09-24 10:44:42
*/
private int size;
/**
* @Fields font : 字体,默认微软雅黑
* @author chenrui
* @date 2024-09-24 10:45:31
*/
private String font = "微软雅黑";
/**
* @Fields highlight : 高亮颜色
* @author chenrui
* @date 2024-09-25 11:20:23
*/
private String highlight;
/**
* @Fields rowFlex : 水平对齐方式默认left
* @author chenrui
* @date 2024-09-27 09:12:18
*/
private String rowFlex = "left";
private List<Object> dashArray = new ArrayList<>();
}

View File

@ -0,0 +1,31 @@
package org.jeecg.modules.airag.wordtpl.dto;
import lombok.Data;
import java.util.Map;
/**
* word模版生成入参
* @author chenrui
* @date 2025/7/10 14:38
*/
@Data
public class WordTplGenDTO {
/**
* 模版id
*/
String templateId;
/**
* 模版code
*/
String templateCode;
/**
* 数据
*/
Map<String,Object> data;
}

View File

@ -0,0 +1,126 @@
package org.jeecg.modules.airag.wordtpl.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* @Description: word模版管理
* @Author: jeecg-boot
* @Date: 2025-07-04
* @Version: V1.0
*/
@Data
@TableName("aigc_word_template")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "word模版管理")
public class EoaWordTemplate implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
/**
* 创建人
*/
@Schema(description = "创建人")
private String createBy;
/**
* 创建日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private Date createTime;
/**
* 更新人
*/
@Schema(description = "更新人")
private String updateBy;
/**
* 更新日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private Date updateTime;
/**
* 所属部门
*/
@Schema(description = "所属部门")
private String sysOrgCode;
/**
* 模版名称
*/
@Excel(name = "模版名称", width = 15)
@Schema(description = "模版名称")
private String name;
/**
* 模版编码
*/
@Excel(name = "模版编码", width = 15)
@Schema(description = "模版编码")
private String code;
/**
* 页眉
*/
@Excel(name = "页眉", width = 15)
@Schema(description = "页眉")
private String header;
/**
* 页脚
*/
@Excel(name = "页脚", width = 15)
@Schema(description = "页脚")
private String footer;
/**
* 主体内容
*/
@Excel(name = "主体内容", width = 15)
@Schema(description = "主体内容")
private String main;
/**
* 页边距
*/
@Excel(name = "页边距", width = 15)
@Schema(description = "页边距")
private String margins;
/**
* 宽度
*/
@Excel(name = "宽度", width = 15)
@Schema(description = "宽度")
private Integer width;
/**
* 高度
*/
@Excel(name = "高度", width = 15)
@Schema(description = "高度")
private Integer height;
/**
* 纸张方向 vertical纵向 horizontal横向
*/
@Excel(name = "纸张方向 vertical纵向 horizontal横向", width = 15)
@Schema(description = "纸张方向 vertical纵向 horizontal横向")
private String paperDirection;
/**
* 水印
*/
@Excel(name = "水印", width = 15)
@Schema(description = "水印")
private String watermark;
}

View File

@ -0,0 +1,14 @@
package org.jeecg.modules.airag.wordtpl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
/**
* @Description: word模版管理
* @Author: jeecg-boot
* @Date: 2025-07-04
* @Version: V1.0
*/
public interface EoaWordTemplateMapper extends BaseMapper<EoaWordTemplate> {
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper">
</mapper>

View File

@ -0,0 +1,26 @@
package org.jeecg.modules.airag.wordtpl.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
import java.io.ByteArrayOutputStream;
/**
* @Description: word模版管理
* @Author: jeecg-boot
* @Date: 2025-07-04
* @Version: V1.0
*/
public interface IEoaWordTemplateService extends IService<EoaWordTemplate> {
/**
* 通过模版生成word文档
*
* @param wordTplGenDTO
* @return
* @author chenrui
* @date 2025/7/10 14:40
*/
void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream);
}

View File

@ -0,0 +1,85 @@
package org.jeecg.modules.airag.wordtpl.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
import org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper;
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.*;
import java.util.Map;
/**
* @Description: word模版管理
* @Author: jeecg-boot
* @Date: 2025-07-04
* @Version: V1.0
*/
@Slf4j
@Service("eoaWordTemplateService")
public class EoaWordTemplateServiceImpl extends ServiceImpl<EoaWordTemplateMapper, EoaWordTemplate> implements IEoaWordTemplateService {
/**
* 内置的系统变量键列表
*/
private static final String[] SYSTEM_KEYS = {
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
DataBaseConstant.SYS_BASE_PATH
};
@Autowired
WordTplUtils wordTplUtils;
@Override
public void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream) {
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
AssertUtils.assertNotEmpty("模版ID不能为空", wordTplGenDTO.getTemplateId());
String templateId = wordTplGenDTO.getTemplateId();
// 生成word模版 date:2025/7/10
EoaWordTemplate template = getById(templateId);
ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
//根据word模版和数据生成word文件
Map<String, Object> data = wordTplGenDTO.getData();
mergeSystemVarsToData(data);
try {
XWPFTemplate.compile(new ByteArrayInputStream(wordTemplateOut.toByteArray())).render(data).write(wordOutputStream);
}catch (Exception e){
log.error(e.getMessage(), e);
throw new JeecgBootException("生成word文档失败请检查模版和数据是否正确");
}
}
/**
* 将系统变量合并到数据中
*
* @param data
* @author chenrui
* @date 2025/7/3 17:43
*/
private static void mergeSystemVarsToData(Map<String, Object> data) {
for (String key : SYSTEM_KEYS) {
if (!data.containsKey(key)) {
String value = JwtUtil.getUserSystemData(key, null);
if (value != null) {
data.put(key, value);
}
}
}
}
}

View File

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

View File

@ -0,0 +1,326 @@
package org.jeecg.modules.demo.mcp;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.config.shiro.IgnoreAuth;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* MCP Server 示例 (Model Context Protocol)
*
* 这是一个符合 MCP 协议的服务端实现,支持 SSE 传输。
*
* 连接地址: http://你的服务器:8080/jeecg-boot/demo/mcp/sse
*
* 提供的工具:
* - hello: 打招呼工具
* - get_time: 获取当前时间
* - calculate: 简单计算器
*/
@Slf4j
@RestController
@RequestMapping("/demo/mcp")
@Tag(name = "MCP Server 示例")
public class McpDemoController {
// 存储 SSE 连接
private final Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
// 定义工具列表
private final List<Map<String, Object>> TOOLS = List.of(
Map.of(
"name", "hello",
"description", "打招呼工具,返回问候语",
"inputSchema", Map.of(
"type", "object",
"properties", Map.of(
"name", Map.of("type", "string", "description", "你的名字")
),
"required", List.of("name")
)
),
Map.of(
"name", "get_time",
"description", "获取当前服务器时间",
"inputSchema", Map.of(
"type", "object",
"properties", Map.of()
)
),
Map.of(
"name", "calculate",
"description", "简单计算器,支持加减乘除",
"inputSchema", Map.of(
"type", "object",
"properties", Map.of(
"a", Map.of("type", "number", "description", "第一个数"),
"b", Map.of("type", "number", "description", "第二个数"),
"operator", Map.of("type", "string", "description", "运算符: +, -, *, /")
),
"required", List.of("a", "b", "operator")
)
)
);
/**
* MCP SSE 端点 - 客户端通过此接口建立 SSE 连接
*/
@IgnoreAuth
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "MCP SSE 连接端点")
public SseEmitter sse(HttpServletRequest request) {
String clientId = UUID.randomUUID().toString();
log.info("[MCP Server] 新客户端 SSE 连接: {}", clientId);
SseEmitter emitter = new SseEmitter(0L); // 不超时
sseEmitters.put(clientId, emitter);
emitter.onCompletion(() -> {
log.info("[MCP Server] 客户端断开: {}", clientId);
sseEmitters.remove(clientId);
});
emitter.onTimeout(() -> {
log.info("[MCP Server] 客户端超时: {}", clientId);
sseEmitters.remove(clientId);
});
emitter.onError(e -> {
log.error("[MCP Server] SSE 错误: {}", e.getMessage());
sseEmitters.remove(clientId);
});
// 发送 endpoint 事件,告诉客户端消息端点地址
try {
String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
String messageEndpoint = baseUrl + request.getContextPath() + "/demo/mcp/message?sessionId=" + clientId;
emitter.send(SseEmitter.event()
.name("endpoint")
.data(messageEndpoint));
log.info("[MCP Server] 发送 endpoint 事件: {}", messageEndpoint);
} catch (IOException e) {
log.error("[MCP Server] 发送 endpoint 事件失败", e);
}
return emitter;
}
/**
* Streamable HTTP 端点 - 同时支持 POST 到 /sse 的 JSON-RPC 请求
* Cursor 客户端会先尝试这种方式
*/
@IgnoreAuth
@PostMapping(value = "/sse")
@Operation(summary = "MCP Streamable HTTP 端点")
public void ssePost(@RequestBody String body, HttpServletResponse response) throws IOException {
log.info("[MCP Server] Streamable HTTP 请求: {}", body);
handleJsonRpcRequest(body, response);
}
/**
* MCP 消息处理端点 - 处理 JSON-RPC 请求
* 直接写入原始 JSON-RPC 响应,避免框架包装
*/
@IgnoreAuth
@PostMapping(value = "/message")
@Operation(summary = "MCP 消息处理")
public void handleMessage(@RequestParam(required = false) String sessionId,
@RequestBody String body,
HttpServletResponse response) throws IOException {
log.info("[MCP Server] 收到消息, sessionId: {}, body: {}", sessionId, body);
handleJsonRpcRequest(body, response);
}
/**
* 处理 JSON-RPC 请求的公共方法
*/
private void handleJsonRpcRequest(String body, HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
try {
JSONObject request = JSON.parseObject(body);
String method = request.getString("method");
Object id = request.get("id");
JSONObject params = request.getJSONObject("params");
// 通知类消息没有id不需要响应
if (id == null) {
log.info("[MCP Server] 收到通知: {}", method);
writer.write("{}");
writer.flush();
return;
}
// 构建 JSON-RPC 2.0 响应
Map<String, Object> jsonRpcResponse = new LinkedHashMap<>();
jsonRpcResponse.put("jsonrpc", "2.0");
jsonRpcResponse.put("id", id);
try {
Object result = switch (method) {
case "initialize" -> handleInitialize(params);
case "initialized", "notifications/initialized" -> handleInitialized();
case "tools/list" -> handleToolsList();
case "tools/call" -> handleToolsCall(params);
case "ping" -> handlePing();
case "notifications/cancelled" -> handleCancelled(params);
default -> {
if (method != null && method.startsWith("notifications/")) {
log.info("[MCP Server] 忽略未知通知: {}", method);
yield Map.of();
}
throw new RuntimeException("未知方法: " + method);
}
};
jsonRpcResponse.put("result", result);
} catch (Exception e) {
log.error("[MCP Server] 处理请求失败", e);
jsonRpcResponse.put("error", Map.of(
"code", -32603,
"message", e.getMessage()
));
}
String responseJson = JSON.toJSONString(jsonRpcResponse);
log.info("[MCP Server] 返回: {}", responseJson);
writer.write(responseJson);
} catch (Exception e) {
log.error("[MCP Server] 解析请求失败", e);
writer.write("{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32700,\"message\":\"Parse error\"}}");
}
writer.flush();
}
/**
* 处理 initialize 请求
*/
private Map<String, Object> handleInitialize(JSONObject params) {
log.info("[MCP Server] 初始化请求: {}", params);
return Map.of(
"protocolVersion", "2024-11-05",
"capabilities", Map.of(
"tools", Map.of()
),
"serverInfo", Map.of(
"name", "jeecg-mcp-demo",
"version", "1.0.0"
)
);
}
/**
* 处理 initialized 通知
*/
private Map<String, Object> handleInitialized() {
log.info("[MCP Server] 客户端已初始化完成");
return Map.of();
}
/**
* 处理 ping 请求
*/
private Map<String, Object> handlePing() {
log.info("[MCP Server] Ping");
return Map.of();
}
/**
* 处理 notifications/cancelled 通知
*/
private Map<String, Object> handleCancelled(JSONObject params) {
log.info("[MCP Server] 请求被取消: {}", params);
return Map.of();
}
/**
* 处理 tools/list 请求
*/
private Map<String, Object> handleToolsList() {
log.info("[MCP Server] 获取工具列表");
return Map.of("tools", TOOLS);
}
/**
* 处理 tools/call 请求
*/
private Map<String, Object> handleToolsCall(JSONObject params) {
String toolName = params.getString("name");
JSONObject arguments = params.getJSONObject("arguments");
if (arguments == null) {
arguments = new JSONObject();
}
log.info("[MCP Server] 调用工具: {}, 参数: {}", toolName, arguments);
String result = switch (toolName) {
case "hello" -> {
String name = arguments.getString("name");
if (name == null || name.isEmpty()) {
name = "World";
}
yield "你好, " + name + "! 欢迎使用 JeecgBoot MCP 服务!";
}
case "get_time" -> {
yield "当前时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
case "calculate" -> {
double a = arguments.getDoubleValue("a");
double b = arguments.getDoubleValue("b");
String op = arguments.getString("operator");
if (op == null) op = "+";
double res = switch (op) {
case "+" -> a + b;
case "-" -> a - b;
case "*" -> a * b;
case "/" -> b != 0 ? a / b : Double.NaN;
default -> throw new RuntimeException("不支持的运算符: " + op);
};
yield String.format("%.2f %s %.2f = %.2f", a, op, b, res);
}
default -> throw new RuntimeException("未知工具: " + toolName);
};
return Map.of(
"content", List.of(Map.of(
"type", "text",
"text", result
))
);
}
/**
* 使用说明页面
*/
@IgnoreAuth
@GetMapping("/info")
@Operation(summary = "MCP Server 使用说明")
public Map<String, Object> info(HttpServletRequest request) {
log.info("[MCP Server] Hello 接口被访问");
String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
return Map.of(
"success", true,
"message", "JeecgBoot MCP Server 示例",
"sseUrl", baseUrl + "/demo/mcp/sse",
"tools", List.of(
Map.of("name", "hello", "description", "打招呼工具", "params", "name: 你的名字"),
Map.of("name", "get_time", "description", "获取当前时间", "params", ""),
Map.of("name", "calculate", "description", "简单计算器", "params", "a, b, operator(+,-,*,/)")
),
"usage", "在 Cursor/Claude 等 MCP 客户端中配置 SSE URL: " + baseUrl + "/demo/mcp/sse",
"example", "请调用 hello 工具,参数 name 填 \"测试用户\""
);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,39 @@
package org.jeecg.common.airag.api;
import org.jeecg.common.airag.api.fallback.AiragBaseApiFallback;
import org.jeecg.common.constant.ServiceNameConstants;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* airag baseAPI
*
* @author sjlei
* @date 2025-12-30
*/
@Component
@FeignClient(contextId = "airagBaseRemoteApi", value = ServiceNameConstants.SERVICE_SYSTEM, fallbackFactory = AiragBaseApiFallback.class)
@ConditionalOnMissingClass("org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl")
public interface IAiragBaseApi {
/**
* 知识库写入文本文档
*
* @param knowledgeId 知识库ID
* @param title 文档标题
* @param content 文档内容
* @return 新增的文档ID
* @author sjlei
* @date 2025-12-30
*/
@PostMapping("/airag/api/knowledgeWriteTextDocument")
String knowledgeWriteTextDocument(
@RequestParam("knowledgeId") String knowledgeId,
@RequestParam("title") String title,
@RequestParam("content") String content
);
}

View File

@ -0,0 +1,18 @@
package org.jeecg.common.airag.api.factory;
import org.jeecg.common.airag.api.IAiragBaseApi;
import org.jeecg.common.airag.api.fallback.AiragBaseApiFallback;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class AiragBaseApiFallbackFactory implements FallbackFactory<IAiragBaseApi> {
@Override
public IAiragBaseApi create(Throwable cause) {
AiragBaseApiFallback fallback = new AiragBaseApiFallback();
fallback.setCause(cause);
return fallback;
}
}

View File

@ -0,0 +1,16 @@
package org.jeecg.common.airag.api.fallback;
import lombok.Setter;
import org.jeecg.common.airag.api.IAiragBaseApi;
public class AiragBaseApiFallback implements IAiragBaseApi {
@Setter
private Throwable cause;
@Override
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
return null;
}
}

View File

@ -18,6 +18,7 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
import java.util.Map;
@ -872,6 +873,18 @@ public interface ISysBaseAPI extends CommonAPI {
@PostMapping(value = "/sys/api/runAiragFlow")
Object runAiragFlow(@RequestBody AiragFlowDTO airagFlowDTO);
/**
* 流式运行AIRag流程
* for [QQYUN-13634]在baseapi里面封装方法方便其他模块调用
*
* @param airagFlowDTO
* @return 流程执行结果,可能是String或者Map
* @author chenrui
* @date 2025/9/2 11:43
*/
@PostMapping(value = "/sys/api/runAiragFlowStream")
SseEmitter runAiragFlowStream(@RequestBody AiragFlowDTO airagFlowDTO);
/**
* 根据部门code或部门id获取部门名称(当前和上级部门)
*

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