From 95412501fc69777ad7db6fef970b479c9651984d Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Mon, 10 Nov 2025 08:03:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E6=96=B0=E5=A2=9EAI=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../boot/platform/ai/config/AiProperties.java | 113 ++++++++ .../ai/controller/AiCommandController.java | 99 +++++++ .../ai/mapper/AiCommandAuditMapper.java | 20 ++ .../ai/model/dto/AiCommandRequestDTO.java | 37 +++ .../ai/model/dto/AiCommandResponseDTO.java | 53 ++++ .../ai/model/dto/AiExecuteRequestDTO.java | 36 +++ .../ai/model/dto/AiExecuteResponseDTO.java | 62 +++++ .../ai/model/dto/FunctionCallDTO.java | 38 +++ .../ai/model/entity/AiCommandAudit.java | 121 ++++++++ .../AbstractOpenAiCompatibleProvider.java | 101 +++++++ .../boot/platform/ai/provider/AiProvider.java | 32 +++ .../ai/provider/AiProviderFactory.java | 51 ++++ .../ai/provider/impl/DeepSeekProvider.java | 25 ++ .../ai/provider/impl/OpenAiProvider.java | 30 ++ .../ai/provider/impl/QwenProvider.java | 25 ++ .../platform/ai/service/AiCommandService.java | 63 +++++ .../ai/service/impl/AiCommandServiceImpl.java | 262 ++++++++++++++++++ src/main/resources/application-dev.yml | 85 +++++- src/main/resources/application-prod.yml | 66 ++++- 19 files changed, 1307 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/youlai/boot/platform/ai/config/AiProperties.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandAuditMapper.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandRequestDTO.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandResponseDTO.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/dto/FunctionCallDTO.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandAudit.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/provider/AbstractOpenAiCompatibleProvider.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/provider/AiProvider.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/provider/AiProviderFactory.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/provider/impl/DeepSeekProvider.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/provider/impl/OpenAiProvider.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/provider/impl/QwenProvider.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java create mode 100644 src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java diff --git a/src/main/java/com/youlai/boot/platform/ai/config/AiProperties.java b/src/main/java/com/youlai/boot/platform/ai/config/AiProperties.java new file mode 100644 index 00000000..69d19a02 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/config/AiProperties.java @@ -0,0 +1,113 @@ +package com.youlai.boot.platform.ai.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * AI 配置属性 + * + * 优势: + * 1. 统一管理所有提供商配置 + * 2. 添加新提供商只需在 yml 中添加配置,无需修改代码 + * 3. 类型安全,支持 IDE 提示 + * + * @author Ray.Hao + */ +@Data +@Component +@ConfigurationProperties(prefix = "ai") +public class AiProperties { + + /** + * 是否启用 AI 功能 + */ + private Boolean enabled = false; + + /** + * 当前使用的提供商(qwen、deepseek、openai 等) + */ + private String provider = "qwen"; + + /** + * 所有提供商的配置 + * Key: 提供商名称(qwen、deepseek、openai) + * Value: 提供商配置 + */ + private Map providers; + + /** + * 安全配置 + */ + private SecurityConfig security = new SecurityConfig(); + + /** + * 限流配置 + */ + private RateLimitConfig rateLimit = new RateLimitConfig(); + + /** + * 提供商配置 + */ + @Data + public static class ProviderConfig { + /** + * API Key + */ + private String apiKey; + + /** + * Base URL(统一命名,符合行业惯例) + */ + private String baseUrl; + + /** + * 模型名称 + */ + private String model; + + /** + * 提供商显示名称(可选) + */ + private String displayName; + + /** + * 超时时间(秒) + */ + private Integer timeout = 30; + } + + /** + * 安全配置 + */ + @Data + public static class SecurityConfig { + private Boolean enableAudit = true; + private Boolean dangerousOperationsConfirm = true; + private java.util.List functionWhitelist; + private java.util.List sensitiveParams; + } + + /** + * 限流配置 + */ + @Data + public static class RateLimitConfig { + private Integer maxExecutionsPerMinute = 10; + private Integer maxExecutionsPerDay = 100; + } + + /** + * 获取当前提供商配置 + */ + public ProviderConfig getCurrentProviderConfig() { + if (providers == null || !providers.containsKey(provider)) { + throw new IllegalStateException("未找到提供商配置: " + provider); + } + return providers.get(provider); + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java new file mode 100644 index 00000000..3597fdca --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java @@ -0,0 +1,99 @@ +package com.youlai.boot.platform.ai.controller; + +import com.youlai.boot.core.web.Result; +import com.youlai.boot.platform.ai.model.dto.*; +import com.youlai.boot.platform.ai.service.AiCommandService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + + +/** + * AI 命令控制器 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Tag(name = "AI命令接口") +@RestController +@RequestMapping("/api/v1/ai/command") +@RequiredArgsConstructor +@Slf4j +public class AiCommandController { + + private final AiCommandService aiCommandService; + + @Operation(summary = "解析自然语言命令") + @PostMapping("/parse") + public Result parseCommand( + @RequestBody AiCommandRequestDTO request, + HttpServletRequest httpRequest + ) { + log.info("收到AI命令解析请求: {}", request.getCommand()); + + try { + AiCommandResponseDTO response = aiCommandService.parseCommand(request, httpRequest); + return Result.success(response); + } catch (Exception e) { + log.error("命令解析失败", e); + return Result.success(AiCommandResponseDTO.builder() + .success(false) + .error(e.getMessage()) + .build()); + } + } + + @Operation(summary = "执行已解析的命令") + @PostMapping("/execute") + public Result executeCommand( + @RequestBody AiExecuteRequestDTO request, + HttpServletRequest httpRequest + ) { + log.info("收到AI命令执行请求: {}", request.getFunctionCall().getName()); + + try { + AiExecuteResponseDTO response = aiCommandService.executeCommand(request, httpRequest); + return Result.success(response); + } catch (Exception e) { + log.error("命令执行失败", e); + return Result.success(AiExecuteResponseDTO.builder() + .success(false) + .error(e.getMessage()) + .build()); + } + } + + @Operation(summary = "获取命令执行历史") + @GetMapping("/history") + public Result getCommandHistory( + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, + @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size + ) { + return Result.success(aiCommandService.getCommandHistory(page, size)); + } + + @Operation(summary = "获取可用的函数列表") + @GetMapping("/functions") + public Result getAvailableFunctions() { + return Result.success(aiCommandService.getAvailableFunctions()); + } + + @Operation(summary = "撤销命令执行") + @PostMapping("/rollback/{auditId}") + public Result rollbackCommand( + @Parameter(description = "审计ID") @PathVariable String auditId + ) { + aiCommandService.rollbackCommand(auditId); + return Result.success("撤销成功"); + } +} + + + + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandAuditMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandAuditMapper.java new file mode 100644 index 00000000..c3fe86e5 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandAuditMapper.java @@ -0,0 +1,20 @@ +package com.youlai.boot.platform.ai.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.youlai.boot.platform.ai.model.entity.AiCommandAudit; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI 命令审计 Mapper + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Mapper +public interface AiCommandAuditMapper extends BaseMapper { +} + + + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandRequestDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandRequestDTO.java new file mode 100644 index 00000000..33b8a811 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandRequestDTO.java @@ -0,0 +1,37 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.Data; +import java.util.Map; + +/** + * AI 命令请求 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +public class AiCommandRequestDTO { + + /** + * 用户输入的自然语言命令 + */ + private String command; + + /** + * 当前页面路由(用于上下文) + */ + private String currentRoute; + + /** + * 当前激活的组件名称 + */ + private String currentComponent; + + /** + * 额外上下文信息 + */ + private Map context; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandResponseDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandResponseDTO.java new file mode 100644 index 00000000..4331b05b --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandResponseDTO.java @@ -0,0 +1,53 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +/** + * AI 命令解析响应 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiCommandResponseDTO { + + /** + * 是否成功解析 + */ + private Boolean success; + + /** + * 解析后的函数调用列表 + */ + private List functionCalls; + + /** + * AI 的理解和说明 + */ + private String explanation; + + /** + * 置信度 (0-1) + */ + private Double confidence; + + /** + * 错误信息 + */ + private String error; + + /** + * 原始 LLM 响应(用于调试) + */ + private String rawResponse; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java new file mode 100644 index 00000000..f091d99b --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java @@ -0,0 +1,36 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.Data; + +/** + * AI 命令执行请求 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +public class AiExecuteRequestDTO { + + /** + * 要执行的函数调用 + */ + private FunctionCallDTO functionCall; + + /** + * 确认模式:auto=自动执行, manual=需要用户确认 + */ + private String confirmMode; + + /** + * 用户确认标志 + */ + private Boolean userConfirmed; + + /** + * 幂等性令牌(防止重复执行) + */ + private String idempotencyKey; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java new file mode 100644 index 00000000..279126d6 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java @@ -0,0 +1,62 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * AI 命令执行响应 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiExecuteResponseDTO { + + /** + * 是否执行成功 + */ + private Boolean success; + + /** + * 执行结果数据 + */ + private Object data; + + /** + * 执行结果说明 + */ + private String message; + + /** + * 影响的记录数 + */ + private Integer affectedRows; + + /** + * 错误信息 + */ + private String error; + + /** + * 审计ID(用于追踪) + */ + private String auditId; + + /** + * 需要用户确认 + */ + private Boolean requiresConfirmation; + + /** + * 确认提示信息 + */ + private String confirmationPrompt; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/FunctionCallDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/FunctionCallDTO.java new file mode 100644 index 00000000..8e08eb2b --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/FunctionCallDTO.java @@ -0,0 +1,38 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.Map; + +/** + * 函数调用 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FunctionCallDTO { + + /** + * 函数名称 + */ + private String name; + + /** + * 函数描述 + */ + private String description; + + /** + * 参数对象 + */ + private Map arguments; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandAudit.java b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandAudit.java new file mode 100644 index 00000000..9746f349 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandAudit.java @@ -0,0 +1,121 @@ +package com.youlai.boot.platform.ai.model.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * AI 命令审计记录 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@TableName("ai_command_audit") +public class AiCommandAudit { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 原始命令 + */ + private String originalCommand; + + /** + * 解析后的函数名称 + */ + private String functionName; + + /** + * 函数参数(JSON) + */ + private String functionArguments; + + /** + * 执行状态:pending, success, failed + */ + private String executeStatus; + + /** + * 执行结果(JSON) + */ + private String executeResult; + + /** + * 错误信息 + */ + private String errorMessage; + + /** + * 影响的记录数 + */ + private Integer affectedRows; + + /** + * 是否危险操作 + */ + private Boolean isDangerous; + + /** + * 是否需要确认 + */ + private Boolean requiresConfirmation; + + /** + * 用户是否确认 + */ + private Boolean userConfirmed; + + /** + * 幂等性令牌 + */ + private String idempotencyKey; + + /** + * IP 地址 + */ + private String ipAddress; + + /** + * 用户代理 + */ + private String userAgent; + + /** + * 当前路由 + */ + private String currentRoute; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 执行时间(毫秒) + */ + private Long executionTime; + + /** + * 备注 + */ + private String remark; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/AbstractOpenAiCompatibleProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/AbstractOpenAiCompatibleProvider.java new file mode 100644 index 00000000..bf7ca322 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/AbstractOpenAiCompatibleProvider.java @@ -0,0 +1,101 @@ +package com.youlai.boot.platform.ai.provider; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.youlai.boot.platform.ai.config.AiProperties; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * OpenAI 兼容协议的抽象提供商 + * + * 适用于:通义千问、DeepSeek、OpenAI、ChatGLM 等兼容 OpenAI API 的模型 + * + * @author Ray.Hao + */ +@Slf4j +public abstract class AbstractOpenAiCompatibleProvider implements AiProvider { + + protected final AiProperties.ProviderConfig config; + + public AbstractOpenAiCompatibleProvider(AiProperties.ProviderConfig config) { + this.config = config; + } + + @Override + public String call(String systemPrompt, String userPrompt) { + if (!isConfigValid()) { + throw new IllegalStateException(getProviderName() + " 配置无效"); + } + + try { + // 构建请求体(OpenAI 标准格式) + JSONObject requestBody = JSONUtil.createObj() + .set("model", config.getModel()) + .set("messages", JSONUtil.createArray() + .put(JSONUtil.createObj() + .set("role", "system") + .set("content", systemPrompt)) + .put(JSONUtil.createObj() + .set("role", "user") + .set("content", userPrompt)) + ) + .set("temperature", 0.7); + + log.info("📤 调用 {} API: {}/chat/completions", getProviderName(), config.getBaseUrl()); + log.debug("请求参数: {}", requestBody); + + // 发送 HTTP 请求 + HttpResponse response = HttpRequest.post(config.getBaseUrl() + "/chat/completions") + .header("Authorization", "Bearer " + config.getApiKey()) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .timeout((int) TimeUnit.SECONDS.toMillis(config.getTimeout())) + .execute(); + + // 检查响应状态 + if (!response.isOk()) { + String errorMsg = String.format("%s API 调用失败: HTTP %d - %s", + getProviderName(), response.getStatus(), response.body()); + log.error(errorMsg); + throw new RuntimeException(errorMsg); + } + + // 解析响应 + JSONObject responseJson = JSONUtil.parseObj(response.body()); + String content = responseJson.getByPath("choices[0].message.content", String.class); + + // 记录 Token 使用情况 + JSONObject usage = responseJson.getJSONObject("usage"); + if (usage != null) { + Integer inputTokens = usage.getInt("prompt_tokens"); + Integer outputTokens = usage.getInt("completion_tokens"); + Integer totalTokens = usage.getInt("total_tokens"); + log.info("✅ {} 响应成功,tokens: 输入={}, 输出={}, 总计={}", + getProviderName(), inputTokens, outputTokens, totalTokens); + } + + log.debug("📥 {} 返回内容: {}", getProviderName(), content); + return content; + + } catch (Exception e) { + String errorMsg = String.format("%s API 调用失败: %s", getProviderName(), e.getMessage()); + log.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } + } + + @Override + public boolean isConfigValid() { + return config != null + && StrUtil.isNotBlank(config.getApiKey()) + && StrUtil.isNotBlank(config.getBaseUrl()) + && StrUtil.isNotBlank(config.getModel()); + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/AiProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/AiProvider.java new file mode 100644 index 00000000..d6c23301 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/AiProvider.java @@ -0,0 +1,32 @@ +package com.youlai.boot.platform.ai.provider; + +/** + * AI 提供商接口 + * + * 策略模式:不同提供商实现各自的调用逻辑 + * + * @author Ray.Hao + */ +public interface AiProvider { + + /** + * 调用 AI API + * + * @param systemPrompt 系统提示词 + * @param userPrompt 用户提示词 + * @return AI 响应内容 + */ + String call(String systemPrompt, String userPrompt); + + /** + * 获取提供商名称 + */ + String getProviderName(); + + /** + * 检查配置是否有效 + */ + boolean isConfigValid(); +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/AiProviderFactory.java b/src/main/java/com/youlai/boot/platform/ai/provider/AiProviderFactory.java new file mode 100644 index 00000000..16b32254 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/AiProviderFactory.java @@ -0,0 +1,51 @@ +package com.youlai.boot.platform.ai.provider; + +import com.youlai.boot.platform.ai.config.AiProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * AI 提供商工厂 + * + * 职责:根据配置获取对应的提供商实例 + * + * @author Ray.Hao + */ +@Component +@RequiredArgsConstructor +public class AiProviderFactory { + + private final AiProperties aiProperties; + + /** + * Spring 自动注入所有 AiProvider 实现类 + * Key: Bean 名称(qwen、deepseek、openai) + * Value: 提供商实例 + */ + private final Map providers; + + /** + * 获取当前配置的提供商 + */ + public AiProvider getCurrentProvider() { + String providerName = aiProperties.getProvider(); + + if (!providers.containsKey(providerName)) { + throw new IllegalStateException("不支持的 AI 提供商: " + providerName + + ",可用提供商: " + providers.keySet()); + } + + AiProvider provider = providers.get(providerName); + + if (!provider.isConfigValid()) { + throw new IllegalStateException(provider.getProviderName() + + " 配置无效,请检查 API Key、Base URL 和 Model 是否配置"); + } + + return provider; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/impl/DeepSeekProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/impl/DeepSeekProvider.java new file mode 100644 index 00000000..12b80aaf --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/impl/DeepSeekProvider.java @@ -0,0 +1,25 @@ +package com.youlai.boot.platform.ai.provider.impl; + +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider; +import org.springframework.stereotype.Component; + +/** + * DeepSeek 提供商 + * + * @author Ray.Hao + */ +@Component("deepseek") +public class DeepSeekProvider extends AbstractOpenAiCompatibleProvider { + + public DeepSeekProvider(AiProperties aiProperties) { + super(aiProperties.getProviders().get("deepseek")); + } + + @Override + public String getProviderName() { + return config.getDisplayName() != null ? config.getDisplayName() : "DeepSeek"; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/impl/OpenAiProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/impl/OpenAiProvider.java new file mode 100644 index 00000000..09613e33 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/impl/OpenAiProvider.java @@ -0,0 +1,30 @@ +package com.youlai.boot.platform.ai.provider.impl; + +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider; +import org.springframework.stereotype.Component; + +/** + * OpenAI 提供商(GPT-4、GPT-3.5 等) + * + * 添加新提供商只需: + * 1. 继承 AbstractOpenAiCompatibleProvider + * 2. 实现 getProviderName() + * 3. 在配置文件中添加配置 + * + * @author Ray.Hao + */ +@Component("openai") +public class OpenAiProvider extends AbstractOpenAiCompatibleProvider { + + public OpenAiProvider(AiProperties aiProperties) { + super(aiProperties.getProviders().get("openai")); + } + + @Override + public String getProviderName() { + return config.getDisplayName() != null ? config.getDisplayName() : "OpenAI"; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/impl/QwenProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/impl/QwenProvider.java new file mode 100644 index 00000000..5a6b6dcd --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/impl/QwenProvider.java @@ -0,0 +1,25 @@ +package com.youlai.boot.platform.ai.provider.impl; + +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider; +import org.springframework.stereotype.Component; + +/** + * 阿里通义千问提供商 + * + * @author Ray.Hao + */ +@Component("qwen") +public class QwenProvider extends AbstractOpenAiCompatibleProvider { + + public QwenProvider(AiProperties aiProperties) { + super(aiProperties.getProviders().get("qwen")); + } + + @Override + public String getProviderName() { + return config.getDisplayName() != null ? config.getDisplayName() : "阿里通义千问"; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java new file mode 100644 index 00000000..f6c5b0a8 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java @@ -0,0 +1,63 @@ +package com.youlai.boot.platform.ai.service; + +import com.youlai.boot.platform.ai.model.dto.*; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; +import java.util.Map; + +/** + * AI 命令服务接口 + * + * @author Ray.Hao + * @since 3.0.0 + */ +public interface AiCommandService { + + /** + * 解析自然语言命令 + * + * @param request 命令请求 + * @param httpRequest HTTP 请求 + * @return 解析结果 + */ + AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest); + + /** + * 执行已解析的命令 + * + * @param request 执行请求 + * @param httpRequest HTTP 请求 + * @return 执行结果 + */ + AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest); + + /** + * 获取命令执行历史 + * + * @param page 页码 + * @param size 每页数量 + * @return 历史记录 + */ + Map getCommandHistory(Integer page, Integer size); + + /** + * 获取可用的函数列表 + * + * @return 函数列表 + */ + List> getAvailableFunctions(); + + /** + * 撤销命令执行 + * + * @param auditId 审计ID + */ + void rollbackCommand(String auditId); +} + + + + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java new file mode 100644 index 00000000..346995b2 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java @@ -0,0 +1,262 @@ +package com.youlai.boot.platform.ai.service.impl; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.model.dto.*; +import com.youlai.boot.platform.ai.model.entity.AiCommandAudit; +import com.youlai.boot.platform.ai.provider.AiProvider; +import com.youlai.boot.platform.ai.provider.AiProviderFactory; +import com.youlai.boot.platform.ai.service.AiCommandService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * AI 命令服务实现类(重构版) + * + * 重构改进: + * 1. ✅ 使用策略模式 + 工厂模式管理提供商,消除 switch-case + * 2. ✅ 配置映射化,添加新提供商只需配置,无需修改代码 + * 3. ✅ 统一命名为 base-url,符合行业惯例 + * 4. ✅ Service 层直接返回 DTO,不包装 Result(由 Controller 统一处理) + * 5. ✅ 职责清晰,扩展性强 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class AiCommandServiceImpl implements AiCommandService { + + private final AiProperties aiProperties; + private final AiProviderFactory providerFactory; + + // 审计日志存储(简化实现,实际应使用数据库) + private final Map auditStore = new HashMap<>(); + + /** + * 解析自然语言命令 + * + * 注意:直接返回 DTO,不包装 Result + * Controller 负责统一包装成 Result + */ + @Override + public AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest) { + // 检查 AI 功能是否启用 + if (!aiProperties.getEnabled()) { + throw new IllegalStateException("AI 功能未启用,请在配置文件中设置 ai.enabled=true"); + } + + try { + // 获取当前提供商(自动校验配置) + AiProvider provider = providerFactory.getCurrentProvider(); + + log.info("📤 使用 {} 解析命令: {}", provider.getProviderName(), request.getCommand()); + + // 构建提示词 + String systemPrompt = buildSystemPrompt(); + String userPrompt = buildUserPrompt(request); + + // 调用 AI API + String response = provider.call(systemPrompt, userPrompt); + + // 解析响应 + return parseAiResponse(response); + + } catch (IllegalStateException e) { + // 配置错误,抛出让 Controller 处理 + throw e; + } catch (Exception e) { + log.error("解析命令失败", e); + throw new RuntimeException("解析命令失败: " + e.getMessage(), e); + } + } + + /** + * 执行已解析的命令 + */ + @Override + public AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) { + // TODO: 实现命令执行逻辑 + throw new UnsupportedOperationException("待实现"); + } + + /** + * 获取命令执行历史 + */ + @Override + public Map getCommandHistory(Integer page, Integer size) { + List allAudits = new ArrayList<>(auditStore.values()); + allAudits.sort(Comparator.comparing(AiCommandAudit::getCreateTime).reversed()); + + int total = allAudits.size(); + int start = (page - 1) * size; + int end = Math.min(start + size, total); + + List pageData = start < total ? allAudits.subList(start, end) : new ArrayList<>(); + + Map result = new HashMap<>(); + result.put("list", pageData); + result.put("total", total); + result.put("page", page); + result.put("size", size); + + return result; + } + + /** + * 获取可用的函数列表 + */ + @Override + public List> getAvailableFunctions() { + List> functions = new ArrayList<>(); + + // 用户管理函数 + functions.add(createFunctionDef( + "deleteUser", + "删除用户", + Map.of("name", "String - 用户姓名", "id", "Long - 用户ID(可选)") + )); + + functions.add(createFunctionDef( + "updateUser", + "更新用户信息", + Map.of("id", "Long - 用户ID", "nickname", "String - 昵称", "status", "Integer - 状态") + )); + + functions.add(createFunctionDef( + "queryUsers", + "查询用户列表", + Map.of("name", "String - 姓名(可选)", "status", "Integer - 状态(可选)") + )); + + // 角色管理函数 + functions.add(createFunctionDef( + "assignRole", + "分配角色给用户", + Map.of("userId", "Long - 用户ID", "roleIds", "List - 角色ID列表") + )); + + return functions; + } + + /** + * 撤销命令执行 + */ + @Override + public void rollbackCommand(String auditId) { + AiCommandAudit audit = auditStore.get(auditId); + if (audit == null) { + throw new RuntimeException("审计记录不存在"); + } + + if (!"success".equals(audit.getExecuteStatus())) { + throw new RuntimeException("只能撤销成功执行的命令"); + } + + // TODO: 实现具体的回滚逻辑 + log.info("撤销命令执行: auditId={}, function={}", auditId, audit.getFunctionName()); + throw new UnsupportedOperationException("回滚功能尚未实现"); + } + + // ==================== 私有方法 ==================== + + /** + * 构建系统提示词(包含可用函数定义) + */ + private String buildSystemPrompt() { + return """ + 你是一个专业的命令解析助手。你的任务是将用户的自然语言命令转换为结构化的函数调用。 + + 可用函数: + 1. queryUsers - 查询用户列表 + 参数:keywords(搜索关键字), status(状态), deptId(部门ID) + + 2. deleteUser - 删除用户 + 参数:userId(用户ID) + + 3. updateUser - 更新用户信息 + 参数:userId(用户ID), nickname(昵称), mobile(手机号) + + 请将命令解析为以下 JSON 格式: + { + "functionCalls": [ + { + "function": "函数名", + "parameters": { "参数名": "参数值" }, + "description": "操作说明" + } + ] + } + """; + } + + /** + * 构建用户提示词 + */ + private String buildUserPrompt(AiCommandRequestDTO request) { + return "请解析以下命令:" + request.getCommand(); + } + + /** + * 解析 AI 响应 + */ + private AiCommandResponseDTO parseAiResponse(String response) { + try { + // 提取 JSON + int jsonStart = response.indexOf("{"); + int jsonEnd = response.lastIndexOf("}") + 1; + + if (jsonStart == -1 || jsonEnd == 0) { + throw new IllegalArgumentException("AI 返回格式错误:未找到 JSON"); + } + + String jsonStr = response.substring(jsonStart, jsonEnd); + JSONObject json = JSONUtil.parseObj(jsonStr); + + // 解析函数调用列表 + List functionCalls = new ArrayList<>(); + JSONArray callsArray = json.getJSONArray("functionCalls"); + + if (callsArray != null) { + for (int i = 0; i < callsArray.size(); i++) { + JSONObject call = callsArray.getJSONObject(i); + functionCalls.add(FunctionCallDTO.builder() + .name(call.getStr("function")) + .arguments(call.getJSONObject("parameters") != null ? + call.getJSONObject("parameters").toBean(Map.class) : new HashMap<>()) + .description(call.getStr("description")) + .build()); + } + } + + return AiCommandResponseDTO.builder() + .success(true) + .functionCalls(functionCalls) + .rawResponse(response) + .build(); + + } catch (Exception e) { + log.error("解析 AI 响应失败", e); + throw new RuntimeException("解析响应失败: " + e.getMessage(), e); + } + } + + /** + * 创建函数定义 + */ + private Map createFunctionDef(String name, String description, Map parameters) { + Map func = new HashMap<>(); + func.put("name", name); + func.put("description", description); + func.put("parameters", parameters); + return func; + } +} + diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 412f9494..da7979d9 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -6,8 +6,8 @@ spring: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true - username: youlai - password: 123456 + username: root + password: Youlai@2025 data: redis: database: 0 @@ -74,7 +74,7 @@ mybatis-plus: security: session: type: jwt # 会话方式 jwt/redis-token - access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期 + access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期 refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期 jwt: secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符) @@ -82,12 +82,12 @@ security: allow-multi-login: true # 是否允许多设备登录 # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等) ignore-urls: - - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) - - /api/v1/auth/captcha # 验证码获取接口 - - /api/v1/auth/refresh-token # 刷新令牌接口 - - /api/v1/auth/logout # 开放退出登录 + - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) + - /api/v1/auth/captcha # 验证码获取接口 + - /api/v1/auth/refresh-token # 刷新令牌接口 + - /api/v1/auth/logout # 开放退出登录 - /api/v1/auth/wx/miniapp/code-login # 微信小程序code登陆 - - /ws/** # WebSocket接口 + - /ws/** # WebSocket接口 # 非安全端点路径,完全绕过 Spring Security 的安全控制 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -153,7 +153,7 @@ springdoc: api-docs: path: /v3/api-docs group-configs: - - group: '系统管理' + - group: "系统管理" paths-to-match: "/**" packages-to-scan: - com.youlai.boot.auth.controller @@ -165,9 +165,9 @@ springdoc: # knife4j 接口文档配置 knife4j: # 是否开启 Knife4j 增强功能 - enable: true # 设置为 true 表示开启增强功能 + enable: true # 设置为 true 表示开启增强功能 # 生产环境配置 - production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用) + production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用) setting: language: zh_cn @@ -223,3 +223,66 @@ wx: miniapp: app-id: xxxxxx app-secret: xxxxxx + +# ==================== AI 命令系统配置 ==================== +ai: + # 是否启用 AI 功能 + enabled: false + + # 当前使用的提供商:qwen、deepseek、openai + provider: qwen + + # 所有提供商配置(统一管理,扩展性强) + providers: + # 阿里通义千问(推荐:有免费额度) + qwen: + # API Key(https://bailian.console.aliyun.com/ 获取) + api-key: ${QWEN_API_KEY:sk-c2941d05bf2f411ca80424fcxxxxxxxx} + + # Base URL(OpenAI 兼容端点) + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + + # 模型:qwen-plus(推荐)、qwen-turbo、qwen-max、qwen-long + model: qwen-plus + + # 显示名称 + display-name: 阿里通义千问 + + # 超时时间(秒) + timeout: 30 + + # DeepSeek + deepseek: + api-key: ${DEEPSEEK_API_KEY:} + base-url: https://api.deepseek.com/v1 + model: deepseek-chat + display-name: DeepSeek + timeout: 30 + + # OpenAI(添加新提供商只需配置,无需修改代码) + openai: + api-key: ${OPENAI_API_KEY:} + base-url: https://api.openai.com/v1 + model: gpt-4 + display-name: OpenAI GPT-4 + timeout: 60 + + # 安全配置 + security: + enable-audit: true + dangerous-operations-confirm: true + function-whitelist: + - deleteUser + - updateUser + - queryUsers + - assignRole + sensitive-params: + - password + - idCard + - bankCard + - token + + # 限流配置 + rate-limit: + max-executions-per-minute: 10 + max-executions-per-day: 100 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 53695a35..def022f8 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -218,4 +218,68 @@ captcha: wx: miniapp: app-id: xxxxxx - app-secret: xxxxxx \ No newline at end of file + app-secret: xxxxxx + + +# ==================== AI 命令系统配置 ==================== +ai: + # 是否启用 AI 功能 + enabled: false + + # 当前使用的提供商:qwen、deepseek、openai + provider: qwen + + # 所有提供商配置(统一管理,扩展性强) + providers: + # 阿里通义千问(推荐:有免费额度) + qwen: + # API Key(https://bailian.console.aliyun.com/ 获取) + api-key: ${QWEN_API_KEY:sk-c2941d05bf2f411ca80424fcxxxxxxxx} + + # Base URL(OpenAI 兼容端点) + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + + # 模型:qwen-plus(推荐)、qwen-turbo、qwen-max、qwen-long + model: qwen-plus + + # 显示名称 + display-name: 阿里通义千问 + + # 超时时间(秒) + timeout: 30 + + # DeepSeek + deepseek: + api-key: ${DEEPSEEK_API_KEY:} + base-url: https://api.deepseek.com/v1 + model: deepseek-chat + display-name: DeepSeek + timeout: 30 + + # OpenAI(添加新提供商只需配置,无需修改代码) + openai: + api-key: ${OPENAI_API_KEY:} + base-url: https://api.openai.com/v1 + model: gpt-4 + display-name: OpenAI GPT-4 + timeout: 60 + + # 安全配置 + security: + enable-audit: true + dangerous-operations-confirm: true + function-whitelist: + - deleteUser + - updateUser + - queryUsers + - assignRole + sensitive-params: + - password + - idCard + - bankCard + - token + + # 限流配置 + rate-limit: + max-executions-per-minute: 10 + max-executions-per-day: 100