暂存修改

This commit is contained in:
ct 2026-05-15 09:36:01 +08:00
parent 39d27a4249
commit bb94a6b9a9
13 changed files with 413 additions and 7 deletions

19
pom.xml
View File

@ -33,6 +33,7 @@
<lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.5.0</dynamic-ds.version>
<snailjob.version>1.9.0</snailjob.version>
<snailai.version>0.0.1</snailai.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.42</lombok.version>
@ -278,6 +279,18 @@
<version>${snailjob.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-agent-starter</artifactId>
<version>${snailai.version}</version>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-openapi-starter</artifactId>
<version>${snailai.version}</version>
</dependency>
<!-- 加密包引入 -->
<dependency>
<groupId>org.bouncycastle</groupId>
@ -341,6 +354,12 @@
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-ai</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-gen</artifactId>

View File

@ -75,6 +75,10 @@
<artifactId>ruoyi-job</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-ai</artifactId>
</dependency>
<!-- demo模块 -->
<dependency>

View File

@ -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

View File

@ -224,7 +224,7 @@ message:
--- # warm-flow工作流配置
warm-flow:
# 是否开启工作流默认true
enabled: true
enabled: false
# 是否开启设计器ui
ui: true
# 是否显示流程图顶部文字

View File

@ -37,6 +37,7 @@
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-push</module>
<module>ruoyi-common-mqtt</module>
<module>ruoyi-common-ai</module>
</modules>
</project>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
<parent>
<artifactId>ruoyi-common</artifactId>
<groupId>org.dromara</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-ai</artifactId>
<description>
ruoyi-common-ai AI公共模块
</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-agent-starter</artifactId>
</dependency>
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-ai-openapi-starter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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 {
}

View File

@ -159,6 +159,13 @@
<version>${revision}</version>
</dependency>
<!-- ai模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ai</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -21,6 +21,7 @@
<module>ruoyi-job</module>
<module>ruoyi-system</module>
<module>ruoyi-workflow</module>
<module>ruoyi-ai</module>
</modules>
</project>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-ai</artifactId>
<description>
ai模块
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-ai</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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
* <p>
* 演示如何使用 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<OpenApiUserVO> registerCurrentUser() {
OpenApiUserVO user = ensureOpenApiUser();
return Result.ok(user);
}
@GetMapping("/user")
@Operation(summary = "获取当前登录用户的 OpenAPI 信息", description = "自动解析当前用户 openId 并查询详情")
public Result<OpenApiUserVO> getUser() {
String openId = ensureOpenId();
OpenApiUserQueryRequest request = new OpenApiUserQueryRequest();
request.setOpenId(openId);
return userClient.getUser(request);
}
// ==================== Agent 相关接口 ====================
@GetMapping("/agents")
@Operation(summary = "获取所有 Agent 列表", description = "查询当前用户可访问的所有智能体")
public Result<List<OpenApiAgentVO>> listAgents() {
return agentClient.listAgents();
}
@GetMapping("/agent/{agentId}")
@Operation(summary = "获取 Agent 详情", description = "根据 ID 查询智能体详细信息")
public Result<OpenApiAgentVO> 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<OpenApiConversationVO> 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<List<OpenApiConversationVO>> 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<List<OpenApiMessageVO>> 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<Void> 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<OpenApiChatSyncResponse> 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<OpenApiUserVO> registerResult = userClient.register(registerRequest);
if (registerResult == null || registerResult.getData() == null) {
throw new SnailAiException("注册 OpenAPI 用户失败,返回为空");
}
OpenApiUserVO user = registerResult.getData();
return user;
}
}

View File

@ -0,0 +1 @@
package org.dromara.ai;