v3.9.1 后台代码

This commit is contained in:
JEECG
2026-01-21 19:02:45 +08:00
parent 3735ca1687
commit e2402c75b0
113 changed files with 9495 additions and 109830 deletions

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

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