diff --git a/pom.xml b/pom.xml
index 6a291539c..b06ce5ba2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,7 @@
2.2.7
4.5.0
2.0.0
- 0.0.4
+ 0.0.5
1.5.0
0.2.0
1.18.42
@@ -55,8 +55,7 @@
2.6.5
3.0.2
7.17.28
-
- 2.0.0-M8
+ 2.0.0
3.5.0
@@ -310,10 +309,17 @@
${snailjob.version}
-
+
com.aizuda
- snail-ai-agent-starter
+ snail-ai-agent-chat-starter
+ ${snailai.version}
+
+
+
+
+ com.aizuda
+ snail-ai-agent-executor-starter
${snailai.version}
diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml
index 18045380d..bdd5c3f86 100644
--- a/ruoyi-admin/src/main/resources/application-dev.yml
+++ b/ruoyi-admin/src/main/resources/application-dev.yml
@@ -32,8 +32,6 @@ snail-job:
snail-ai:
# 启用客户端模式
enabled: false
- # 聊天发送模式: stream(流式) / sync(同步)
- chat-mode: stream
# ==================== Server 连接 ====================
# Server 端 gRPC 地址(即 snail-ai-starter 的 snail-ai.server.grpc-port)
server:
@@ -44,7 +42,7 @@ snail-ai:
# 应用 ID(在 Server「应用管理」页面创建后获取)
app-id: 1
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
- token: SAI_7c557fc05a304b0482a65ca67afdb0b8
+ token: SAI_566a6bfbc26e4998b4841cc927d50c5d
port: 18889
# Skill 文件临时目录
skill-temp-dir: /tmp/snail-ai-agent/skills
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index 18d036f7e..cbecae3e4 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -35,8 +35,6 @@ snail-job:
snail-ai:
# 启用客户端模式
enabled: false
- # 聊天发送模式: stream(流式) / sync(同步)
- chat-mode: stream
# ==================== Server 连接 ====================
# Server 端 gRPC 地址(即 snail-ai-starter 的 snail-ai.server.grpc-port)
server:
@@ -47,7 +45,7 @@ snail-ai:
# 应用 ID(在 Server「应用管理」页面创建后获取)
app-id: 1
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
- token: SAI_7c557fc05a304b0482a65ca67afdb0b8
+ token: SAI_566a6bfbc26e4998b4841cc927d50c5d
port: 18889
# Skill 文件临时目录
skill-temp-dir: /tmp/snail-ai-agent/skills
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index 01963ea80..5ad4c6820 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -109,6 +109,8 @@ security:
- /*/api-docs
- /*/api-docs/**
- /warm-flow-ui/config
+ - /snail-chat/**
+ - /api/snail/chat/**
# MyBatisPlus配置
# https://baomidou.com/config/
diff --git a/ruoyi-common/ruoyi-common-ai/pom.xml b/ruoyi-common/ruoyi-common-ai/pom.xml
index 0d3b4852b..6a1b4e037 100644
--- a/ruoyi-common/ruoyi-common-ai/pom.xml
+++ b/ruoyi-common/ruoyi-common-ai/pom.xml
@@ -23,10 +23,16 @@
ruoyi-common-core
-
+
com.aizuda
- snail-ai-agent-starter
+ snail-ai-agent-chat-starter
+
+
+
+
+ com.aizuda
+ snail-ai-agent-executor-starter
diff --git a/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/handler/SnailAiChatExceptionHandler.java b/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/handler/SnailAiChatExceptionHandler.java
new file mode 100644
index 000000000..1d93eda83
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/handler/SnailAiChatExceptionHandler.java
@@ -0,0 +1,124 @@
+package org.dromara.common.ai.handler;
+
+import com.aizuda.snail.ai.agent.chat.starter.SnailAiChatGatewayController;
+import com.aizuda.snail.ai.common.execption.BaseSnailAiException;
+import com.aizuda.snail.ai.common.execption.SnailAiAuthenticationException;
+import com.aizuda.snail.ai.common.model.ModelCallException;
+import com.aizuda.snail.ai.common.model.Result;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.MessageSourceResolvable;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.HandlerMethodValidationException;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+/**
+ * Snail AI Chat 网关异常处理器。
+ *
+ * /api/snail/chat/** 接口由 Snail AI SDK 前端消费,响应结构需要保持 SDK Result(status/message/data),
+ * 不能落到 RuoYi 通用 R(code/msg/data) 格式。
+ */
+@Slf4j
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@RestControllerAdvice(assignableTypes = SnailAiChatGatewayController.class)
+public class SnailAiChatExceptionHandler {
+
+ private static final int AUTHENTICATION_ERROR_STATUS = 5001;
+
+ @ExceptionHandler(SnailAiAuthenticationException.class)
+ public Result handleAuthenticationException(SnailAiAuthenticationException e) {
+ log.warn("Snail AI Chat authentication failed: {}", e.getMessage());
+ return Result.fail(AUTHENTICATION_ERROR_STATUS, defaultMessage(e, "认证失败,请重新登录"));
+ }
+
+ @ExceptionHandler(BaseSnailAiException.class)
+ public Result handleSnailAiException(BaseSnailAiException e) {
+ log.warn("Snail AI Chat request failed: {}", e.getMessage());
+ return Result.fail(defaultMessage(e, "AI 服务请求失败,请稍后再试"));
+ }
+
+ @ExceptionHandler(ModelCallException.class)
+ public Result handleModelCallException(ModelCallException e) {
+ log.warn("Snail AI Chat model call failed: {}", e.getMessage(), e);
+ return Result.fail(defaultMessage(e, "模型调用失败,请稍后再试"));
+ }
+
+ @ExceptionHandler({
+ BindException.class,
+ MethodArgumentNotValidException.class,
+ HandlerMethodValidationException.class
+ })
+ public Result handleValidationException(Exception e) {
+ log.warn("Snail AI Chat validation failed: {}", e.getMessage());
+ return Result.fail(validationMessage(e));
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ public Result handleConstraintViolationException(ConstraintViolationException e) {
+ log.warn("Snail AI Chat constraint validation failed: {}", e.getMessage());
+ String message = e.getConstraintViolations().stream()
+ .map(ConstraintViolation::getMessage)
+ .filter(this::hasText)
+ .collect(Collectors.joining(", "));
+ return Result.fail(hasText(message) ? message : "请求参数校验失败");
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
+ log.warn("Snail AI Chat request body parse failed: {}", e.getMessage());
+ return Result.fail("请求参数格式错误");
+ }
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public Result handleIllegalArgumentException(IllegalArgumentException e) {
+ log.warn("Snail AI Chat illegal argument: {}", e.getMessage());
+ return Result.fail(defaultMessage(e, "请求参数不合法"));
+ }
+
+ @ExceptionHandler(IllegalStateException.class)
+ public Result handleIllegalStateException(IllegalStateException e) {
+ log.warn("Snail AI Chat illegal state: {}", e.getMessage());
+ return Result.fail(defaultMessage(e, "AI 会话状态异常,请刷新后重试"));
+ }
+
+ @ExceptionHandler(Exception.class)
+ public Result handleException(Exception e) {
+ log.error("Snail AI Chat unexpected exception", e);
+ return Result.fail("AI 服务异常,请稍后再试");
+ }
+
+ private String validationMessage(Exception e) {
+ Collection extends MessageSourceResolvable> errors;
+ if (e instanceof BindException bindException) {
+ errors = bindException.getAllErrors();
+ } else if (e instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
+ errors = methodArgumentNotValidException.getBindingResult().getAllErrors();
+ } else if (e instanceof HandlerMethodValidationException handlerMethodValidationException) {
+ errors = handlerMethodValidationException.getAllErrors();
+ } else {
+ return "请求参数校验失败";
+ }
+ String message = errors.stream()
+ .map(MessageSourceResolvable::getDefaultMessage)
+ .filter(this::hasText)
+ .collect(Collectors.joining(", "));
+ return hasText(message) ? message : "请求参数校验失败";
+ }
+
+ private String defaultMessage(Throwable e, String fallback) {
+ return hasText(e.getMessage()) ? e.getMessage() : fallback;
+ }
+
+ private boolean hasText(String value) {
+ return value != null && !value.isBlank();
+ }
+}
diff --git a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java
index d61f77b64..0c25bab94 100644
--- a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java
+++ b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java
@@ -2,33 +2,19 @@ package org.dromara.ai.controller;
import com.aizuda.snail.ai.common.execption.SnailAiException;
import com.aizuda.snail.ai.common.model.Result;
-import com.aizuda.snail.ai.common.openapi.dto.*;
-import com.aizuda.snail.ai.openapi.client.core.api.OpenApiAgentClient;
-import com.aizuda.snail.ai.openapi.client.core.api.OpenApiChatClient;
-import com.aizuda.snail.ai.openapi.client.core.api.OpenApiConversationClient;
+import com.aizuda.snail.ai.common.openapi.dto.OpenApiUserRegisterRequest;
+import com.aizuda.snail.ai.common.openapi.dto.OpenApiUserVO;
import com.aizuda.snail.ai.openapi.client.core.api.OpenApiUserClient;
-import com.aizuda.snail.ai.openapi.client.core.listener.SseEventListener;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.constraints.Min;
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.api.model.LoginUser;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
/**
* Snail AI OpenAPI 控制器
@@ -47,20 +33,7 @@ public class SnailAiController extends BaseController {
* Snail AI 成功状态码。
*/
private static final int SNAIL_AI_SUCCESS = 1;
- /**
- * SSE 超时时间。
- */
- private static final long SSE_TIMEOUT = 300000L;
-
- private final OpenApiAgentClient agentClient;
- private final OpenApiChatClient chatClient;
- private final OpenApiConversationClient conversationClient;
private final OpenApiUserClient userClient;
- /**
- * 聊天模式。
- */
- @Value("${snail-ai.chat-mode:stream}")
- private String chatMode;
/**
* 注册当前登录用户并返回 OpenAPI 用户信息。
@@ -70,272 +43,6 @@ public class SnailAiController extends BaseController {
return R.ok(ensureOpenApiUser());
}
- /**
- * 查询当前登录用户对应的 OpenAPI 用户信息。
- */
- @GetMapping("/user")
- public R getUser() {
- OpenApiUserQueryRequest request = new OpenApiUserQueryRequest();
- request.setOpenId(ensureOpenId());
- return toR(userClient.getUser(request));
- }
-
- /**
- * 查询当前用户可访问的智能体列表。
- */
- @GetMapping("/agents")
- public R> listAgents() {
- return toR(agentClient.listAgents());
- }
-
- /**
- * 根据智能体 ID 查询智能体详情。
- */
- @GetMapping("/agent/{agentId}")
- public R getAgent(@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId) {
- OpenApiAgentIdentityRequest request = new OpenApiAgentIdentityRequest();
- request.setAgentId(agentId);
- return toR(agentClient.getAgent(request));
- }
-
- /**
- * 为指定智能体创建新会话。
- */
- @PostMapping("/agent/{agentId}/conversation")
- public R createConversation(
- @NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
- @RequestBody OpenApiCreateConversationRequest request) {
- request.setAgentId(agentId);
- request.setOpenId(ensureOpenId());
- return toR(conversationClient.createConversation(request));
- }
-
- /**
- * 分页查询指定智能体下的会话列表。
- */
- @GetMapping("/agent/{agentId}/conversations")
- public R> listConversations(
- @NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
- @Min(value = 1, message = "页码不能小于1") @RequestParam(defaultValue = "1") int page,
- @Min(value = 1, message = "每页条数不能小于1") @RequestParam(defaultValue = "10") int size) {
- OpenApiConversationQueryRequest request = new OpenApiConversationQueryRequest();
- request.setAgentId(agentId);
- request.setOpenId(ensureOpenId());
- request.setPage(page);
- request.setSize(size);
- return toPageR(conversationClient.listConversations(request));
- }
-
- /**
- * 查询指定会话的消息历史。
- */
- @GetMapping("/agent/{agentId}/conversation/{conversationId}/messages")
- public R> getMessages(
- @NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
- @NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
- OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest();
- request.setAgentId(agentId);
- request.setConversationId(conversationId);
- request.setOpenId(ensureOpenId());
- return toR(conversationClient.getMessages(request));
- }
-
- /**
- * 删除指定会话。
- */
- @DeleteMapping("/agent/{agentId}/conversation/{conversationId}")
- public R deleteConversation(
- @NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
- @NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
- OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest();
- request.setAgentId(agentId);
- request.setConversationId(conversationId);
- request.setOpenId(ensureOpenId());
- return toR(conversationClient.deleteConversation(request));
- }
-
- /**
- * 获取当前聊天发送模式。
- */
- @GetMapping("/chat/mode")
- public R