From af21d69e65bda2c7cd66863eaaf5289e90faa8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Tue, 26 May 2026 10:39:07 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=20!pr850=20?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81=E7=94=A8=E6=B3=95=E4=B8=8E?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 4 +- .../src/main/resources/application-dev.yml | 6 +- .../src/main/resources/application-prod.yml | 6 +- .../src/main/resources/application.yml | 1 - ruoyi-common/ruoyi-common-ai/pom.xml | 11 +- ruoyi-common/ruoyi-common-bom/pom.xml | 3 +- ruoyi-modules/ruoyi-ai/pom.xml | 12 + .../ai/controller/OpenApiDemoController.java | 263 -------------- .../ai/controller/SnailAiController.java | 338 ++++++++++++++++++ .../java/org/dromara/ai/package-info.java | 1 - script/sql/oracle/oracle_ry_vue.sql | 1 + script/sql/postgres/postgres_ry_vue.sql | 1 + script/sql/ry_ai.sql | 1 - script/sql/ry_vue.sql | 1 + script/sql/sqlserver/sqlserver_ry_vue.sql | 2 + 15 files changed, 368 insertions(+), 283 deletions(-) delete mode 100644 ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/OpenApiDemoController.java create mode 100644 ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java delete mode 100644 ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/package-info.java delete mode 100644 script/sql/ry_ai.sql diff --git a/pom.xml b/pom.xml index a8666c382..2f2e657aa 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ 2.2.7 4.5.0 2.0.0 - 0.0.1 + 0.0.2 1.5.0 0.2.0 1.18.42 @@ -56,7 +56,7 @@ 3.0.2 7.17.28 - 2.0.0-M6 + 2.0.0-M7 3.5.0 diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index c808767fa..fdc4e95f7 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -1,7 +1,7 @@ --- # 监控中心配置 spring.boot.admin.client: # 增加客户端开关 - enabled: true + enabled: false url: http://localhost:9090/admin instance: service-host-type: IP @@ -13,7 +13,7 @@ spring.boot.admin.client: --- # snail-job 配置 snail-job: - enabled: true + enabled: false # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务 group: "ruoyi_group" # SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表 @@ -31,7 +31,7 @@ snail-job: --- # snail-ai 配置 snail-ai: # 启用客户端模式 - enabled: true + enabled: false # 聊天发送模式: stream(流式) / sync(同步) chat-mode: stream # ==================== Server 连接 ==================== diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 76274c81d..18383c1b9 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -4,7 +4,7 @@ spring.servlet.multipart.location: /ruoyi/server/temp --- # 监控中心配置 spring.boot.admin.client: # 增加客户端开关 - enabled: true + enabled: false url: http://localhost:9090/admin instance: service-host-type: IP @@ -16,7 +16,7 @@ spring.boot.admin.client: --- # snail-job 配置 snail-job: - enabled: true + enabled: false # 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务 group: "ruoyi_group" # SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config`表 @@ -34,7 +34,7 @@ snail-job: --- # snail-ai 配置 snail-ai: # 启用客户端模式 - enabled: true + enabled: false # 聊天发送模式: stream(流式) / sync(同步) chat-mode: stream # ==================== Server 连接 ==================== diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 081030690..8c9f26c19 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -109,7 +109,6 @@ security: - /*/api-docs - /*/api-docs/** - /warm-flow-ui/config - - /snail-ai/agent/*/chat/stream # MyBatisPlus配置 # https://baomidou.com/config/ diff --git a/ruoyi-common/ruoyi-common-ai/pom.xml b/ruoyi-common/ruoyi-common-ai/pom.xml index f1eb0712b..34f028163 100644 --- a/ruoyi-common/ruoyi-common-ai/pom.xml +++ b/ruoyi-common/ruoyi-common-ai/pom.xml @@ -3,8 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - ruoyi-common org.dromara + ruoyi-common ${revision} 4.0.0 @@ -18,8 +18,8 @@ - org.springframework.boot - spring-boot-autoconfigure + org.dromara + ruoyi-common-core @@ -32,10 +32,5 @@ snail-ai-openapi-starter - - org.dromara - ruoyi-common-core - - diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index 710cc4bc3..5f222e4f5 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -170,8 +170,9 @@ org.dromara ruoyi-common-ai + ${revision} - + org.dromara diff --git a/ruoyi-modules/ruoyi-ai/pom.xml b/ruoyi-modules/ruoyi-ai/pom.xml index 1baf5cb8a..e44919b00 100644 --- a/ruoyi-modules/ruoyi-ai/pom.xml +++ b/ruoyi-modules/ruoyi-ai/pom.xml @@ -17,6 +17,14 @@ + + org.dromara + ruoyi-common-core + + + org.dromara + ruoyi-api + org.dromara ruoyi-common-ai @@ -25,6 +33,10 @@ org.dromara ruoyi-common-satoken + + org.dromara + ruoyi-common-web + diff --git a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/OpenApiDemoController.java b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/OpenApiDemoController.java deleted file mode 100644 index b27d9be6a..000000000 --- a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/OpenApiDemoController.java +++ /dev/null @@ -1,263 +0,0 @@ -package org.dromara.ai.controller; - -import com.aizuda.snail.ai.common.execption.SnailAiException; -import com.aizuda.snail.ai.common.model.PageResult; -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.openapi.client.core.api.OpenApiUserClient; -import com.aizuda.snail.ai.openapi.client.core.listener.SseEventListener; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.dromara.common.satoken.utils.LoginHelper; -import org.springframework.beans.factory.annotation.Value; -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; - -/** - * OpenAPI 使用示例 Controller - * 演示如何使用 OpenAPI Client 调用 Snail AI 服务端接口 - * - * @author opensnail - * @date 2026-04-25 - */ -@Slf4j -@RestController -@RequestMapping("/snail-ai") -@RequiredArgsConstructor -public class OpenApiDemoController { - - 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; - - // ==================== User 相关接口 ==================== - - /** - * 注册当前登录用户并返回 OpenAPI 用户信息。 - */ - @PostMapping("/user/register") - public Result registerCurrentUser() { - OpenApiUserVO user = ensureOpenApiUser(); - return Result.ok(user); - } - - /** - * 查询当前登录用户对应的 OpenAPI 用户信息。 - */ - @GetMapping("/user") - public Result getUser() { - String openId = ensureOpenId(); - OpenApiUserQueryRequest request = new OpenApiUserQueryRequest(); - request.setOpenId(openId); - return userClient.getUser(request); - } - - // ==================== Agent 相关接口 ==================== - - /** - * 查询当前用户可访问的智能体列表。 - */ - @GetMapping("/agents") - public Result> listAgents() { - return agentClient.listAgents(); - } - - /** - * 根据智能体 ID 查询智能体详情。 - */ - @GetMapping("/agent/{agentId}") - public Result getAgent( - @PathVariable Long agentId) { - OpenApiAgentIdentityRequest request = new OpenApiAgentIdentityRequest(); - request.setAgentId(agentId); - return agentClient.getAgent(request); - } - - // ==================== Conversation 相关接口 ==================== - - /** - * 为指定智能体创建新会话。 - */ - @PostMapping("/agent/{agentId}/conversation") - public Result createConversation( - @PathVariable Long agentId, - @RequestBody OpenApiCreateConversationRequest request) { - request.setAgentId(agentId); - request.setOpenId(ensureOpenId()); - return conversationClient.createConversation(request); - } - - /** - * 分页查询指定智能体下的会话列表。 - */ - @GetMapping("/agent/{agentId}/conversations") - public PageResult> listConversations( - @PathVariable Long agentId, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size) { - OpenApiConversationQueryRequest request = new OpenApiConversationQueryRequest(); - request.setAgentId(agentId); - request.setOpenId(ensureOpenId()); - request.setPage(page); - request.setSize(size); - return conversationClient.listConversations(request); - } - - /** - * 查询指定会话的消息历史。 - */ - @GetMapping("/agent/{agentId}/conversation/{conversationId}/messages") - public Result> getMessages( - @PathVariable Long agentId, - @PathVariable String conversationId) { - OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest(); - request.setAgentId(agentId); - request.setConversationId(conversationId); - request.setOpenId(ensureOpenId()); - return conversationClient.getMessages(request); - } - - /** - * 删除指定会话。 - */ - @DeleteMapping("/agent/{agentId}/conversation/{conversationId}") - public Result deleteConversation( - @PathVariable Long agentId, - @PathVariable String conversationId) { - OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest(); - request.setAgentId(agentId); - request.setConversationId(conversationId); - request.setOpenId(ensureOpenId()); - return conversationClient.deleteConversation(request); - } - - // ==================== Chat 相关接口 ==================== - - /** - * 获取当前聊天发送模式。 - */ - @GetMapping("/chat/mode") - public Result> getChatMode() { - String mode = "sync".equalsIgnoreCase(chatMode) ? "sync" : "stream"; - return Result.ok(Map.of("mode", mode)); - } - - /** - * 同步对话接口。 - */ - @PostMapping("/agent/{agentId}/chat/sync") - public Result chatSync( - @PathVariable Long agentId, - @RequestBody OpenApiChatRequest request) { - request.setAgentId(agentId); - request.setOpenId(ensureOpenId()); - log.info("Sync chat request: agentId={}, content={}", agentId, request.getContent()); - return chatClient.chatSync(request); - } - - /** - * 流式对话接口,按 SSE 事件返回消息分片。 - */ - @GetMapping("/agent/{agentId}/chat/stream") - public SseEmitter chatStream( - @PathVariable Long agentId, - @RequestParam String content, - @RequestParam(required = false) String conversationId) { - SseEmitter emitter = new SseEmitter(300000L); - emitter.onTimeout(() -> { - safeSend(emitter, "error", "SSE stream timeout"); - safeSend(emitter, "done", ""); - emitter.complete(); - }); - emitter.onError(error -> log.warn("SSE emitter error: {}", error.getMessage())); - - OpenApiChatRequest request = new OpenApiChatRequest(); - request.setAgentId(agentId); - request.setOpenId(ensureOpenId()); - request.setContent(content); - request.setConversationId(conversationId); - - log.info("Stream chat request: agentId={}, content={}", agentId, content); - try { - chatClient.chatStream(request, new SseEventListener() { - @Override - public void onText(String text) { - safeSend(emitter, "text", text); - } - - @Override - public void onThinking(String thinking) { - safeSend(emitter, "thinking", thinking); - } - - @Override - public void onComplete(String data) { - safeSend(emitter, "done", data); - log.info("Stream chat completed"); - emitter.complete(); - } - - @Override - public void onError(String errorMessage) { - log.error("Stream chat error: {}", errorMessage); - safeSend(emitter, "error", errorMessage); - safeSend(emitter, "done", ""); - emitter.complete(); - } - }); - } catch (Exception e) { - log.error("Stream chat exception", e); - safeSend(emitter, "error", "stream exception: " + e.getMessage()); - safeSend(emitter, "done", ""); - emitter.complete(); - } - - return emitter; - } - - /** - * 输出一条 SSE 事件。 - */ - private void safeSend(SseEmitter emitter, String event, String data) { - try { - emitter.send(SseEmitter.event().name(event).data(data == null ? "" : data)); - } catch (IOException e) { - log.warn("SSE send failed, event={}", event, e); - } - } - - /** - * 获取当前登录用户对应的 openId,不存在时会自动注册。 - */ - private String ensureOpenId() { - return ensureOpenApiUser().getOpenId(); - } - - /** - * 确保当前登录用户已注册为 OpenAPI 用户。 - */ - private OpenApiUserVO ensureOpenApiUser() { - Long userId = LoginHelper.getUserId(); - String username = LoginHelper.getLoginUser().getNickname(); - - OpenApiUserRegisterRequest registerRequest = new OpenApiUserRegisterRequest(); - registerRequest.setExternalId(String.valueOf(userId)); - registerRequest.setNickname(username); - Result registerResult = userClient.register(registerRequest); - if (registerResult == null || registerResult.getData() == null) { - throw new SnailAiException("注册 OpenAPI 用户失败,返回为空"); - } - OpenApiUserVO user = registerResult.getData(); - return user; - } -} 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 new file mode 100644 index 000000000..5d6baba14 --- /dev/null +++ b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java @@ -0,0 +1,338 @@ +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.OpenApiAgentIdentityRequest; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiAgentVO; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiChatRequest; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiChatSyncResponse; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiConversationIdentityRequest; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiConversationQueryRequest; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiConversationVO; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiCreateConversationRequest; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiMessageVO; +import com.aizuda.snail.ai.common.openapi.dto.OpenApiUserQueryRequest; +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.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.openapi.client.core.api.OpenApiUserClient; +import com.aizuda.snail.ai.openapi.client.core.listener.SseEventListener; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +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; + +/** + * Snail AI OpenAPI 控制器 + * + * @author opensnail + * @date 2026-04-25 + */ +@Slf4j +@Validated +@RestController +@RequestMapping("/snail-ai") +@RequiredArgsConstructor +public class SnailAiController extends BaseController { + + private static final int SNAIL_AI_SUCCESS = 1; + 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 用户信息。 + */ + @PostMapping("/user/register") + public R registerCurrentUser() { + 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> getChatMode() { + String mode = "sync".equalsIgnoreCase(chatMode) ? "sync" : "stream"; + return R.ok(Map.of("mode", mode)); + } + + /** + * 同步对话接口。 + */ + @PostMapping("/agent/{agentId}/chat/sync") + public R chatSync( + @NotNull(message = "智能体ID不能为空") @PathVariable Long agentId, + @RequestBody OpenApiChatRequest request) { + request.setAgentId(agentId); + request.setOpenId(ensureOpenId()); + log.info("Sync chat request: agentId={}", agentId); + return toR(chatClient.chatSync(request)); + } + + /** + * 流式对话接口,按 SSE 事件返回消息分片。 + */ + @PostMapping(value = "/agent/{agentId}/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter chatStream( + @NotNull(message = "智能体ID不能为空") @PathVariable Long agentId, + @RequestBody OpenApiChatRequest request) { + SseEmitter emitter = new SseEmitter(SSE_TIMEOUT); + AtomicBoolean closed = new AtomicBoolean(false); + emitter.onTimeout(() -> { + if (!closed.get()) { + safeSend(emitter, closed, "error", "SSE stream timeout"); + safeSend(emitter, closed, "done", ""); + if (closed.compareAndSet(false, true)) { + emitter.complete(); + } + } + }); + emitter.onCompletion(() -> closed.set(true)); + emitter.onError(error -> { + closed.set(true); + log.warn("SSE emitter error: {}", error.getMessage()); + }); + + request.setAgentId(agentId); + request.setOpenId(ensureOpenId()); + + log.info("Stream chat request: agentId={}", agentId); + try { + chatClient.chatStream(request, new SseEventListener() { + @Override + public void onText(String text) { + safeSend(emitter, closed, "text", text); + } + + @Override + public void onThinking(String thinking) { + safeSend(emitter, closed, "thinking", thinking); + } + + @Override + public void onComplete(String data) { + if (!closed.get()) { + safeSend(emitter, closed, "done", data); + log.info("Stream chat completed"); + if (closed.compareAndSet(false, true)) { + emitter.complete(); + } + } + } + + @Override + public void onError(String errorMessage) { + log.error("Stream chat error: {}", errorMessage); + if (!closed.get()) { + safeSend(emitter, closed, "error", errorMessage); + safeSend(emitter, closed, "done", ""); + if (closed.compareAndSet(false, true)) { + emitter.complete(); + } + } + } + }); + } catch (Exception e) { + log.error("Stream chat exception", e); + if (!closed.get()) { + safeSend(emitter, closed, "error", "stream exception: " + e.getMessage()); + safeSend(emitter, closed, "done", ""); + if (closed.compareAndSet(false, true)) { + emitter.complete(); + } + } + } + + return emitter; + } + + /** + * 转换 Snail AI 普通响应为项目统一响应。 + */ + private R toR(Result result) { + if (result == null) { + return R.fail("Snail AI 服务返回为空"); + } + if (result.getStatus() != SNAIL_AI_SUCCESS) { + return R.fail(result.getMessage()); + } + return R.ok(result.getData()); + } + + /** + * 转换 Snail AI 分页响应为项目统一分页响应。 + */ + private R> toPageR(com.aizuda.snail.ai.common.model.PageResult> result) { + R> response = toR(result); + if (R.isError(response)) { + return R.fail(response.getMsg()); + } + return R.ok(PageResult.build(response.getData(), result.getTotal())); + } + + /** + * 输出一条 SSE 事件。 + */ + private boolean safeSend(SseEmitter emitter, AtomicBoolean closed, String event, String data) { + if (closed.get()) { + return false; + } + try { + emitter.send(SseEmitter.event().name(event).data(data == null ? "" : data)); + return true; + } catch (IOException e) { + closed.set(true); + log.warn("SSE send failed, event={}", event, e); + emitter.completeWithError(e); + return false; + } + } + + /** + * 获取当前登录用户对应的 openId,不存在时会自动注册。 + */ + private String ensureOpenId() { + return ensureOpenApiUser().getOpenId(); + } + + /** + * 确保当前登录用户已注册为 OpenAPI 用户。 + */ + private OpenApiUserVO ensureOpenApiUser() { + Long userId = LoginHelper.getUserId(); + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser == null || userId == null) { + throw new SnailAiException("当前登录用户为空"); + } + + OpenApiUserRegisterRequest registerRequest = new OpenApiUserRegisterRequest(); + registerRequest.setExternalId(String.valueOf(userId)); + registerRequest.setNickname(loginUser.getNickname()); + Result registerResult = userClient.register(registerRequest); + if (registerResult == null) { + throw new SnailAiException("注册 OpenAPI 用户失败,返回为空"); + } + if (registerResult.getStatus() != SNAIL_AI_SUCCESS) { + throw new SnailAiException(registerResult.getMessage()); + } + if (registerResult.getData() == null) { + throw new SnailAiException("注册 OpenAPI 用户失败,返回为空"); + } + return registerResult.getData(); + } +} diff --git a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/package-info.java b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/package-info.java deleted file mode 100644 index d97a8cbe5..000000000 --- a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package org.dromara.ai; diff --git a/script/sql/oracle/oracle_ry_vue.sql b/script/sql/oracle/oracle_ry_vue.sql index fc98c3007..631a2fa0f 100644 --- a/script/sql/oracle/oracle_ry_vue.sql +++ b/script/sql/oracle/oracle_ry_vue.sql @@ -346,6 +346,7 @@ insert into sys_menu values(1761400000000000001, '系统管理', 0, 1, 'system', insert into sys_menu values(1761400000000000002, '系统监控', 0, 3, 'monitor', null, '', 'N', 'Y', 'M', '0', '0', '', 'monitor', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, '系统监控目录'); insert into sys_menu values(1761400000000000003, '系统工具', 0, 4, 'tool', null, '', 'N', 'Y', 'M', '0', '0', '', 'tool', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, '系统工具目录'); insert into sys_menu values(1761400000000000005, '测试菜单', 0, 5, 'demo', null, '', 'N', 'Y', 'M', '0', '0', null, 'star', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, ''); +insert into sys_menu values(1761400000000000006, 'AI会话', 0, 6, 'ai/chat', 'ai/chat/index', '', 'N', 'Y', 'C', '0', '0', '', 'checkbox', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, 'AI聊天菜单'); insert into sys_menu values(1761400000000000004, 'PLUS官网', 0, 9, 'https://gitee.com/dromara/RuoYi-Vue-Plus', null, '', 'Y', 'Y', 'M', '0', '0', '', 'guide', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, 'RuoYi-Vue-Plus官网地址'); -- 二级菜单 insert into sys_menu values(1761400000000000100, '用户管理', 1761400000000000001, 1, 'user', 'system/user/index', '', 'N', 'Y', 'C', '0', '0', 'system:user:list', 'user', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, '用户管理菜单'); diff --git a/script/sql/postgres/postgres_ry_vue.sql b/script/sql/postgres/postgres_ry_vue.sql index 8f0f1cbfc..ac2a81012 100644 --- a/script/sql/postgres/postgres_ry_vue.sql +++ b/script/sql/postgres/postgres_ry_vue.sql @@ -343,6 +343,7 @@ insert into sys_menu values(1761400000000000001, '系统管理', 0, 1, 'system', insert into sys_menu values(1761400000000000002, '系统监控', 0, 3, 'monitor', null, '', 'N', 'Y', 'M', '0', '0', '', 'monitor', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, '系统监控目录'); insert into sys_menu values(1761400000000000003, '系统工具', 0, 4, 'tool', null, '', 'N', 'Y', 'M', '0', '0', '', 'tool', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, '系统工具目录'); insert into sys_menu values(1761400000000000005, '测试菜单', 0, 5, 'demo', null, '', 'N', 'Y', 'M', '0', '0', null, 'star', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, '测试菜单'); +insert into sys_menu values(1761400000000000006, 'AI会话', 0, 6, 'ai/chat', 'ai/chat/index', '', 'N', 'Y', 'C', '0', '0', '', 'checkbox', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, 'AI聊天菜单'); insert into sys_menu values(1761400000000000004, 'PLUS官网', 0, 9, 'https://gitee.com/dromara/RuoYi-Vue-Plus', null, '', 'Y', 'Y', 'M', '0', '0', '', 'guide', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, 'RuoYi-Vue-Plus官网地址'); -- 二级菜单 insert into sys_menu values(1761400000000000100, '用户管理', 1761400000000000001, 1, 'user', 'system/user/index', '', 'N', 'Y', 'C', '0', '0', 'system:user:list', 'user', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, '用户管理菜单'); diff --git a/script/sql/ry_ai.sql b/script/sql/ry_ai.sql deleted file mode 100644 index d52ff0db7..000000000 --- a/script/sql/ry_ai.sql +++ /dev/null @@ -1 +0,0 @@ -INSERT INTO sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `active_menu`, `ext`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2056263986068676609, 'AI聊天', 0, 1, 'ai/chat', 'ai/chat/index', NULL, 'N', 'Y', 'C', '0', '0', NULL, 'checkbox', '', '', 1761000000000000103, 1761100000000000001, '2026-05-18 14:41:52', 1761100000000000001, '2026-05-18 14:43:41', ''); \ No newline at end of file diff --git a/script/sql/ry_vue.sql b/script/sql/ry_vue.sql index 95db7f68d..e6b2d3dda 100644 --- a/script/sql/ry_vue.sql +++ b/script/sql/ry_vue.sql @@ -212,6 +212,7 @@ insert into sys_menu values(1761400000000000001, '系统管理', 0, 1, 'system', insert into sys_menu values(1761400000000000002, '系统监控', 0, 3, 'monitor', null, '', 'N', 'Y', 'M', '0', '0', '', 'monitor', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '系统监控目录'); insert into sys_menu values(1761400000000000003, '系统工具', 0, 4, 'tool', null, '', 'N', 'Y', 'M', '0', '0', '', 'tool', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '系统工具目录'); insert into sys_menu values(1761400000000000005, '测试菜单', 0, 5, 'demo', null, '', 'N', 'Y', 'M', '0', '0', '', 'star', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '测试菜单'); +insert into sys_menu values(1761400000000000006, 'AI会话', 0, 6, 'ai/chat', 'ai/chat/index', '', 'N', 'Y', 'C', '0', '0', '', 'checkbox', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, 'AI聊天菜单'); insert into sys_menu values(1761400000000000004, 'PLUS官网', 0, 9, 'https://gitee.com/dromara/RuoYi-Vue-Plus', null, '', 'Y', 'Y', 'M', '0', '0', '', 'guide', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, 'RuoYi-Vue-Plus官网地址'); -- 二级菜单 insert into sys_menu values(1761400000000000100, '用户管理', 1761400000000000001, 1, 'user', 'system/user/index', '', 'N', 'Y', 'C', '0', '0', 'system:user:list', 'user', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '用户管理菜单'); diff --git a/script/sql/sqlserver/sqlserver_ry_vue.sql b/script/sql/sqlserver/sqlserver_ry_vue.sql index 460135d97..12e3eb02c 100644 --- a/script/sql/sqlserver/sqlserver_ry_vue.sql +++ b/script/sql/sqlserver/sqlserver_ry_vue.sql @@ -1378,6 +1378,8 @@ insert into sys_menu values(1761400000000000003, N'系统工具', 0, 4, N'tool', GO insert into sys_menu values(1761400000000000005, N'测试菜单', 0, 5, N'demo', NULL, N'', N'N', N'Y', N'M', N'0', N'0', NULL, N'star', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), NULL, NULL, N''); GO +insert into sys_menu values(1761400000000000006, N'AI会话', 0, 6, N'ai/chat', N'ai/chat/index', N'', N'N', N'Y', N'C', N'0', N'0', N'', N'checkbox', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), NULL, NULL, N'AI聊天菜单'); +GO insert into sys_menu values(1761400000000000004, N'PLUS官网', 0, 9, N'https://gitee.com/dromara/RuoYi-Vue-Plus', null, N'', N'Y', N'Y', N'M', N'0', N'0', N'', N'guide', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), null, null, N'RuoYi-Vue-Plus官网地址'); GO insert into sys_menu values(1761400000000000100, N'用户管理', 1761400000000000001, 1, N'user', N'system/user/index', N'', N'N', N'Y', N'C', N'0', N'0', N'system:user:list', N'user', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), NULL, NULL, N'用户管理菜单');