diff --git a/pom.xml b/pom.xml index 6a291539c..b06ce5ba2 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ 2.2.7 4.5.0 2.0.0 - 0.0.4 + 0.0.5 1.5.0 0.2.0 1.18.42 @@ -55,8 +55,7 @@ 2.6.5 3.0.2 7.17.28 - - 2.0.0-M8 + 2.0.0 3.5.0 @@ -310,10 +309,17 @@ ${snailjob.version} - + com.aizuda - snail-ai-agent-starter + snail-ai-agent-chat-starter + ${snailai.version} + + + + + com.aizuda + snail-ai-agent-executor-starter ${snailai.version} diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 18045380d..bdd5c3f86 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -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 diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index 18d036f7e..cbecae3e4 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -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 diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 01963ea80..5ad4c6820 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -109,6 +109,8 @@ security: - /*/api-docs - /*/api-docs/** - /warm-flow-ui/config + - /snail-chat/** + - /api/snail/chat/** # MyBatisPlus配置 # https://baomidou.com/config/ diff --git a/ruoyi-common/ruoyi-common-ai/pom.xml b/ruoyi-common/ruoyi-common-ai/pom.xml index 0d3b4852b..6a1b4e037 100644 --- a/ruoyi-common/ruoyi-common-ai/pom.xml +++ b/ruoyi-common/ruoyi-common-ai/pom.xml @@ -23,10 +23,16 @@ ruoyi-common-core - + com.aizuda - snail-ai-agent-starter + snail-ai-agent-chat-starter + + + + + com.aizuda + snail-ai-agent-executor-starter diff --git a/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/handler/SnailAiChatExceptionHandler.java b/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/handler/SnailAiChatExceptionHandler.java new file mode 100644 index 000000000..1d93eda83 --- /dev/null +++ b/ruoyi-common/ruoyi-common-ai/src/main/java/org/dromara/common/ai/handler/SnailAiChatExceptionHandler.java @@ -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 网关异常处理器。 + *

+ * /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 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 handleSnailAiException(BaseSnailAiException e) { + log.warn("Snail AI Chat request failed: {}", e.getMessage()); + return Result.fail(defaultMessage(e, "AI 服务请求失败,请稍后再试")); + } + + @ExceptionHandler(ModelCallException.class) + public Result 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 handleValidationException(Exception e) { + log.warn("Snail AI Chat validation failed: {}", e.getMessage()); + return Result.fail(validationMessage(e)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public Result 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 handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.warn("Snail AI Chat request body parse failed: {}", e.getMessage()); + return Result.fail("请求参数格式错误"); + } + + @ExceptionHandler(IllegalArgumentException.class) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.warn("Snail AI Chat illegal argument: {}", e.getMessage()); + return Result.fail(defaultMessage(e, "请求参数不合法")); + } + + @ExceptionHandler(IllegalStateException.class) + public Result handleIllegalStateException(IllegalStateException e) { + log.warn("Snail AI Chat illegal state: {}", e.getMessage()); + return Result.fail(defaultMessage(e, "AI 会话状态异常,请刷新后重试")); + } + + @ExceptionHandler(Exception.class) + public Result handleException(Exception e) { + log.error("Snail AI Chat unexpected exception", e); + return Result.fail("AI 服务异常,请稍后再试"); + } + + private String validationMessage(Exception e) { + Collection 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(); + } +} diff --git a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java index d61f77b64..0c25bab94 100644 --- a/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java +++ b/ruoyi-modules/ruoyi-ai/src/main/java/org/dromara/ai/controller/SnailAiController.java @@ -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 getUser() { - OpenApiUserQueryRequest request = new OpenApiUserQueryRequest(); - request.setOpenId(ensureOpenId()); - return toR(userClient.getUser(request)); - } - - /** - * 查询当前用户可访问的智能体列表。 - */ - @GetMapping("/agents") - public R> listAgents() { - return toR(agentClient.listAgents()); - } - - /** - * 根据智能体 ID 查询智能体详情。 - */ - @GetMapping("/agent/{agentId}") - public R 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 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> 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> 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 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> getChatMode() { - String mode = "sync".equalsIgnoreCase(chatMode) ? "sync" : "stream"; - return R.ok(Map.of("mode", mode)); - } - - /** - * 同步对话接口。 - */ - @PostMapping("/agent/{agentId}/chat/sync") - public R 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 R toR(Result 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 R> toPageR(com.aizuda.snail.ai.common.model.PageResult> result) { - R> 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 用户。 */ diff --git a/script/docker/nginx/conf/nginx.conf b/script/docker/nginx/conf/nginx.conf index 871bf9ce0..ad70f5642 100644 --- a/script/docker/nginx/conf/nginx.conf +++ b/script/docker/nginx/conf/nginx.conf @@ -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; diff --git a/script/sql/oracle/oracle_ry_vue.sql b/script/sql/oracle/oracle_ry_vue.sql index a6be205b0..b911b1435 100644 --- a/script/sql/oracle/oracle_ry_vue.sql +++ b/script/sql/oracle/oracle_ry_vue.sql @@ -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, '操作日志菜单'); diff --git a/script/sql/postgres/postgres_ry_vue.sql b/script/sql/postgres/postgres_ry_vue.sql index 47dc9243c..f9d6a28ee 100644 --- a/script/sql/postgres/postgres_ry_vue.sql +++ b/script/sql/postgres/postgres_ry_vue.sql @@ -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, '操作日志菜单'); diff --git a/script/sql/ry_ai.sql b/script/sql/ry_ai.sql index 74a14a238..4a1e8b3d4 100644 --- a/script/sql/ry_ai.sql +++ b/script/sql/ry_ai.sql @@ -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()); diff --git a/script/sql/ry_vue.sql b/script/sql/ry_vue.sql index 716c814bc..9841276a6 100644 --- a/script/sql/ry_vue.sql +++ b/script/sql/ry_vue.sql @@ -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, '操作日志菜单'); diff --git a/script/sql/sqlserver/sqlserver_ry_vue.sql b/script/sql/sqlserver/sqlserver_ry_vue.sql index 3096f16dd..c77b125e8 100644 --- a/script/sql/sqlserver/sqlserver_ry_vue.sql +++ b/script/sql/sqlserver/sqlserver_ry_vue.sql @@ -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'登录日志菜单');