update snail-ai 0.0.5
This commit is contained in:
@@ -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.4</snailai.version>
|
||||
<snailai.version>0.0.5</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>
|
||||
@@ -55,8 +55,7 @@
|
||||
<mica-mqtt.version>2.6.5</mica-mqtt.version>
|
||||
<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-M8</spring-ai.version>
|
||||
<spring-ai.version>2.0.0</spring-ai.version>
|
||||
|
||||
<!-- 插件版本 -->
|
||||
<maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
|
||||
@@ -310,10 +309,17 @@
|
||||
<version>${snailjob.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Snail AI Agent 启动器 -->
|
||||
<!-- Snail AI Agent 聊天室 -->
|
||||
<dependency>
|
||||
<groupId>com.aizuda</groupId>
|
||||
<artifactId>snail-ai-agent-starter</artifactId>
|
||||
<artifactId>snail-ai-agent-chat-starter</artifactId>
|
||||
<version>${snailai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Snail AI Agent 执行器 -->
|
||||
<dependency>
|
||||
<groupId>com.aizuda</groupId>
|
||||
<artifactId>snail-ai-agent-executor-starter</artifactId>
|
||||
<version>${snailai.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ snail-job:
|
||||
snail-ai:
|
||||
# 启用客户端模式
|
||||
enabled: false
|
||||
# 聊天发送模式: stream(流式) / sync(同步)
|
||||
chat-mode: stream
|
||||
# ==================== Server 连接 ====================
|
||||
# Server 端 gRPC 地址(即 snail-ai-starter 的 snail-ai.server.grpc-port)
|
||||
server:
|
||||
@@ -44,7 +42,7 @@ snail-ai:
|
||||
# 应用 ID(在 Server「应用管理」页面创建后获取)
|
||||
app-id: 1
|
||||
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
|
||||
token: SAI_7c557fc05a304b0482a65ca67afdb0b8
|
||||
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
|
||||
port: 18889
|
||||
# Skill 文件临时目录
|
||||
skill-temp-dir: /tmp/snail-ai-agent/skills
|
||||
|
||||
@@ -35,8 +35,6 @@ snail-job:
|
||||
snail-ai:
|
||||
# 启用客户端模式
|
||||
enabled: false
|
||||
# 聊天发送模式: stream(流式) / sync(同步)
|
||||
chat-mode: stream
|
||||
# ==================== Server 连接 ====================
|
||||
# Server 端 gRPC 地址(即 snail-ai-starter 的 snail-ai.server.grpc-port)
|
||||
server:
|
||||
@@ -47,7 +45,7 @@ snail-ai:
|
||||
# 应用 ID(在 Server「应用管理」页面创建后获取)
|
||||
app-id: 1
|
||||
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
|
||||
token: SAI_7c557fc05a304b0482a65ca67afdb0b8
|
||||
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
|
||||
port: 18889
|
||||
# Skill 文件临时目录
|
||||
skill-temp-dir: /tmp/snail-ai-agent/skills
|
||||
|
||||
@@ -109,6 +109,8 @@ security:
|
||||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
- /snail-chat/**
|
||||
- /api/snail/chat/**
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
|
||||
@@ -23,10 +23,16 @@
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Snail AI Agent 启动器 -->
|
||||
<!-- Snail AI Agent 聊天室 -->
|
||||
<dependency>
|
||||
<groupId>com.aizuda</groupId>
|
||||
<artifactId>snail-ai-agent-starter</artifactId>
|
||||
<artifactId>snail-ai-agent-chat-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Snail AI Agent 执行器 -->
|
||||
<dependency>
|
||||
<groupId>com.aizuda</groupId>
|
||||
<artifactId>snail-ai-agent-executor-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Snail AI OpenAPI 启动器 -->
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package org.dromara.common.ai.handler;
|
||||
|
||||
import com.aizuda.snail.ai.agent.chat.starter.SnailAiChatGatewayController;
|
||||
import com.aizuda.snail.ai.common.execption.BaseSnailAiException;
|
||||
import com.aizuda.snail.ai.common.execption.SnailAiAuthenticationException;
|
||||
import com.aizuda.snail.ai.common.model.ModelCallException;
|
||||
import com.aizuda.snail.ai.common.model.Result;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Snail AI Chat 网关异常处理器。
|
||||
* <p>
|
||||
* /api/snail/chat/** 接口由 Snail AI SDK 前端消费,响应结构需要保持 SDK Result(status/message/data),
|
||||
* 不能落到 RuoYi 通用 R(code/msg/data) 格式。
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@RestControllerAdvice(assignableTypes = SnailAiChatGatewayController.class)
|
||||
public class SnailAiChatExceptionHandler {
|
||||
|
||||
private static final int AUTHENTICATION_ERROR_STATUS = 5001;
|
||||
|
||||
@ExceptionHandler(SnailAiAuthenticationException.class)
|
||||
public Result<Void> handleAuthenticationException(SnailAiAuthenticationException e) {
|
||||
log.warn("Snail AI Chat authentication failed: {}", e.getMessage());
|
||||
return Result.fail(AUTHENTICATION_ERROR_STATUS, defaultMessage(e, "认证失败,请重新登录"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(BaseSnailAiException.class)
|
||||
public Result<Void> handleSnailAiException(BaseSnailAiException e) {
|
||||
log.warn("Snail AI Chat request failed: {}", e.getMessage());
|
||||
return Result.fail(defaultMessage(e, "AI 服务请求失败,请稍后再试"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ModelCallException.class)
|
||||
public Result<Void> handleModelCallException(ModelCallException e) {
|
||||
log.warn("Snail AI Chat model call failed: {}", e.getMessage(), e);
|
||||
return Result.fail(defaultMessage(e, "模型调用失败,请稍后再试"));
|
||||
}
|
||||
|
||||
@ExceptionHandler({
|
||||
BindException.class,
|
||||
MethodArgumentNotValidException.class,
|
||||
HandlerMethodValidationException.class
|
||||
})
|
||||
public Result<Void> handleValidationException(Exception e) {
|
||||
log.warn("Snail AI Chat validation failed: {}", e.getMessage());
|
||||
return Result.fail(validationMessage(e));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
|
||||
log.warn("Snail AI Chat constraint validation failed: {}", e.getMessage());
|
||||
String message = e.getConstraintViolations().stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.filter(this::hasText)
|
||||
.collect(Collectors.joining(", "));
|
||||
return Result.fail(hasText(message) ? message : "请求参数校验失败");
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public Result<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
|
||||
log.warn("Snail AI Chat request body parse failed: {}", e.getMessage());
|
||||
return Result.fail("请求参数格式错误");
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public Result<Void> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
log.warn("Snail AI Chat illegal argument: {}", e.getMessage());
|
||||
return Result.fail(defaultMessage(e, "请求参数不合法"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
public Result<Void> handleIllegalStateException(IllegalStateException e) {
|
||||
log.warn("Snail AI Chat illegal state: {}", e.getMessage());
|
||||
return Result.fail(defaultMessage(e, "AI 会话状态异常,请刷新后重试"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<Void> handleException(Exception e) {
|
||||
log.error("Snail AI Chat unexpected exception", e);
|
||||
return Result.fail("AI 服务异常,请稍后再试");
|
||||
}
|
||||
|
||||
private String validationMessage(Exception e) {
|
||||
Collection<? extends MessageSourceResolvable> errors;
|
||||
if (e instanceof BindException bindException) {
|
||||
errors = bindException.getAllErrors();
|
||||
} else if (e instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
|
||||
errors = methodArgumentNotValidException.getBindingResult().getAllErrors();
|
||||
} else if (e instanceof HandlerMethodValidationException handlerMethodValidationException) {
|
||||
errors = handlerMethodValidationException.getAllErrors();
|
||||
} else {
|
||||
return "请求参数校验失败";
|
||||
}
|
||||
String message = errors.stream()
|
||||
.map(MessageSourceResolvable::getDefaultMessage)
|
||||
.filter(this::hasText)
|
||||
.collect(Collectors.joining(", "));
|
||||
return hasText(message) ? message : "请求参数校验失败";
|
||||
}
|
||||
|
||||
private String defaultMessage(Throwable e, String fallback) {
|
||||
return hasText(e.getMessage()) ? e.getMessage() : fallback;
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
}
|
||||
+5
-298
@@ -2,33 +2,19 @@ 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.*;
|
||||
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.common.openapi.dto.OpenApiUserRegisterRequest;
|
||||
import com.aizuda.snail.ai.common.openapi.dto.OpenApiUserVO;
|
||||
import com.aizuda.snail.ai.openapi.client.core.api.OpenApiUserClient;
|
||||
import com.aizuda.snail.ai.openapi.client.core.listener.SseEventListener;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.*;
|
||||
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;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Snail AI OpenAPI 控制器
|
||||
@@ -47,20 +33,7 @@ public class SnailAiController extends BaseController {
|
||||
* Snail AI 成功状态码。
|
||||
*/
|
||||
private static final int SNAIL_AI_SUCCESS = 1;
|
||||
/**
|
||||
* SSE 超时时间。
|
||||
*/
|
||||
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 用户信息。
|
||||
@@ -70,272 +43,6 @@ public class SnailAiController extends BaseController {
|
||||
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,
|
||||
HttpServletResponse response) {
|
||||
prepareSseResponse(response);
|
||||
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() {
|
||||
/**
|
||||
* 推送普通文本片段。
|
||||
*
|
||||
* @param text 文本片段
|
||||
*/
|
||||
@Override
|
||||
public void onText(String text) {
|
||||
safeSend(emitter, closed, "text", text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送思考内容片段。
|
||||
*
|
||||
* @param thinking 思考内容
|
||||
*/
|
||||
@Override
|
||||
public void onThinking(String thinking) {
|
||||
safeSend(emitter, closed, "thinking", thinking);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流式对话完成事件。
|
||||
*
|
||||
* @param data 完成数据
|
||||
*/
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流式对话异常事件。
|
||||
*
|
||||
* @param errorMessage 错误信息
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 SSE 响应头,覆盖统一鉴权成功路径中的默认 JSON 响应类型。
|
||||
*
|
||||
* @param response 当前响应
|
||||
*/
|
||||
private void prepareSseResponse(HttpServletResponse response) {
|
||||
response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("X-Accel-Buffering", "no");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户对应的 openId,不存在时会自动注册。
|
||||
*/
|
||||
private String ensureOpenId() {
|
||||
return ensureOpenApiUser().getOpenId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保当前登录用户已注册为 OpenAPI 用户。
|
||||
*/
|
||||
|
||||
@@ -49,6 +49,10 @@ http {
|
||||
server 127.0.0.1:8800;
|
||||
}
|
||||
|
||||
upstream snailai-server {
|
||||
server 127.0.0.1:8900;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
@@ -147,6 +151,28 @@ http {
|
||||
proxy_pass http://snailjob-server/snail-job/;
|
||||
}
|
||||
|
||||
location /snail-ai/ {
|
||||
# 设置客户端请求头中的 Host 信息(保持原始 Host)
|
||||
proxy_set_header Host $http_host;
|
||||
# 获取客户端真实 IP
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# 自定义头 REMOTE-HOST,记录客户端 IP
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# 获取完整的客户端 IP 链(经过多级代理时)
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# 设置后端响应超时时间(这里是 24 小时,适合长连接/SSE)
|
||||
proxy_read_timeout 86400s;
|
||||
# SSE (Server-Sent Events) 与 WebSocket 支持参数
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
# 禁用代理缓冲,直接传输给客户端
|
||||
proxy_buffering off;
|
||||
# 禁用代理缓存
|
||||
proxy_cache off;
|
||||
proxy_pass http://snailai-server/snail-ai/;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
|
||||
@@ -375,6 +375,8 @@ insert into sys_menu values(1761400000000000117, 'Admin监控', 1761400000000000
|
||||
insert into sys_menu values(1761400000000000118, '文件管理', 1761400000000000001, 10, 'oss', 'system/oss/index', '', 'N', 'Y', 'C', '0', '0', 'system:oss:list', 'upload', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, '文件管理菜单');
|
||||
-- snail-job server控制台
|
||||
insert into sys_menu values(1761400000000000120, '任务调度中心', 1761400000000000002, 5, 'snailjob', 'monitor/snailjob/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:snailjob:list', 'job', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, 'snailjob控制台菜单');
|
||||
-- snail-ai server控制台
|
||||
insert into sys_menu values(1761400000000000121, 'AI控制台', 1761400000000000002, 7, 'snailai', 'monitor/snailai/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:snailai:list', 'checkbox', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, 'AI控制台菜单');
|
||||
|
||||
-- 三级菜单
|
||||
insert into sys_menu values(1761400000000000500, '操作日志', 1761400000000000108, 1, 'operlog', 'monitor/operlog/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:operlog:list', 'form', '', '', 1761000000000000103, 1761100000000000001, sysdate, null, null, '操作日志菜单');
|
||||
|
||||
@@ -372,6 +372,8 @@ insert into sys_menu values(1761400000000000117, 'Admin监控', 1761400000000000
|
||||
insert into sys_menu values(1761400000000000118, '文件管理', 1761400000000000001, 10, 'oss', 'system/oss/index', '', 'N', 'Y', 'C', '0', '0', 'system:oss:list', 'upload', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, '文件管理菜单');
|
||||
-- snail-job server控制台
|
||||
insert into sys_menu values(1761400000000000120, '任务调度中心', 1761400000000000002, 6, 'snailjob', 'monitor/snailjob/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:snailjob:list', 'job', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, 'SnailJob控制台菜单');
|
||||
-- snail-ai server控制台
|
||||
insert into sys_menu values(1761400000000000121, 'AI控制台', 1761400000000000002, 7, 'snailai', 'monitor/snailai/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:snailai:list', 'checkbox', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, 'AI控制台菜单');
|
||||
|
||||
-- 三级菜单
|
||||
insert into sys_menu values(1761400000000000500, '操作日志', 1761400000000000108, 1, 'operlog', 'monitor/operlog/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:operlog:list', 'form', '', '', 1761000000000000103, 1761100000000000001, now(), null, null, '操作日志菜单');
|
||||
|
||||
+29
-32
@@ -97,11 +97,6 @@ CREATE TABLE sai_user
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4;
|
||||
|
||||
-- 默认管理员:admin / admin123
|
||||
INSERT INTO sai_user (id, role, username, email, password, create_dt, update_dt)
|
||||
VALUES (1, 2, 'admin', '', 'pbkdf2$120000$c25haWwtYWktYWRtaW4tMQ==$kakglT/wYKOgv/77Ah1stie58d/JbY2nGgq5DwgUBw4=',
|
||||
'2026-02-11 13:56:48.210429', '2026-02-11 13:56:48.210429');
|
||||
|
||||
-- ============================================
|
||||
-- 1. AI 模型提供商表
|
||||
-- ============================================
|
||||
@@ -134,6 +129,7 @@ CREATE TABLE IF NOT EXISTS sai_model_config
|
||||
model_name VARCHAR(255) NOT NULL COMMENT '模型名称',
|
||||
model_key VARCHAR(100) NOT NULL COMMENT '模型标识符',
|
||||
model_type VARCHAR(50) NOT NULL COMMENT '模型类型(CHAT/EMBEDDING/RERANKER/IMAGE/SPEECH)',
|
||||
adapter_key VARCHAR(100) COMMENT '底层协议适配器标识(openai-compatible/http等)',
|
||||
description VARCHAR(1000) COMMENT '模型描述',
|
||||
api_key VARCHAR(1000) COMMENT 'API密钥(加密存储)',
|
||||
api_endpoint VARCHAR(500) COMMENT 'API端点URL',
|
||||
@@ -156,9 +152,6 @@ CREATE INDEX idx_is_default ON sai_model_config (is_default);
|
||||
CREATE INDEX idx_scope ON sai_model_config (scope);
|
||||
CREATE INDEX idx_model_key ON sai_model_config (model_key);
|
||||
|
||||
INSERT INTO sai_model_config VALUES (1, 5, 'glm-5.1', 'glm-5.1', 'CHAT', '', 'xxx', 'https://dashscope.aliyuncs.com/compatible-mode/v1', '{\"frequencyPenalty\":0.0,\"maxTokens\":20000,\"presencePenalty\":0.0,\"stopSequences\":[],\"stream\":true,\"temperature\":0.7,\"timeoutMs\":300000,\"topK\":1,\"topP\":1.0}', NULL, 'GLOBAL', 1, 1, '2026-06-05 09:34:01', '2026-06-05 09:34:02');
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 3. 模型使用统计表
|
||||
-- ============================================
|
||||
@@ -186,19 +179,6 @@ CREATE INDEX idx_model_id ON sai_model_usage_stat (model_id);
|
||||
CREATE INDEX idx_user_id ON sai_model_usage_stat (user_id);
|
||||
CREATE INDEX idx_last_used_dt ON sai_model_usage_stat (last_used_dt);
|
||||
|
||||
-- ============================================
|
||||
-- 初始化数据 (可选)
|
||||
-- ============================================
|
||||
-- 插入常见的AI提供商(重复 provider_key 则忽略)
|
||||
INSERT IGNORE INTO sai_model_provider (provider_name, provider_key, description, is_enabled)
|
||||
VALUES ('OpenAI', 'openai', 'OpenAI官方模型 (GPT-4, GPT-3.5等)', 1),
|
||||
('Claude', 'claude', 'Anthropic Claude模型', 1),
|
||||
('Ollama', 'ollama', '本地开源模型 (Llama, Mistral等)', 1),
|
||||
('Google Gemini', 'gemini', 'Google Gemini模型', 1),
|
||||
('阿里云百炼', 'qwen', '阿里云百炼 OpenAI 兼容模型 (Qwen等)', 1),
|
||||
('DeepSeek', 'deepseek', 'DeepSeek OpenAI 兼容模型', 1),
|
||||
('智谱AI', 'zhipu', '智谱AI OpenAI 兼容模型 (GLM等)', 1);
|
||||
|
||||
-- ============================================
|
||||
-- 智能体相关表
|
||||
-- ============================================
|
||||
@@ -237,9 +217,6 @@ CREATE TABLE IF NOT EXISTS sai_agent
|
||||
CREATE INDEX idx_agent_creator ON sai_agent (creator_id);
|
||||
CREATE INDEX idx_agent_featured ON sai_agent (is_featured);
|
||||
|
||||
INSERT INTO sai_agent VALUES (1, '智测先锋专家', '智测先锋专家是一款专注于软件测试与质量保障领域的智能助手。它能够高效生成覆盖全面的测试用例,深度分析Bug根因并提供修复建议,支持编写自动化测试脚本,以及解读复杂的测试报告。适用于软件开发周期的各个QA阶段,包括单元测试、接口测试、UI自动化及回归测试规划。其核心特点是逻辑严密、注重边界与异常场景,帮助团队大幅提升测试效率与软件质量。', NULL, '你是一位资深的软件测试与质量保障(QA)专家,名为“智测先锋专家”。\\n\\n【角色定位】你是开发团队的最后一道防线,致力于保障软件产品的卓越质量。\\n\\n【专业领域】精通黑盒与白盒测试、自动化测试框架(如Selenium、Pytest)、接口与性能测试、安全测试及CI/CD持续集成流程。\\n\\n【回答风格】逻辑严密、条理清晰、客观专业。善于使用结构化排版(如Markdown列表、代码块、表格)呈现测试用例和步骤,语言精炼,直击痛点。\\n\\n【行为指南】\\n1. 生成测试用例:必须覆盖正常流、异常流、边界值和兼容性等方面,确保测试的全面性与无遗漏。\\n2. 分析Bug根因:从代码逻辑、数据状态、环境配置等多维度推导,不仅给出修复建议,更要提供预防性的测试策略。\\n3. 编写自动化脚本:确保代码规范、包含必要注释与断言(Assert),并明确说明运行依赖与环境配置。\\n4. 需求澄清:若用户提问模糊,主动追问业务背景、技术栈等关键细节,拒绝给出宽泛且无实操价值的答案。\\n5. 风险预警:始终秉持质量第一理念,在解答中适时提示潜在的测试盲区与质量风险。', '你好!我是智测先锋专家,你的专属软件测试与质量保障顾问。无论是编写用例还是排查Bug,我都能为你提供专业支持!', '[\"如何为一个用户登录接口设计全面的测试用例?\",\"帮我分析这个空指针异常Bug的可能根因及修复建议。\",\"请提供一段Python的Pytest接口自动化测试脚本示例。\",\"怎样制定一个高效的回归测试策略?\"]', 2, 0, 0, 0, 0, 0, NULL, 1, 20, 1, 0, 1, 1, NULL, '1', '2026-06-05 09:34:50', '2026-06-05 09:34:51');
|
||||
|
||||
|
||||
-- 智能体对话表
|
||||
CREATE TABLE IF NOT EXISTS sai_agent_conversation
|
||||
(
|
||||
@@ -436,11 +413,6 @@ CREATE TABLE IF NOT EXISTS sai_store_instance
|
||||
CREATE INDEX idx_store_instance_category ON sai_store_instance (category);
|
||||
CREATE INDEX idx_store_instance_type ON sai_store_instance (type);
|
||||
|
||||
-- ============================================================
|
||||
-- 记忆系统(配置 / 主表 / 历史 / 摘要 / 统计 / 提取进度)
|
||||
-- 依赖:sai_store_instance(sai_memory_conversation 外键)
|
||||
-- ============================================================
|
||||
|
||||
-- 客户端应用
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS sai_app
|
||||
@@ -459,9 +431,6 @@ CREATE TABLE IF NOT EXISTS sai_app
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT = '客户端应用';
|
||||
|
||||
INSERT INTO sai_app VALUES (1, '1', '测试', '', 'SAI_566a6bfbc26e4998b4841cc927d50c5d', 'LEAST_LOAD', 1, '2026-06-05 09:32:50', '2026-06-05 09:32:50');
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- AI客户端实例节点
|
||||
-- ----------------------------
|
||||
@@ -503,6 +472,7 @@ CREATE TABLE sai_openapi_user
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='OpenAPI 外部用户映射表';
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- 通用资源存储
|
||||
-- ----------------------------
|
||||
@@ -526,3 +496,30 @@ CREATE TABLE IF NOT EXISTS sai_resource
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT = '通用资源存储';
|
||||
|
||||
-- ============================================
|
||||
-- 初始化数据
|
||||
-- ============================================
|
||||
|
||||
-- 默认管理员:admin / admin123
|
||||
INSERT INTO sai_user (id, role, username, email, password, create_dt, update_dt)
|
||||
VALUES (1, 2, 'admin', '', 'pbkdf2$120000$c25haWwtYWktYWRtaW4tMQ==$kakglT/wYKOgv/77Ah1stie58d/JbY2nGgq5DwgUBw4=',
|
||||
'2026-02-11 13:56:48.210429', '2026-02-11 13:56:48.210429');
|
||||
|
||||
-- 插入常见的AI提供商
|
||||
INSERT INTO sai_model_provider (provider_name, provider_key, description, is_enabled)
|
||||
VALUES ('OpenAI', 'openai', 'OpenAI官方模型 (GPT-4, GPT-3.5等)', 1),
|
||||
('Claude', 'claude', 'Anthropic Claude模型', 1),
|
||||
('Ollama', 'ollama', '本地开源模型 (Llama, Mistral等)', 1),
|
||||
('Google Gemini', 'gemini', 'Google Gemini模型', 1),
|
||||
('阿里云百炼', 'qwen', '阿里云百炼 OpenAI 兼容模型 (Qwen等)', 1),
|
||||
('DeepSeek', 'deepseek', 'DeepSeek OpenAI 兼容模型', 1),
|
||||
('智谱AI', 'zhipu', '智谱AI OpenAI 兼容模型 (GLM等)', 1);
|
||||
|
||||
INSERT INTO sai_model_config VALUES (1, 5, 'glm-5.1', 'glm-5.1', 'CHAT', 'openai-compatible', '', 'xxx', 'https://dashscope.aliyuncs.com/compatible-mode/v1', '{\"frequencyPenalty\":0.0,\"maxTokens\":20000,\"presencePenalty\":0.0,\"stopSequences\":[],\"stream\":true,\"temperature\":0.7,\"timeoutMs\":300000,\"topK\":1,\"topP\":1.0}', NULL, 'GLOBAL', 1, 1, '2026-06-05 09:34:01', '2026-06-05 09:34:02');
|
||||
|
||||
INSERT INTO sai_agent VALUES (1, '智测先锋专家', '智测先锋专家是一款专注于软件测试与质量保障领域的智能助手。它能够高效生成覆盖全面的测试用例,深度分析Bug根因并提供修复建议,支持编写自动化测试脚本,以及解读复杂的测试报告。适用于软件开发周期的各个QA阶段,包括单元测试、接口测试、UI自动化及回归测试规划。其核心特点是逻辑严密、注重边界与异常场景,帮助团队大幅提升测试效率与软件质量。', NULL, '你是一位资深的软件测试与质量保障(QA)专家,名为“智测先锋专家”。\\n\\n【角色定位】你是开发团队的最后一道防线,致力于保障软件产品的卓越质量。\\n\\n【专业领域】精通黑盒与白盒测试、自动化测试框架(如Selenium、Pytest)、接口与性能测试、安全测试及CI/CD持续集成流程。\\n\\n【回答风格】逻辑严密、条理清晰、客观专业。善于使用结构化排版(如Markdown列表、代码块、表格)呈现测试用例和步骤,语言精炼,直击痛点。\\n\\n【行为指南】\\n1. 生成测试用例:必须覆盖正常流、异常流、边界值和兼容性等方面,确保测试的全面性与无遗漏。\\n2. 分析Bug根因:从代码逻辑、数据状态、环境配置等多维度推导,不仅给出修复建议,更要提供预防性的测试策略。\\n3. 编写自动化脚本:确保代码规范、包含必要注释与断言(Assert),并明确说明运行依赖与环境配置。\\n4. 需求澄清:若用户提问模糊,主动追问业务背景、技术栈等关键细节,拒绝给出宽泛且无实操价值的答案。\\n5. 风险预警:始终秉持质量第一理念,在解答中适时提示潜在的测试盲区与质量风险。', '你好!我是智测先锋专家,你的专属软件测试与质量保障顾问。无论是编写用例还是排查Bug,我都能为你提供专业支持!', '[\"如何为一个用户登录接口设计全面的测试用例?\",\"帮我分析这个空指针异常Bug的可能根因及修复建议。\",\"请提供一段Python的Pytest接口自动化测试脚本示例。\",\"怎样制定一个高效的回归测试策略?\"]', 2, 0, 0, 0, 0, 0, NULL, 1, 20, 1, 0, 1, 1, NULL, '1', '2026-06-05 09:34:50', '2026-06-05 09:34:51');
|
||||
|
||||
INSERT INTO sai_app VALUES (1, '1', '测试', '', 'SAI_566a6bfbc26e4998b4841cc927d50c5d', 'LEAST_LOAD', 1, '2026-06-05 09:32:50', '2026-06-05 09:32:50');
|
||||
|
||||
INSERT INTO sai_openapi_user VALUES (1, '1', '46ed53c6a20044c7bbd870848e80f92f', 1, '1', 'test', NOW(), NOW());
|
||||
|
||||
@@ -240,6 +240,8 @@ insert into sys_menu values(1761400000000000117, 'Admin监控', 1761400000000000
|
||||
insert into sys_menu values(1761400000000000118, '文件管理', 1761400000000000001, 10, 'oss', 'system/oss/index', '', 'N', 'Y', 'C', '0', '0', 'system:oss:list', 'upload', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '文件管理菜单');
|
||||
-- snail-job server控制台
|
||||
insert into sys_menu values(1761400000000000120, '任务调度中心', 1761400000000000002, 6, 'snailjob', 'monitor/snailjob/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:snailjob:list', 'job', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, 'SnailJob控制台菜单');
|
||||
-- snail-ai server控制台
|
||||
insert into sys_menu values(1761400000000000121, 'AI控制台', 1761400000000000002, 7, 'snailai', 'monitor/snailai/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:snailai:list', 'checkbox', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, 'AI控制台菜单');
|
||||
|
||||
-- 三级菜单
|
||||
insert into sys_menu values(1761400000000000500, '操作日志', 1761400000000000108, 1, 'operlog', 'monitor/operlog/index', '', 'N', 'Y', 'C', '0', '0', 'monitor:operlog:list', 'form', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '操作日志菜单');
|
||||
|
||||
@@ -1416,6 +1416,8 @@ insert into sys_menu values(1761400000000000118, N'文件管理', 17614000000000
|
||||
GO
|
||||
insert into sys_menu values(1761400000000000120, N'任务调度中心', 1761400000000000002, 5, N'snailjob', N'monitor/snailjob/index', N'', N'N', N'Y', N'C', N'0', N'0', N'monitor:snailjob:list', N'job', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), NULL, NULL, N'SnailJob控制台菜单');
|
||||
GO
|
||||
insert into sys_menu values(1761400000000000121, N'AI控制台', 1761400000000000002, 7, N'snailai', N'monitor/snailai/index', N'', N'N', N'Y', N'C', N'0', N'0', N'monitor:snailai:list', N'checkbox', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), NULL, NULL, N'AI控制台菜单');
|
||||
GO
|
||||
insert into sys_menu values(1761400000000000500, N'操作日志', 1761400000000000108, 1, N'operlog', N'monitor/operlog/index', N'', N'N', N'Y', N'C', N'0', N'0', N'monitor:operlog:list', N'form', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), NULL, NULL, N'操作日志菜单');
|
||||
GO
|
||||
insert into sys_menu values(1761400000000000501, N'登录日志', 1761400000000000108, 2, N'logininfo', N'monitor/logininfo/index', N'', N'N', N'Y', N'C', N'0', N'0', N'monitor:logininfo:list', N'logininfo', N'', N'', 1761000000000000103, 1761100000000000001, getdate(), NULL, NULL, N'登录日志菜单');
|
||||
|
||||
Reference in New Issue
Block a user