mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2026-06-10 07:46:08 +00:00
update 优化 !pr850 相关代码用法与问题
This commit is contained in:
4
pom.xml
4
pom.xml
@@ -33,7 +33,7 @@
|
||||
<lock4j.version>2.2.7</lock4j.version>
|
||||
<dynamic-ds.version>4.5.0</dynamic-ds.version>
|
||||
<snailjob.version>2.0.0</snailjob.version>
|
||||
<snailai.version>0.0.1</snailai.version>
|
||||
<snailai.version>0.0.2</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>
|
||||
@@ -56,7 +56,7 @@
|
||||
<easy-es.version>3.0.2</easy-es.version>
|
||||
<elasticsearch-client.version>7.17.28</elasticsearch-client.version>
|
||||
<!-- Spring AI 2.0 预览版,正式版发布后仅需调整此版本号 -->
|
||||
<spring-ai.version>2.0.0-M6</spring-ai.version>
|
||||
<spring-ai.version>2.0.0-M7</spring-ai.version>
|
||||
|
||||
<!-- 插件版本 -->
|
||||
<maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
|
||||
|
||||
@@ -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 连接 ====================
|
||||
|
||||
@@ -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 连接 ====================
|
||||
|
||||
@@ -109,7 +109,6 @@ security:
|
||||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
- /snail-ai/agent/*/chat/stream
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
|
||||
@@ -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">
|
||||
<parent>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -18,8 +18,8 @@
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -32,10 +32,5 @@
|
||||
<artifactId>snail-ai-openapi-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -170,8 +170,9 @@
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-ai</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- mcp模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-ai</artifactId>
|
||||
@@ -25,6 +33,10 @@
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-satoken</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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<OpenApiUserVO> registerCurrentUser() {
|
||||
OpenApiUserVO user = ensureOpenApiUser();
|
||||
return Result.ok(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前登录用户对应的 OpenAPI 用户信息。
|
||||
*/
|
||||
@GetMapping("/user")
|
||||
public Result<OpenApiUserVO> getUser() {
|
||||
String openId = ensureOpenId();
|
||||
OpenApiUserQueryRequest request = new OpenApiUserQueryRequest();
|
||||
request.setOpenId(openId);
|
||||
return userClient.getUser(request);
|
||||
}
|
||||
|
||||
// ==================== Agent 相关接口 ====================
|
||||
|
||||
/**
|
||||
* 查询当前用户可访问的智能体列表。
|
||||
*/
|
||||
@GetMapping("/agents")
|
||||
public Result<List<OpenApiAgentVO>> listAgents() {
|
||||
return agentClient.listAgents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据智能体 ID 查询智能体详情。
|
||||
*/
|
||||
@GetMapping("/agent/{agentId}")
|
||||
public Result<OpenApiAgentVO> getAgent(
|
||||
@PathVariable Long agentId) {
|
||||
OpenApiAgentIdentityRequest request = new OpenApiAgentIdentityRequest();
|
||||
request.setAgentId(agentId);
|
||||
return agentClient.getAgent(request);
|
||||
}
|
||||
|
||||
// ==================== Conversation 相关接口 ====================
|
||||
|
||||
/**
|
||||
* 为指定智能体创建新会话。
|
||||
*/
|
||||
@PostMapping("/agent/{agentId}/conversation")
|
||||
public Result<OpenApiConversationVO> createConversation(
|
||||
@PathVariable Long agentId,
|
||||
@RequestBody OpenApiCreateConversationRequest request) {
|
||||
request.setAgentId(agentId);
|
||||
request.setOpenId(ensureOpenId());
|
||||
return conversationClient.createConversation(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询指定智能体下的会话列表。
|
||||
*/
|
||||
@GetMapping("/agent/{agentId}/conversations")
|
||||
public PageResult<List<OpenApiConversationVO>> 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<List<OpenApiMessageVO>> 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<Void> 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<Map<String, String>> getChatMode() {
|
||||
String mode = "sync".equalsIgnoreCase(chatMode) ? "sync" : "stream";
|
||||
return Result.ok(Map.of("mode", mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步对话接口。
|
||||
*/
|
||||
@PostMapping("/agent/{agentId}/chat/sync")
|
||||
public Result<OpenApiChatSyncResponse> 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<OpenApiUserVO> registerResult = userClient.register(registerRequest);
|
||||
if (registerResult == null || registerResult.getData() == null) {
|
||||
throw new SnailAiException("注册 OpenAPI 用户失败,返回为空");
|
||||
}
|
||||
OpenApiUserVO user = registerResult.getData();
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -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<OpenApiUserVO> registerCurrentUser() {
|
||||
return R.ok(ensureOpenApiUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前登录用户对应的 OpenAPI 用户信息。
|
||||
*/
|
||||
@GetMapping("/user")
|
||||
public R<OpenApiUserVO> getUser() {
|
||||
OpenApiUserQueryRequest request = new OpenApiUserQueryRequest();
|
||||
request.setOpenId(ensureOpenId());
|
||||
return toR(userClient.getUser(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户可访问的智能体列表。
|
||||
*/
|
||||
@GetMapping("/agents")
|
||||
public R<List<OpenApiAgentVO>> listAgents() {
|
||||
return toR(agentClient.listAgents());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据智能体 ID 查询智能体详情。
|
||||
*/
|
||||
@GetMapping("/agent/{agentId}")
|
||||
public R<OpenApiAgentVO> 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<OpenApiConversationVO> 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<PageResult<OpenApiConversationVO>> 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<List<OpenApiMessageVO>> 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<Void> 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<Map<String, String>> getChatMode() {
|
||||
String mode = "sync".equalsIgnoreCase(chatMode) ? "sync" : "stream";
|
||||
return R.ok(Map.of("mode", mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步对话接口。
|
||||
*/
|
||||
@PostMapping("/agent/{agentId}/chat/sync")
|
||||
public R<OpenApiChatSyncResponse> 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 <T> R<T> toR(Result<T> 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 <T> R<PageResult<T>> toPageR(com.aizuda.snail.ai.common.model.PageResult<List<T>> result) {
|
||||
R<List<T>> 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<OpenApiUserVO> 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();
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package org.dromara.ai;
|
||||
@@ -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, '用户管理菜单');
|
||||
|
||||
@@ -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, '用户管理菜单');
|
||||
|
||||
@@ -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', '');
|
||||
@@ -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, '用户管理菜单');
|
||||
|
||||
@@ -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'用户管理菜单');
|
||||
|
||||
Reference in New Issue
Block a user