mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-16 13:23:42 +00:00
feat: 发布3.0版本,新增文档处理能力和演示模式
- 升级langchain4j版本至1.13.0 - 新增docx/pdf/xlsx文档处理技能模块 - 添加演示模式配置和切面拦截 - 优化聊天服务和可观测性监听器 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package org.ruoyi.common.chat.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Agent 对话请求对象(简化版)
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/04/10
|
||||
*/
|
||||
@Data
|
||||
public class AgentChatRequest {
|
||||
|
||||
/**
|
||||
* 对话消息
|
||||
*/
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 会话id(可选,不传则不保存历史)
|
||||
*/
|
||||
private Long sessionId;
|
||||
|
||||
}
|
||||
@@ -3,8 +3,13 @@ package org.ruoyi.common.chat.domain.dto.request;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
@@ -62,7 +67,6 @@ public class ChatRequest {
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
|
||||
/**
|
||||
* 对话id(每个聊天窗口都不一样)
|
||||
*/
|
||||
@@ -76,8 +80,28 @@ public class ChatRequest {
|
||||
private Boolean enableThinking = false;
|
||||
|
||||
/**
|
||||
* 是否支持联网
|
||||
* 对话模型详情
|
||||
*/
|
||||
private Boolean enableInternet;
|
||||
private ChatModelVo chatModelVo;
|
||||
|
||||
/**
|
||||
* 对话事件
|
||||
*/
|
||||
private SseEmitter emitter;
|
||||
|
||||
/**
|
||||
* 当前登录用户id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 当前登录用户TOKEN
|
||||
*/
|
||||
private String tokenValue;
|
||||
|
||||
/**
|
||||
* 完整的上下文
|
||||
*/
|
||||
private List<ChatMessage> contextMessages;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.ruoyi.common.security.config;
|
||||
|
||||
import cn.dev33.satoken.context.SaTokenContextForThreadLocalStaff;
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.filter.SaServletFilter;
|
||||
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
|
||||
@@ -49,6 +50,11 @@ public class SecurityConfig implements WebMvcConfigurer {
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册路由拦截器,自定义验证规则
|
||||
registry.addInterceptor(new SaInterceptor(handler -> {
|
||||
// 异步线程中 SaToken 上下文不存在,跳过检查
|
||||
// 这避免了 SSE 流式响应完成后 emitter.complete() 触发的问题
|
||||
if (SaTokenContextForThreadLocalStaff.getModelBoxOrNull() == null) {
|
||||
return;
|
||||
}
|
||||
AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
|
||||
// 登录验证 -- 排除多个路径
|
||||
SaRouter
|
||||
|
||||
@@ -202,12 +202,14 @@ public class SseEmitterManager {
|
||||
public void sendEvent(Long userId, SseEventDto eventDto) {
|
||||
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
|
||||
if (MapUtil.isNotEmpty(emitters)) {
|
||||
log.debug("【SSE发送】userId: {}, emitter数量: {}, event: {}", userId, emitters.size(), eventDto.getEvent());
|
||||
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
|
||||
try {
|
||||
entry.getValue().send(SseEmitter.event()
|
||||
.name(eventDto.getEvent())
|
||||
.data(JSONUtil.toJsonStr(eventDto)));
|
||||
} catch (Exception e) {
|
||||
log.error("【SSE发送失败】userId: {}, token: {}, error: {}", userId, entry.getKey(), e.getMessage());
|
||||
SseEmitter remove = emitters.remove(entry.getKey());
|
||||
if (remove != null) {
|
||||
remove.complete();
|
||||
@@ -215,6 +217,7 @@ public class SseEmitterManager {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("【SSE发送失败】userId: {} 没有活跃的SSE连接, 当前连接用户: {}", userId, USER_TOKEN_EMITTERS.keySet());
|
||||
USER_TOKEN_EMITTERS.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,21 @@ public class SseEventDto implements Serializable {
|
||||
.error(error)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MCP 工具事件
|
||||
*/
|
||||
public static SseEventDto mcpTool(String toolName, String status, String result) {
|
||||
return SseEventDto.builder()
|
||||
.event("mcp_tool")
|
||||
.content(buildMcpJson(toolName, status, result))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String buildMcpJson(String toolName, String status, String result) {
|
||||
return String.format("{\"toolName\":\"%s\",\"status\":\"%s\",\"result\":\"%s\"}",
|
||||
toolName != null ? toolName.replace("\"", "\\\"") : "",
|
||||
status != null ? status : "",
|
||||
result != null ? result.replace("\"", "\\\"").replace("\n", "\\n") : "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.ruoyi.common.web.aspectj;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.utils.ServletUtils;
|
||||
import org.ruoyi.common.web.config.properties.DemoProperties;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 演示模式切面 - 拦截写操作
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@RequiredArgsConstructor
|
||||
public class DemoAspect {
|
||||
|
||||
private final DemoProperties demoProperties;
|
||||
|
||||
/**
|
||||
* 需要拦截的 HTTP 方法
|
||||
*/
|
||||
private static final Set<String> WRITE_METHODS = Set.of(
|
||||
"POST", "PUT", "DELETE", "PATCH"
|
||||
);
|
||||
|
||||
/**
|
||||
* 拦截所有 Controller 的写操作
|
||||
*/
|
||||
@Around("execution(* org.ruoyi..controller..*.*(..))")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
// 未开启演示模式,直接放行
|
||||
if (!Boolean.TRUE.equals(demoProperties.getEnabled())) {
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
HttpServletRequest request = ServletUtils.getRequest();
|
||||
if (request == null) {
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
String method = request.getMethod();
|
||||
String requestUri = request.getRequestURI();
|
||||
|
||||
// 非写操作,放行
|
||||
if (!WRITE_METHODS.contains(method.toUpperCase())) {
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
// 检查排除路径
|
||||
for (String exclude : demoProperties.getExcludes()) {
|
||||
if (match(exclude, requestUri)) {
|
||||
return point.proceed();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("演示模式拦截写操作: {} {}", method, requestUri);
|
||||
throw new ServiceException(demoProperties.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径匹配(支持通配符 * 和 **)
|
||||
*/
|
||||
private boolean match(String pattern, String path) {
|
||||
if (pattern.endsWith("/**")) {
|
||||
String prefix = pattern.substring(0, pattern.length() - 3);
|
||||
return path.startsWith(prefix);
|
||||
}
|
||||
if (pattern.endsWith("/*")) {
|
||||
String prefix = pattern.substring(0, pattern.length() - 2);
|
||||
if (!path.startsWith(prefix)) {
|
||||
return false;
|
||||
}
|
||||
String suffix = path.substring(prefix.length());
|
||||
return suffix.indexOf('/') == -1;
|
||||
}
|
||||
return path.equals(pattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.ruoyi.common.web.config;
|
||||
|
||||
import org.ruoyi.common.web.aspectj.DemoAspect;
|
||||
import org.ruoyi.common.web.config.properties.DemoProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 演示模式自动配置
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(DemoProperties.class)
|
||||
public class DemoAutoConfiguration {
|
||||
|
||||
/**
|
||||
* 注册演示模式切面
|
||||
*/
|
||||
@Bean
|
||||
public DemoAspect demoAspect(DemoProperties demoProperties) {
|
||||
return new DemoAspect(demoProperties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.ruoyi.common.web.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 演示模式 配置属性
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "demo")
|
||||
public class DemoProperties {
|
||||
|
||||
/**
|
||||
* 是否开启演示模式
|
||||
*/
|
||||
private Boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 提示消息
|
||||
*/
|
||||
private String message = "演示模式,不允许进行写操作";
|
||||
|
||||
/**
|
||||
* 排除的路径(这些路径不受演示模式限制)
|
||||
*/
|
||||
private List<String> excludes = new ArrayList<>();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
org.ruoyi.common.web.config.CaptchaConfig
|
||||
org.ruoyi.common.web.config.DemoAutoConfiguration
|
||||
org.ruoyi.common.web.config.FilterConfig
|
||||
org.ruoyi.common.web.config.I18nConfig
|
||||
org.ruoyi.common.web.config.ResourcesConfig
|
||||
|
||||
Reference in New Issue
Block a user