From 7b81f13866e1a7daeb04e9c5f0968434fc4fde27 Mon Sep 17 00:00:00 2001 From: ct <164434275@qq.com> Date: Tue, 19 May 2026 14:03:01 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E6=94=B9=E4=BD=BF=E7=94=A8S?= =?UTF-8?q?pring=20sse=E6=96=B9=E5=BC=8F=202=E3=80=81=E5=A2=9E=E5=8A=A0sec?= =?UTF-8?q?urity=E6=8E=92=E9=99=A4=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 1 + .../ai/controller/OpenApiDemoController.java | 176 ++++++++---------- 2 files changed, 74 insertions(+), 103 deletions(-) diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index b8848f036..9b28f219b 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -109,6 +109,7 @@ security: - /*/api-docs - /*/api-docs/** - /warm-flow-ui/config + - /snail-ai/agent/*/chat/stream # MyBatisPlus配置 # https://baomidou.com/config/ 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 index f429ea797..b27d9be6a 100644 --- 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 @@ -9,27 +9,19 @@ 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 io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.satoken.utils.LoginHelper; -import org.springframework.http.MediaType; 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.io.PrintWriter; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; /** * OpenAPI 使用示例 Controller - *

* 演示如何使用 OpenAPI Client 调用 Snail AI 服务端接口 * * @author opensnail @@ -39,7 +31,6 @@ import java.util.concurrent.TimeUnit; @RestController @RequestMapping("/snail-ai") @RequiredArgsConstructor -@Tag(name = "OpenAPI Demo", description = "OpenAPI 客户端使用示例") public class OpenApiDemoController { private final OpenApiAgentClient agentClient; @@ -51,15 +42,19 @@ public class OpenApiDemoController { // ==================== User 相关接口 ==================== + /** + * 注册当前登录用户并返回 OpenAPI 用户信息。 + */ @PostMapping("/user/register") - @Operation(summary = "注册当前登录用户", description = "使用当前系统登录用户自动注册,返回 openId") public Result registerCurrentUser() { OpenApiUserVO user = ensureOpenApiUser(); return Result.ok(user); } + /** + * 查询当前登录用户对应的 OpenAPI 用户信息。 + */ @GetMapping("/user") - @Operation(summary = "获取当前登录用户的 OpenAPI 信息", description = "自动解析当前用户 openId 并查询详情") public Result getUser() { String openId = ensureOpenId(); OpenApiUserQueryRequest request = new OpenApiUserQueryRequest(); @@ -69,16 +64,19 @@ public class OpenApiDemoController { // ==================== Agent 相关接口 ==================== + /** + * 查询当前用户可访问的智能体列表。 + */ @GetMapping("/agents") - @Operation(summary = "获取所有 Agent 列表", description = "查询当前用户可访问的所有智能体") public Result> listAgents() { return agentClient.listAgents(); } + /** + * 根据智能体 ID 查询智能体详情。 + */ @GetMapping("/agent/{agentId}") - @Operation(summary = "获取 Agent 详情", description = "根据 ID 查询智能体详细信息") public Result getAgent( - @Parameter(description = "Agent ID", required = true, example = "1") @PathVariable Long agentId) { OpenApiAgentIdentityRequest request = new OpenApiAgentIdentityRequest(); request.setAgentId(agentId); @@ -87,26 +85,25 @@ public class OpenApiDemoController { // ==================== Conversation 相关接口 ==================== + /** + * 为指定智能体创建新会话。 + */ @PostMapping("/agent/{agentId}/conversation") - @Operation(summary = "创建会话", description = "为指定 Agent 创建一个新的对话会话") public Result createConversation( - @Parameter(description = "Agent ID", required = true, example = "1") @PathVariable Long agentId, - @Parameter(description = "创建会话请求") @RequestBody OpenApiCreateConversationRequest request) { request.setAgentId(agentId); request.setOpenId(ensureOpenId()); return conversationClient.createConversation(request); } + /** + * 分页查询指定智能体下的会话列表。 + */ @GetMapping("/agent/{agentId}/conversations") - @Operation(summary = "获取会话列表", description = "查询指定 Agent 的所有会话(分页)") public PageResult> listConversations( - @Parameter(description = "Agent ID", required = true, example = "1") @PathVariable Long agentId, - @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") int page, - @Parameter(description = "每页数量", example = "10") @RequestParam(defaultValue = "10") int size) { OpenApiConversationQueryRequest request = new OpenApiConversationQueryRequest(); request.setAgentId(agentId); @@ -116,12 +113,12 @@ public class OpenApiDemoController { return conversationClient.listConversations(request); } + /** + * 查询指定会话的消息历史。 + */ @GetMapping("/agent/{agentId}/conversation/{conversationId}/messages") - @Operation(summary = "获取会话消息", description = "查询指定会话的所有消息记录") public Result> getMessages( - @Parameter(description = "Agent ID", required = true, example = "1") @PathVariable Long agentId, - @Parameter(description = "会话 ID", required = true, example = "conv-123") @PathVariable String conversationId) { OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest(); request.setAgentId(agentId); @@ -130,12 +127,12 @@ public class OpenApiDemoController { return conversationClient.getMessages(request); } + /** + * 删除指定会话。 + */ @DeleteMapping("/agent/{agentId}/conversation/{conversationId}") - @Operation(summary = "删除会话", description = "删除指定的对话会话") public Result deleteConversation( - @Parameter(description = "Agent ID", required = true, example = "1") @PathVariable Long agentId, - @Parameter(description = "会话 ID", required = true, example = "conv-123") @PathVariable String conversationId) { OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest(); request.setAgentId(agentId); @@ -146,19 +143,21 @@ public class OpenApiDemoController { // ==================== Chat 相关接口 ==================== + /** + * 获取当前聊天发送模式。 + */ @GetMapping("/chat/mode") - @Operation(summary = "获取聊天发送模式", description = "返回 stream(流式) 或 sync(同步)") public Result> getChatMode() { String mode = "sync".equalsIgnoreCase(chatMode) ? "sync" : "stream"; return Result.ok(Map.of("mode", mode)); } + /** + * 同步对话接口。 + */ @PostMapping("/agent/{agentId}/chat/sync") - @Operation(summary = "同步对话", description = "发送消息并等待 AI 回复(非流式)") public Result chatSync( - @Parameter(description = "Agent ID", required = true, example = "1") @PathVariable Long agentId, - @Parameter(description = "对话请求") @RequestBody OpenApiChatRequest request) { request.setAgentId(agentId); request.setOpenId(ensureOpenId()); @@ -166,21 +165,21 @@ public class OpenApiDemoController { return chatClient.chatSync(request); } + /** + * 流式对话接口,按 SSE 事件返回消息分片。 + */ @GetMapping("/agent/{agentId}/chat/stream") - @Operation(summary = "流式对话", description = "发送消息并以 SSE 流式接收 AI 回复") - public void chatStream( - @Parameter(description = "Agent ID", required = true, example = "1") + public SseEmitter chatStream( @PathVariable Long agentId, - @Parameter(description = "用户消息", required = true, example = "你好") @RequestParam String content, - @Parameter(description = "会话 ID(可选)", example = "conv-123") - @RequestParam(required = false) String conversationId, - HttpServletResponse response) { - response.setStatus(HttpServletResponse.SC_OK); - response.setCharacterEncoding("UTF-8"); - response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE); - response.setHeader("Cache-Control", "no-cache"); - response.setHeader("Connection", "keep-alive"); + @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); @@ -189,93 +188,64 @@ public class OpenApiDemoController { request.setConversationId(conversationId); log.info("Stream chat request: agentId={}, content={}", agentId, content); - CountDownLatch latch = new CountDownLatch(1); - final boolean[] completed = {false}; try { - PrintWriter writer = response.getWriter(); chatClient.chatStream(request, new SseEventListener() { @Override public void onText(String text) { - try { - writeSseEvent(writer, "text", text); - } catch (IOException e) { - log.error("Failed to send SSE text", e); - completed[0] = true; - latch.countDown(); - } + safeSend(emitter, "text", text); } @Override public void onThinking(String thinking) { - try { - writeSseEvent(writer, "thinking", thinking); - } catch (IOException e) { - log.error("Failed to send SSE thinking", e); - } + safeSend(emitter, "thinking", thinking); } @Override public void onComplete(String data) { - try { - writeSseEvent(writer, "done", data); - log.info("Stream chat completed"); - } catch (IOException e) { - log.error("Failed to send SSE completion", e); - } finally { - completed[0] = true; - latch.countDown(); - } + safeSend(emitter, "done", data); + log.info("Stream chat completed"); + emitter.complete(); } @Override public void onError(String errorMessage) { log.error("Stream chat error: {}", errorMessage); - try { - writeSseEvent(writer, "error", errorMessage); - } catch (IOException e) { - log.error("Failed to send SSE error", e); - } finally { - completed[0] = true; - latch.countDown(); - } + safeSend(emitter, "error", errorMessage); + safeSend(emitter, "done", ""); + emitter.complete(); } }); - latch.await(5, TimeUnit.MINUTES); - if (!completed[0]) { - writeSseEvent(writer, "error", "SSE stream timeout"); - writeSseEvent(writer, "done", ""); - } - writer.flush(); } catch (Exception e) { log.error("Stream chat exception", e); - try { - PrintWriter writer = response.getWriter(); - writeSseEvent(writer, "error", "stream exception: " + e.getMessage()); - writeSseEvent(writer, "done", ""); - writer.flush(); - } catch (IOException ex) { - log.error("Failed to write stream exception", ex); - } - } - } - - private void writeSseEvent(PrintWriter writer, String event, String data) throws IOException { - synchronized (writer) { - writer.write("event: " + event + "\n"); - String payload = data == null ? "" : data; - String[] lines = payload.split("\\R", -1); - for (String line : lines) { - writer.write("data: " + line + "\n"); - } - writer.write("\n"); - writer.flush(); + 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();