diff --git a/pom.xml b/pom.xml index 02a984b55..81d638836 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ 2.2.7 4.5.0 1.9.0 + 0.0.1 1.5.0 0.2.0 1.18.42 @@ -278,6 +279,18 @@ ${snailjob.version} + + com.aizuda + snail-ai-agent-starter + ${snailai.version} + + + + com.aizuda + snail-ai-openapi-starter + ${snailai.version} + + org.bouncycastle @@ -341,6 +354,12 @@ ${revision} + + org.dromara + ruoyi-ai + ${revision} + + org.dromara ruoyi-gen diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 80ef4b3d5..e29d4a656 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -75,6 +75,10 @@ ruoyi-job + + org.dromara + ruoyi-ai + diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 1b52fab96..f7a9a684e 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -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` 表 @@ -28,6 +28,39 @@ snail-job: # 客户端ip指定 host: +--- # snail-ai 配置 +snail-ai: + # 启用客户端模式 + enabled: true + # ==================== Server 连接 ==================== + # Server 端 gRPC 地址(即 snail-ai-starter 的 snail-ai.server.grpc-port) + server: + host: 127.0.0.1 + port: 18888 + # ==================== 客户端配置 ==================== + # 本客户端 gRPC 端口(Server 通过此端口分发 Chat 请求) + # 应用 ID(在 Server「应用管理」页面创建后获取) + app-id: 1 + # 认证令牌(在 Server「应用管理」页面创建时自动生成) + token: SAI_ce6fbc820c50456baecc7cdcf2a14b1b + port: 18889 + # Skill 文件临时目录 + skill-temp-dir: /tmp/snail-ai-agent/skills + # ==================== OpenAPI Client 配置 ==================== + open-api: + # 启用 OpenAPI Client + enabled: true + # Server HTTP 端口 + web-port: 18080 + # 是否使用 HTTPS + https: false + # API 路径前缀 + prefix: snail-ai + # 超时配置(毫秒) + connect-timeout-ms: 5000 + read-timeout-ms: 60000 + chat-timeout-ms: 300000 + --- # 数据源配置 spring: datasource: @@ -47,9 +80,9 @@ spring: driverClassName: com.mysql.cj.jdbc.Driver # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562 # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题) - url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + url: jdbc:mysql://8.136.96.136:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true username: root - password: root + password: mysql_5NrZmE # # 从库数据源 # slave: # lazy: true @@ -96,13 +129,13 @@ spring: spring.data: redis: # 地址 - host: localhost + host: 8.136.96.136 # 端口,默认为6379 port: 6379 # 数据库索引 - database: 0 + database: 11 # redis 密码必须配置 - password: ruoyi123 + password: redis_JsjKmr # 连接超时时间 timeout: 10s # 是否开启ssl diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index b8848f036..324f4ab19 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -224,7 +224,7 @@ message: --- # warm-flow工作流配置 warm-flow: # 是否开启工作流,默认true - enabled: true + enabled: false # 是否开启设计器ui ui: true # 是否显示流程图顶部文字 diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index f6a30c47f..c88e1fd11 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -37,6 +37,7 @@ ruoyi-common-encrypt ruoyi-common-push ruoyi-common-mqtt + ruoyi-common-ai diff --git a/ruoyi-common/ruoyi-common-ai/pom.xml b/ruoyi-common/ruoyi-common-ai/pom.xml new file mode 100644 index 000000000..f1eb0712b --- /dev/null +++ b/ruoyi-common/ruoyi-common-ai/pom.xml @@ -0,0 +1,41 @@ + + + + ruoyi-common + org.dromara + ${revision} + + 4.0.0 + + ruoyi-common-ai + + + ruoyi-common-ai AI公共模块 + + + + + + org.springframework.boot + spring-boot-autoconfigure + + + + com.aizuda + snail-ai-agent-starter + + + + com.aizuda + snail-ai-openapi-starter + + + + org.dromara + ruoyi-common-core + + + + diff --git a/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/config/SnailAiConfig.java b/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/config/SnailAiConfig.java new file mode 100644 index 000000000..5ade5cbdd --- /dev/null +++ b/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/config/SnailAiConfig.java @@ -0,0 +1,16 @@ +package org.dromara.common.ai.config; + +import com.aizuda.snail.ai.agent.starter.EnableSnailAiAgent; +import com.aizuda.snail.ai.openapi.client.starter.EnableSnailAiOpenApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * Snail AI 自动配置 + */ +@AutoConfiguration +@ConditionalOnProperty(prefix = "snail-ai", name = "enabled", havingValue = "true") +@EnableSnailAiAgent +@EnableSnailAiOpenApi +public class SnailAiConfig { +} diff --git a/ruoyi-common/ruoyi-common-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/ruoyi-common/ruoyi-common-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..32af79519 --- /dev/null +++ b/ruoyi-common/ruoyi-common-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.dromara.common.ai.config.SnailAiConfig diff --git a/ruoyi-common/ruoyi-common-bom/pom.xml b/ruoyi-common/ruoyi-common-bom/pom.xml index b60c35f40..0526082c4 100644 --- a/ruoyi-common/ruoyi-common-bom/pom.xml +++ b/ruoyi-common/ruoyi-common-bom/pom.xml @@ -159,6 +159,13 @@ ${revision} + + + org.dromara + ruoyi-common-ai + ${revision} + + diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index 971929a54..4a79d2500 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -21,6 +21,7 @@ ruoyi-job ruoyi-system ruoyi-workflow + ruoyi-ai diff --git a/ruoyi-modules/ruoyi-ai/pom.xml b/ruoyi-modules/ruoyi-ai/pom.xml new file mode 100644 index 000000000..1baf5cb8a --- /dev/null +++ b/ruoyi-modules/ruoyi-ai/pom.xml @@ -0,0 +1,31 @@ + + + + org.dromara + ruoyi-modules + ${revision} + + 4.0.0 + jar + ruoyi-ai + + + ai模块 + + + + + + org.dromara + ruoyi-common-ai + + + org.dromara + ruoyi-common-satoken + + + + + 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 new file mode 100644 index 000000000..5b62e5ed5 --- /dev/null +++ b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/OpenApiDemoController.java @@ -0,0 +1,251 @@ +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 io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; + +/** + * OpenAPI 使用示例 Controller + *

+ * 演示如何使用 OpenAPI Client 调用 Snail AI 服务端接口 + * + * @author opensnail + * @date 2026-04-25 + */ +@Slf4j +@RestController +@RequestMapping("/snail-ai") +@RequiredArgsConstructor +@Tag(name = "OpenAPI Demo", description = "OpenAPI 客户端使用示例") +public class OpenApiDemoController { + + private final OpenApiAgentClient agentClient; + private final OpenApiChatClient chatClient; + private final OpenApiConversationClient conversationClient; + private final OpenApiUserClient userClient; + + // ==================== User 相关接口 ==================== + + @PostMapping("/user/register") + @Operation(summary = "注册当前登录用户", description = "使用当前系统登录用户自动注册,返回 openId") + public Result registerCurrentUser() { + OpenApiUserVO user = ensureOpenApiUser(); + return Result.ok(user); + } + + @GetMapping("/user") + @Operation(summary = "获取当前登录用户的 OpenAPI 信息", description = "自动解析当前用户 openId 并查询详情") + public Result getUser() { + String openId = ensureOpenId(); + OpenApiUserQueryRequest request = new OpenApiUserQueryRequest(); + request.setOpenId(openId); + return userClient.getUser(request); + } + + // ==================== Agent 相关接口 ==================== + + @GetMapping("/agents") + @Operation(summary = "获取所有 Agent 列表", description = "查询当前用户可访问的所有智能体") + public Result> listAgents() { + return agentClient.listAgents(); + } + + @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); + return agentClient.getAgent(request); + } + + // ==================== 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); + request.setOpenId(ensureOpenId()); + request.setPage(page); + request.setSize(size); + 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); + request.setConversationId(conversationId); + request.setOpenId(ensureOpenId()); + 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); + request.setConversationId(conversationId); + request.setOpenId(ensureOpenId()); + return conversationClient.deleteConversation(request); + } + + // ==================== Chat 相关接口 ==================== + + @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()); + log.info("Sync chat request: agentId={}, content={}", agentId, request.getContent()); + return chatClient.chatSync(request); + } + + @GetMapping("/agent/{agentId}/chat/stream") + @Operation(summary = "流式对话", description = "发送消息并以 SSE 流式接收 AI 回复") + public SseEmitter chatStream( + @Parameter(description = "Agent ID", required = true, example = "1") + @PathVariable Long agentId, + @Parameter(description = "用户消息", required = true, example = "你好") + @RequestParam String content, + @Parameter(description = "会话 ID(可选)", example = "conv-123") + @RequestParam(required = false) String conversationId) { + + SseEmitter emitter = new SseEmitter(300000L); // 5 分钟超时 + + 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) { + try { + emitter.send(SseEmitter.event() + .name("text") + .data(text)); + } catch (IOException e) { + log.error("Failed to send SSE text", e); + emitter.completeWithError(e); + } + } + + @Override + public void onThinking(String thinking) { + try { + emitter.send(SseEmitter.event() + .name("thinking") + .data(thinking)); + } catch (IOException e) { + log.error("Failed to send SSE thinking", e); + } + } + + @Override + public void onComplete(String data) { + try { + emitter.send(SseEmitter.event() + .name("done") + .data(data)); + emitter.complete(); + log.info("Stream chat completed"); + } catch (IOException e) { + log.error("Failed to send SSE completion", e); + emitter.completeWithError(e); + } + } + + @Override + public void onError(String errorMessage) { + log.error("Stream chat error: {}", errorMessage); + try { + emitter.send(SseEmitter.event() + .name("error") + .data(errorMessage)); + } catch (IOException e) { + log.error("Failed to send SSE error", e); + } + emitter.completeWithError(new SnailAiException(errorMessage)); + } + }); + } catch (Exception e) { + log.error("Stream chat exception", e); + emitter.completeWithError(e); + } + + return emitter; + } + + private String ensureOpenId() { + return ensureOpenApiUser().getOpenId(); + } + + 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/package-info.java b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/package-info.java new file mode 100644 index 000000000..d97a8cbe5 --- /dev/null +++ b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/package-info.java @@ -0,0 +1 @@ +package org.dromara.ai;