refactor(ai): 引入 Spring AI 简化 LLM 集成,通过 Function Call 为管理系统提供智能化交互入口。
This commit is contained in:
@@ -11,7 +11,6 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan // 开启配置属性绑定
|
||||
public class YouLaiBootApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.config;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.system.service.WebSocketService;
|
||||
import com.youlai.boot.platform.websocket.service.WebSocketService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
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<String, ProviderConfig> 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<String> functionWhitelist;
|
||||
private java.util.List<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.youlai.boot.platform.ai.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import com.youlai.boot.platform.ai.tools.UserTools;
|
||||
|
||||
/**
|
||||
* Spring AI 配置类
|
||||
*
|
||||
* 使用 Spring AI 自动配置,支持:
|
||||
* - OpenAI
|
||||
* - 通义千问(DashScope 兼容 OpenAI 协议)
|
||||
* - DeepSeek(兼容 OpenAI 协议)
|
||||
* - 其他兼容 OpenAI 协议的模型
|
||||
*
|
||||
* 配置方式:
|
||||
* spring.ai.openai.api-key: xxx
|
||||
* spring.ai.openai.base-url: xxx
|
||||
* spring.ai.openai.chat.options.model: xxx
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "spring.ai.openai.chat", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class SpringAiConfig {
|
||||
|
||||
/**
|
||||
* 创建 ChatClient(Spring AI 核心客户端)
|
||||
* <p>
|
||||
* OpenAiChatModel 由 Spring AI 自动配置创建
|
||||
* 根据 spring.ai.openai.* 配置自动初始化
|
||||
*/
|
||||
@Bean
|
||||
public ChatClient chatClient(OpenAiChatModel chatModel, UserTools userTools) {
|
||||
log.info("✅ Spring AI ChatClient 初始化成功");
|
||||
log.info("📋 当前配置 - 模型: {}", chatModel.getDefaultOptions().getModel());
|
||||
// 将 UserTools 注册为默认工具,所有调用都可使用
|
||||
return ChatClient.builder(chatModel)
|
||||
.defaultTools(userTools)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package com.youlai.boot.platform.ai.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.platform.ai.model.dto.*;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -11,9 +18,8 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* AI 命令控制器
|
||||
* AI 命令控制器(基于 Spring AI)
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
@@ -25,75 +31,60 @@ import org.springframework.web.bind.annotation.*;
|
||||
@Slf4j
|
||||
public class AiCommandController {
|
||||
|
||||
private final AiCommandService aiCommandService;
|
||||
private final AiCommandService aiCommandService;
|
||||
private final AiCommandRecordService recordService;
|
||||
|
||||
@Operation(summary = "解析自然语言命令")
|
||||
@PostMapping("/parse")
|
||||
public Result<AiCommandResponseDTO> parseCommand(
|
||||
@RequestBody AiCommandRequestDTO request,
|
||||
HttpServletRequest httpRequest
|
||||
) {
|
||||
log.info("收到AI命令解析请求: {}", request.getCommand());
|
||||
@Operation(summary = "解析自然语言命令")
|
||||
@PostMapping("/parse")
|
||||
public Result<AiParseResponseDTO> parseCommand(
|
||||
@RequestBody AiParseRequestDTO 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());
|
||||
}
|
||||
try {
|
||||
AiParseResponseDTO response = aiCommandService.parseCommand(request, httpRequest);
|
||||
return Result.success(response);
|
||||
} catch (Exception e) {
|
||||
log.error("命令解析失败", e);
|
||||
return Result.success(AiParseResponseDTO.builder()
|
||||
.success(false)
|
||||
.error(e.getMessage())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "执行已解析的命令")
|
||||
@PostMapping("/execute")
|
||||
public Result<AiExecuteResponseDTO> 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 = "执行已解析的命令")
|
||||
@PostMapping("/execute")
|
||||
public Result<Object> executeCommand(
|
||||
@RequestBody AiExecuteRequestDTO request,
|
||||
HttpServletRequest httpRequest
|
||||
) {
|
||||
log.info("收到AI命令执行请求: {}", request.getFunctionCall().getName());
|
||||
try {
|
||||
Object result = aiCommandService.executeCommand(request, httpRequest);
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("命令执行失败", e);
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@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 = "获取AI命令记录分页列表")
|
||||
@GetMapping("/records")
|
||||
public PageResult<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||
IPage<AiCommandRecordVO> page = recordService.getRecordPage(queryParams);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取可用的函数列表")
|
||||
@GetMapping("/functions")
|
||||
public Result<?> getAvailableFunctions() {
|
||||
return Result.success(aiCommandService.getAvailableFunctions());
|
||||
}
|
||||
@Operation(summary = "撤销命令执行")
|
||||
@PostMapping("/rollback/{recordId}")
|
||||
public Result<?> rollbackCommand(
|
||||
@Parameter(description = "记录ID") @PathVariable String recordId
|
||||
) {
|
||||
recordService.rollbackCommand(recordId);
|
||||
return Result.success("撤销成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "撤销命令执行")
|
||||
@PostMapping("/rollback/{auditId}")
|
||||
public Result<?> rollbackCommand(
|
||||
@Parameter(description = "审计ID") @PathVariable String auditId
|
||||
) {
|
||||
aiCommandService.rollbackCommand(auditId);
|
||||
return Result.success("撤销成功");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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<AiCommandAudit> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.youlai.boot.platform.ai.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* AI 命令记录 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiCommandRecordMapper extends BaseMapper<AiCommandRecord> {
|
||||
|
||||
/**
|
||||
* 获取 AI 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandRecordVO> getRecordPage(Page<AiCommandRecordVO> page, AiCommandPageQuery queryParams);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,20 @@ import lombok.Data;
|
||||
@Data
|
||||
public class AiExecuteRequestDTO {
|
||||
|
||||
/**
|
||||
* 关联的解析日志ID
|
||||
*/
|
||||
private String parseLogId;
|
||||
|
||||
/**
|
||||
* 原始命令(用于审计)
|
||||
*/
|
||||
private String originalCommand;
|
||||
|
||||
/**
|
||||
* 要执行的函数调用
|
||||
*/
|
||||
private FunctionCallDTO functionCall;
|
||||
private AiFunctionCallDTO functionCall;
|
||||
|
||||
/**
|
||||
* 确认模式:auto=自动执行, manual=需要用户确认
|
||||
@@ -30,6 +40,11 @@ public class AiExecuteRequestDTO {
|
||||
* 幂等性令牌(防止重复执行)
|
||||
*/
|
||||
private String idempotencyKey;
|
||||
|
||||
/**
|
||||
* 当前页面路由
|
||||
*/
|
||||
private String currentRoute;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -43,9 +43,9 @@ public class AiExecuteResponseDTO {
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* 审计ID(用于追踪)
|
||||
* 记录ID(用于追踪)
|
||||
*/
|
||||
private String auditId;
|
||||
private Long recordId;
|
||||
|
||||
/**
|
||||
* 需要用户确认
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.NoArgsConstructor;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 函数调用 DTO
|
||||
* AI 函数调用 DTO
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
@@ -16,7 +16,7 @@ import java.util.Map;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FunctionCallDTO {
|
||||
public class AiFunctionCallDTO {
|
||||
|
||||
/**
|
||||
* 函数名称
|
||||
@@ -34,5 +34,3 @@ public class FunctionCallDTO {
|
||||
private Map<String, Object> arguments;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import lombok.Data;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 命令请求 DTO
|
||||
* AI 解析请求 DTO
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
public class AiCommandRequestDTO {
|
||||
public class AiParseRequestDTO {
|
||||
|
||||
/**
|
||||
* 用户输入的自然语言命令
|
||||
@@ -33,5 +33,3 @@ public class AiCommandRequestDTO {
|
||||
private Map<String, Object> context;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.NoArgsConstructor;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 命令解析响应 DTO
|
||||
* AI 解析响应 DTO
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
@@ -16,7 +16,12 @@ import java.util.List;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AiCommandResponseDTO {
|
||||
public class AiParseResponseDTO {
|
||||
|
||||
/**
|
||||
* 解析日志ID(用于关联执行记录)
|
||||
*/
|
||||
private Long parseLogId;
|
||||
|
||||
/**
|
||||
* 是否成功解析
|
||||
@@ -26,7 +31,7 @@ public class AiCommandResponseDTO {
|
||||
/**
|
||||
* 解析后的函数调用列表
|
||||
*/
|
||||
private List<FunctionCallDTO> functionCalls;
|
||||
private List<AiFunctionCallDTO> functionCalls;
|
||||
|
||||
/**
|
||||
* AI 的理解和说明
|
||||
@@ -49,5 +54,3 @@ public class AiCommandResponseDTO {
|
||||
private String rawResponse;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.youlai.boot.platform.ai.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* AI 命令记录实体(合并解析和执行记录)
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("ai_command_record")
|
||||
public class AiCommandRecord extends BaseEntity {
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** 原始命令 */
|
||||
private String originalCommand;
|
||||
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
/** AI 供应商(qwen/openai/deepseek等) */
|
||||
private String provider;
|
||||
|
||||
/** AI 模型(qwen-plus/qwen-max/gpt-4-turbo等) */
|
||||
private String model;
|
||||
|
||||
/** 解析是否成功 */
|
||||
private Boolean parseSuccess;
|
||||
|
||||
/** 解析出的函数调用列表(JSON) */
|
||||
private String functionCalls;
|
||||
|
||||
/** AI 的理解说明 */
|
||||
private String explanation;
|
||||
|
||||
/** 置信度(0.00-1.00) */
|
||||
private BigDecimal confidence;
|
||||
|
||||
/** 解析错误信息 */
|
||||
private String parseErrorMessage;
|
||||
|
||||
/** 输入 Token 数量 */
|
||||
private Integer inputTokens;
|
||||
|
||||
/** 输出 Token 数量 */
|
||||
private Integer outputTokens;
|
||||
|
||||
/** 总 Token 数量 */
|
||||
private Integer totalTokens;
|
||||
|
||||
/** 解析耗时(毫秒) */
|
||||
private Long parseTime;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
/** 执行的函数名称 */
|
||||
private String functionName;
|
||||
|
||||
/** 函数参数(JSON) */
|
||||
private String functionArguments;
|
||||
|
||||
/** 执行状态:pending, success, failed */
|
||||
private String executeStatus;
|
||||
|
||||
/** 执行结果(JSON) */
|
||||
private String executeResult;
|
||||
|
||||
/** 执行错误信息 */
|
||||
private String executeErrorMessage;
|
||||
|
||||
/** 影响的记录数 */
|
||||
private Integer affectedRows;
|
||||
|
||||
/** 是否危险操作 */
|
||||
private Boolean isDangerous;
|
||||
|
||||
/** 是否需要确认 */
|
||||
private Boolean requiresConfirmation;
|
||||
|
||||
/** 用户是否确认 */
|
||||
private Boolean userConfirmed;
|
||||
|
||||
/** 幂等性令牌(防止重复执行) */
|
||||
private String idempotencyKey;
|
||||
|
||||
/** 执行耗时(毫秒) */
|
||||
private Long executionTime;
|
||||
|
||||
// ==================== 通用字段 ====================
|
||||
|
||||
/** IP 地址 */
|
||||
private String ipAddress;
|
||||
|
||||
/** 用户代理 */
|
||||
private String userAgent;
|
||||
|
||||
/** 当前页面路由 */
|
||||
private String currentRoute;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.youlai.boot.platform.ai.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BasePageQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI命令记录分页查询对象
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "AI命令记录分页查询对象")
|
||||
@Getter
|
||||
@Setter
|
||||
public class AiCommandPageQuery extends BasePageQuery {
|
||||
|
||||
@Schema(description = "关键字(原始命令/函数名称/用户名)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "执行状态(pending-待执行, success-成功, failed-失败)")
|
||||
private String executeStatus;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "是否危险操作")
|
||||
private Boolean isDangerous;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
private List<String> createTime;
|
||||
|
||||
@Schema(description = "函数名称")
|
||||
private String functionName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.youlai.boot.platform.ai.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BasePageQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI命令解析日志分页查询对象
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "AI命令解析日志分页查询对象")
|
||||
@Getter
|
||||
@Setter
|
||||
public class AiParseLogPageQuery extends BasePageQuery {
|
||||
|
||||
@Schema(description = "关键字(原始命令/用户名)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "解析是否成功(0-失败, 1-成功)")
|
||||
private Boolean parseSuccess;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "AI提供商(qwen/openai/deepseek/gemini等)")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "AI模型(qwen-plus/qwen-max/gpt-4-turbo等)")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
private List<String> createTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.youlai.boot.platform.ai.model.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* AI命令记录VO(合并解析和执行记录)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI命令记录VO")
|
||||
public class AiCommandRecordVO implements Serializable {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "原始命令")
|
||||
private String originalCommand;
|
||||
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
@Schema(description = "AI供应商")
|
||||
private String provider;
|
||||
|
||||
@Schema(description = "AI模型")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "解析是否成功")
|
||||
private Boolean parseSuccess;
|
||||
|
||||
@Schema(description = "解析出的函数调用列表(JSON)")
|
||||
private String functionCalls;
|
||||
|
||||
@Schema(description = "AI的理解说明")
|
||||
private String explanation;
|
||||
|
||||
@Schema(description = "置信度")
|
||||
private BigDecimal confidence;
|
||||
|
||||
@Schema(description = "解析错误信息")
|
||||
private String parseErrorMessage;
|
||||
|
||||
@Schema(description = "输入Token数量")
|
||||
private Integer inputTokens;
|
||||
|
||||
@Schema(description = "输出Token数量")
|
||||
private Integer outputTokens;
|
||||
|
||||
@Schema(description = "总Token数量")
|
||||
private Integer totalTokens;
|
||||
|
||||
@Schema(description = "解析耗时(毫秒)")
|
||||
private Long parseTime;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
@Schema(description = "执行的函数名称")
|
||||
private String functionName;
|
||||
|
||||
@Schema(description = "函数参数(JSON)")
|
||||
private String functionArguments;
|
||||
|
||||
@Schema(description = "执行状态")
|
||||
private String executeStatus;
|
||||
|
||||
@Schema(description = "执行结果(JSON)")
|
||||
private String executeResult;
|
||||
|
||||
@Schema(description = "执行错误信息")
|
||||
private String executeErrorMessage;
|
||||
|
||||
@Schema(description = "影响的记录数")
|
||||
private Integer affectedRows;
|
||||
|
||||
@Schema(description = "是否危险操作")
|
||||
private Boolean isDangerous;
|
||||
|
||||
@Schema(description = "是否需要确认")
|
||||
private Boolean requiresConfirmation;
|
||||
|
||||
@Schema(description = "用户是否确认")
|
||||
private Boolean userConfirmed;
|
||||
|
||||
@Schema(description = "执行耗时(毫秒)")
|
||||
private Long executionTime;
|
||||
|
||||
// ==================== 通用字段 ====================
|
||||
|
||||
@Schema(description = "IP地址")
|
||||
private String ipAddress;
|
||||
|
||||
@Schema(description = "用户代理")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "当前页面路由")
|
||||
private String currentRoute;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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<String, AiProvider> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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() : "阿里通义千问";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.youlai.boot.platform.ai.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
|
||||
/**
|
||||
* AI 命令记录服务接口
|
||||
*/
|
||||
public interface AiCommandRecordService extends IService<AiCommandRecord> {
|
||||
|
||||
/**
|
||||
* 获取命令记录分页列表
|
||||
*
|
||||
* @param queryParams 查询参数
|
||||
* @return 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 撤销命令执行
|
||||
*
|
||||
* @param recordId 记录ID
|
||||
*/
|
||||
void rollbackCommand(String recordId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,63 +1,29 @@
|
||||
package com.youlai.boot.platform.ai.service;
|
||||
|
||||
import com.youlai.boot.platform.ai.model.dto.*;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI 命令服务接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
* AI 命令编排服务:负责对外的解析与执行编排
|
||||
*/
|
||||
public interface AiCommandService {
|
||||
|
||||
/**
|
||||
* 解析自然语言命令
|
||||
*
|
||||
* @param request 命令请求
|
||||
* @param httpRequest HTTP 请求
|
||||
* @return 解析结果
|
||||
*/
|
||||
AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest);
|
||||
/**
|
||||
* 解析自然语言命令
|
||||
*/
|
||||
AiParseResponseDTO parseCommand(AiParseRequestDTO request, HttpServletRequest httpRequest);
|
||||
|
||||
/**
|
||||
* 执行已解析的命令
|
||||
*
|
||||
* @param request 执行请求
|
||||
* @param httpRequest HTTP 请求
|
||||
* @return 执行结果
|
||||
*/
|
||||
AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest);
|
||||
|
||||
/**
|
||||
* 获取命令执行历史
|
||||
*
|
||||
* @param page 页码
|
||||
* @param size 每页数量
|
||||
* @return 历史记录
|
||||
*/
|
||||
Map<String, Object> getCommandHistory(Integer page, Integer size);
|
||||
|
||||
/**
|
||||
* 获取可用的函数列表
|
||||
*
|
||||
* @return 函数列表
|
||||
*/
|
||||
List<Map<String, Object>> getAvailableFunctions();
|
||||
|
||||
/**
|
||||
* 撤销命令执行
|
||||
*
|
||||
* @param auditId 审计ID
|
||||
*/
|
||||
void rollbackCommand(String auditId);
|
||||
/**
|
||||
* 执行已解析的命令
|
||||
*
|
||||
* @param request 执行请求
|
||||
* @param httpRequest HTTP 请求
|
||||
* @return 执行结果数据(成功时返回)
|
||||
* @throws Exception 执行失败时抛出异常
|
||||
*/
|
||||
Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.youlai.boot.platform.ai.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* AI 命令记录服务实现类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AiCommandRecordServiceImpl extends ServiceImpl<AiCommandRecordMapper, AiCommandRecord>
|
||||
implements AiCommandRecordService {
|
||||
|
||||
@Override
|
||||
public IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||
Page<AiCommandRecordVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
return this.baseMapper.getRecordPage(page, queryParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollbackCommand(String recordId) {
|
||||
AiCommandRecord record = this.getById(recordId);
|
||||
if (record == null) {
|
||||
throw new RuntimeException("命令记录不存在");
|
||||
}
|
||||
|
||||
if (!"success".equals(record.getExecuteStatus())) {
|
||||
throw new RuntimeException("只能撤销成功执行的命令");
|
||||
}
|
||||
|
||||
// TODO: 实现具体的回滚逻辑
|
||||
log.info("撤销命令执行: recordId={}, function={}", recordId, record.getFunctionName());
|
||||
throw new UnsupportedOperationException("回滚功能尚未实现");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,262 +1,364 @@
|
||||
package com.youlai.boot.platform.ai.service.impl;
|
||||
|
||||
import cn.hutool.core.lang.TypeReference;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
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.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiFunctionCallDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
|
||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||
import com.youlai.boot.platform.ai.tools.UserTools;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* AI 命令服务实现类(重构版)
|
||||
*
|
||||
* 重构改进:
|
||||
* 1. ✅ 使用策略模式 + 工厂模式管理提供商,消除 switch-case
|
||||
* 2. ✅ 配置映射化,添加新提供商只需配置,无需修改代码
|
||||
* 3. ✅ 统一命名为 base-url,符合行业惯例
|
||||
* 4. ✅ Service 层直接返回 DTO,不包装 Result(由 Controller 统一处理)
|
||||
* 5. ✅ 职责清晰,扩展性强
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
* AI 命令编排服务实现
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AiCommandServiceImpl implements AiCommandService {
|
||||
|
||||
private final AiProperties aiProperties;
|
||||
private final AiProviderFactory providerFactory;
|
||||
private static final String SYSTEM_PROMPT = """
|
||||
你是一个智能的企业操作助手,需要将用户的自然语言命令解析成标准的函数调用。
|
||||
请返回严格的 JSON 格式,包含字段:
|
||||
- success: boolean
|
||||
- explanation: string
|
||||
- confidence: number (0-1)
|
||||
- error: string
|
||||
- provider: string
|
||||
- model: string
|
||||
- functionCalls: 数组,每个元素包含 name、description、arguments(对象)
|
||||
当无法识别命令时,success=false,并给出 error。
|
||||
""";
|
||||
|
||||
// 审计日志存储(简化实现,实际应使用数据库)
|
||||
private final Map<String, AiCommandAudit> auditStore = new HashMap<>();
|
||||
private final AiCommandRecordService recordService;
|
||||
private final UserTools userTools;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
/**
|
||||
* 解析自然语言命令
|
||||
*
|
||||
* 注意:直接返回 DTO,不包装 Result
|
||||
* Controller 负责统一包装成 Result
|
||||
*/
|
||||
@Override
|
||||
public AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest) {
|
||||
// 检查 AI 功能是否启用
|
||||
if (!aiProperties.getEnabled()) {
|
||||
throw new IllegalStateException("AI 功能未启用,请在配置文件中设置 ai.enabled=true");
|
||||
@Override
|
||||
public AiParseResponseDTO parseCommand(AiParseRequestDTO request, HttpServletRequest httpRequest) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
String command = Optional.ofNullable(request.getCommand()).orElse("").trim();
|
||||
|
||||
if (StrUtil.isBlank(command)) {
|
||||
return AiParseResponseDTO.builder()
|
||||
.success(false)
|
||||
.error("命令不能为空")
|
||||
.functionCalls(Collections.emptyList())
|
||||
.build();
|
||||
}
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
||||
|
||||
AiCommandRecord record = new AiCommandRecord();
|
||||
record.setUserId(userId);
|
||||
record.setUsername(username);
|
||||
record.setOriginalCommand(command);
|
||||
record.setIpAddress(ipAddress);
|
||||
record.setCurrentRoute(request.getCurrentRoute());
|
||||
record.setProvider("spring-ai");
|
||||
record.setModel("auto");
|
||||
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String userPrompt = buildUserPrompt(request);
|
||||
|
||||
try {
|
||||
log.info("📤 发送命令至 AI 模型: {}", command);
|
||||
ChatResponse chatResponse = chatClient.prompt()
|
||||
.system(systemPrompt)
|
||||
.user(userPrompt)
|
||||
.call().chatResponse();
|
||||
|
||||
String rawContent = Optional.ofNullable(chatResponse.getResult())
|
||||
.map(result -> result.getOutput().getText())
|
||||
.orElse("");
|
||||
|
||||
ParseResult parseResult = parseAiResponse(rawContent);
|
||||
|
||||
record.setProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
|
||||
record.setModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
|
||||
record.setParseSuccess(parseResult.success());
|
||||
record.setExplanation(parseResult.explanation());
|
||||
record.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
|
||||
record.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
|
||||
record.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
|
||||
record.setParseTime(System.currentTimeMillis() - startTime);
|
||||
|
||||
recordService.save(record);
|
||||
|
||||
AiParseResponseDTO response = AiParseResponseDTO.builder()
|
||||
.parseLogId(record.getId())
|
||||
.success(parseResult.success())
|
||||
.functionCalls(parseResult.functionCalls())
|
||||
.explanation(parseResult.explanation())
|
||||
.confidence(parseResult.confidence())
|
||||
.error(parseResult.error())
|
||||
.rawResponse(rawContent)
|
||||
.build();
|
||||
|
||||
if (!parseResult.success()) {
|
||||
log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
|
||||
} else {
|
||||
log.info("✅ 解析成功,审计记录ID: {}", record.getId());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
record.setParseSuccess(false);
|
||||
record.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
|
||||
record.setParseErrorMessage(e.getMessage());
|
||||
record.setParseTime(duration);
|
||||
recordService.save(record);
|
||||
|
||||
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSystemPrompt() {
|
||||
return SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
private String buildUserPrompt(AiParseRequestDTO request) {
|
||||
JSONObject payload = JSONUtil.createObj()
|
||||
.set("command", request.getCommand())
|
||||
.set("currentRoute", request.getCurrentRoute())
|
||||
.set("currentComponent", request.getCurrentComponent())
|
||||
.set("context", Optional.ofNullable(request.getContext()).orElse(Collections.emptyMap()))
|
||||
.set("availableFunctions", availableFunctions());
|
||||
|
||||
return StrUtil.format("""
|
||||
请根据以下上下文识别用户意图,并输出符合系统提示要求的 JSON:
|
||||
{}
|
||||
""", JSONUtil.toJsonPrettyStr(payload));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> availableFunctions() {
|
||||
return List.of(
|
||||
Map.of(
|
||||
"name", "updateUserNickname",
|
||||
"description", "根据用户名更新用户昵称",
|
||||
"requiredParameters", List.of("username", "nickname")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private ParseResult parseAiResponse(String rawContent) {
|
||||
if (StrUtil.isBlank(rawContent)) {
|
||||
throw new IllegalStateException("AI 返回内容为空");
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject jsonObject = JSONUtil.parseObj(rawContent);
|
||||
boolean success = jsonObject.getBool("success", false);
|
||||
String explanation = jsonObject.getStr("explanation");
|
||||
Double confidence = jsonObject.containsKey("confidence") ? jsonObject.getDouble("confidence") : null;
|
||||
String error = jsonObject.getStr("error");
|
||||
String provider = jsonObject.getStr("provider");
|
||||
String model = jsonObject.getStr("model");
|
||||
|
||||
List<AiFunctionCallDTO> functionCalls = toFunctionCallList(jsonObject.getJSONArray("functionCalls"));
|
||||
|
||||
return new ParseResult(success, explanation, confidence, error, provider, model, functionCalls);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("无法解析 AI 响应: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AiFunctionCallDTO> toFunctionCallList(JSONArray array) {
|
||||
if (array == null || array.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<AiFunctionCallDTO> result = new ArrayList<>();
|
||||
for (Object element : array) {
|
||||
JSONObject functionJson = JSONUtil.parseObj(element);
|
||||
Map<String, Object> arguments = Optional.ofNullable(functionJson.getJSONObject("arguments"))
|
||||
.map(obj -> obj.toBean(new TypeReference<Map<String, Object>>() {
|
||||
}))
|
||||
.orElse(Collections.emptyMap());
|
||||
|
||||
result.add(AiFunctionCallDTO.builder()
|
||||
.name(functionJson.getStr("name"))
|
||||
.description(functionJson.getStr("description"))
|
||||
.arguments(arguments)
|
||||
.build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private record ParseResult(
|
||||
boolean success,
|
||||
String explanation,
|
||||
Double confidence,
|
||||
String error,
|
||||
String provider,
|
||||
String model,
|
||||
List<AiFunctionCallDTO> functionCalls
|
||||
) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) throws Exception {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 获取用户信息
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
||||
|
||||
AiFunctionCallDTO functionCall = request.getFunctionCall();
|
||||
|
||||
// 判断是否为危险操作
|
||||
boolean isDangerous = isDangerousOperation(functionCall.getName());
|
||||
|
||||
// 根据解析日志ID获取审计记录,如果不存在则创建新记录
|
||||
AiCommandRecord record;
|
||||
if (StrUtil.isNotBlank(request.getParseLogId())) {
|
||||
// 更新已存在的审计记录(解析阶段已创建)
|
||||
record = recordService.getById(request.getParseLogId());
|
||||
if (record == null) {
|
||||
throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId());
|
||||
}
|
||||
} else {
|
||||
// 如果没有解析日志ID,创建新记录(兼容直接执行的情况)
|
||||
record = new AiCommandRecord();
|
||||
record.setUserId(userId);
|
||||
record.setUsername(username);
|
||||
record.setOriginalCommand(request.getOriginalCommand());
|
||||
record.setIpAddress(ipAddress);
|
||||
record.setCurrentRoute(request.getCurrentRoute());
|
||||
recordService.save(record);
|
||||
}
|
||||
|
||||
// 更新执行相关字段
|
||||
record.setFunctionName(functionCall.getName());
|
||||
record.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
|
||||
record.setIsDangerous(isDangerous);
|
||||
record.setRequiresConfirmation(request.getConfirmMode() != null &&
|
||||
"manual".equals(request.getConfirmMode()));
|
||||
record.setUserConfirmed(request.getUserConfirmed());
|
||||
record.setIdempotencyKey(request.getIdempotencyKey());
|
||||
record.setUserAgent(httpRequest.getHeader("User-Agent"));
|
||||
record.setExecuteStatus("pending");
|
||||
|
||||
try {
|
||||
// 幂等性检查
|
||||
if (StrUtil.isNotBlank(request.getIdempotencyKey())) {
|
||||
AiCommandRecord existing = recordService.getOne(
|
||||
new LambdaQueryWrapper<AiCommandRecord>()
|
||||
.eq(AiCommandRecord::getIdempotencyKey, request.getIdempotencyKey())
|
||||
.ne(AiCommandRecord::getId, record.getId()) // 排除当前记录
|
||||
);
|
||||
if (existing != null) {
|
||||
log.warn("⚠️ 检测到重复执行,幂等性令牌: {}", request.getIdempotencyKey());
|
||||
throw new IllegalStateException("该操作已执行,请勿重复提交");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前提供商(自动校验配置)
|
||||
AiProvider provider = providerFactory.getCurrentProvider();
|
||||
|
||||
log.info("📤 使用 {} 解析命令: {}", provider.getProviderName(), request.getCommand());
|
||||
// 🎯 执行具体的函数调用
|
||||
Object result = executeFunctionCall(functionCall);
|
||||
|
||||
// 构建提示词
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String userPrompt = buildUserPrompt(request);
|
||||
// 更新执行成功
|
||||
record.setExecuteStatus("success");
|
||||
record.setExecuteResult(JSONUtil.toJsonStr(result));
|
||||
record.setExecutionTime(System.currentTimeMillis() - startTime);
|
||||
|
||||
// 调用 AI API
|
||||
String response = provider.call(systemPrompt, userPrompt);
|
||||
// 更新审计记录
|
||||
recordService.updateById(record);
|
||||
|
||||
// 解析响应
|
||||
return parseAiResponse(response);
|
||||
log.info("✅ 命令执行成功,审计记录ID: {}", record.getId());
|
||||
|
||||
} catch (IllegalStateException e) {
|
||||
// 配置错误,抛出让 Controller 处理
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("解析命令失败", e);
|
||||
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 更新执行失败
|
||||
record.setExecuteStatus("failed");
|
||||
record.setExecuteErrorMessage(e.getMessage());
|
||||
record.setExecutionTime(System.currentTimeMillis() - startTime);
|
||||
|
||||
// 更新审计记录
|
||||
recordService.updateById(record);
|
||||
|
||||
log.error("❌ 命令执行失败,审计记录ID: {}", record.getId(), e);
|
||||
|
||||
// 抛出异常,由 Controller 统一处理
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为危险操作
|
||||
*/
|
||||
private boolean isDangerousOperation(String functionName) {
|
||||
String[] dangerousKeywords = {"delete", "remove", "drop", "truncate", "clear"};
|
||||
String lowerName = functionName.toLowerCase();
|
||||
for (String keyword : dangerousKeywords) {
|
||||
if (lowerName.contains(keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行具体的函数调用
|
||||
*/
|
||||
private Object executeFunctionCall(AiFunctionCallDTO functionCall) {
|
||||
String functionName = functionCall.getName();
|
||||
Map<String, Object> arguments = functionCall.getArguments();
|
||||
|
||||
log.info("🎯 执行函数: {}, 参数: {}", functionName, arguments);
|
||||
|
||||
// 根据函数名称路由到不同的处理器
|
||||
switch (functionName) {
|
||||
case "updateUserNickname":
|
||||
return executeUpdateUserNickname(arguments);
|
||||
default:
|
||||
throw new UnsupportedOperationException("不支持的函数: " + functionName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Tool: 根据用户名更新用户昵称
|
||||
*/
|
||||
private Object executeUpdateUserNickname(Map<String, Object> arguments) {
|
||||
String username = (String) arguments.get("username");
|
||||
String nickname = (String) arguments.get("nickname");
|
||||
|
||||
log.info("🔧 [Tool] 更新用户昵称: username={}, nickname={}", username, nickname);
|
||||
String resultMsg = userTools.updateUserNickname(username, nickname);
|
||||
|
||||
boolean success = resultMsg != null && resultMsg.contains("成功");
|
||||
if (!success) {
|
||||
throw new RuntimeException(resultMsg != null ? resultMsg : "更新用户昵称失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行已解析的命令
|
||||
*/
|
||||
@Override
|
||||
public AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) {
|
||||
// TODO: 实现命令执行逻辑
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命令执行历史
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getCommandHistory(Integer page, Integer size) {
|
||||
List<AiCommandAudit> 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<AiCommandAudit> pageData = start < total ? allAudits.subList(start, end) : new ArrayList<>();
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("list", pageData);
|
||||
result.put("total", total);
|
||||
result.put("page", page);
|
||||
result.put("size", size);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的函数列表
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> getAvailableFunctions() {
|
||||
List<Map<String, Object>> 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<Long> - 角色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<FunctionCallDTO> 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<String, Object> createFunctionDef(String name, String description, Map<String, String> parameters) {
|
||||
Map<String, Object> func = new HashMap<>();
|
||||
func.put("name", name);
|
||||
func.put("description", description);
|
||||
func.put("parameters", parameters);
|
||||
return func;
|
||||
}
|
||||
return Map.of("username", username, "nickname", nickname, "message", resultMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.youlai.boot.platform.ai.tools;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.youlai.boot.system.model.entity.User;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
|
||||
/**
|
||||
* 基于 Spring AI Tool 的用户管理工具
|
||||
*
|
||||
* 提供受控的 CRUD 能力,供 LLM 通过 Tool Calling 调用
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserTools {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Tool(description = "根据关键字在用户列表中筛选用户")
|
||||
public String queryUser(
|
||||
@ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords
|
||||
) {
|
||||
// 返回搜索关键字,前端会在用户列表页面进行筛选
|
||||
return "将在用户列表中搜索:" + keywords;
|
||||
}
|
||||
|
||||
@Tool(description = "根据用户名更新用户昵称")
|
||||
public String updateUserNickname(
|
||||
@ToolParam(description = "用户名") String username,
|
||||
@ToolParam(description = "新的昵称") String nickname
|
||||
) {
|
||||
|
||||
boolean ok = userService.update(new LambdaUpdateWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.set(User::getNickname, nickname)
|
||||
);
|
||||
return ok ? "用户昵称更新成功" : "用户昵称更新失败";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.system.service;
|
||||
package com.youlai.boot.platform.websocket.service;
|
||||
|
||||
/**
|
||||
* WebSocket服务接口
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.youlai.boot.system.service.impl;
|
||||
package com.youlai.boot.platform.websocket.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.youlai.boot.system.model.dto.DictEventDTO;
|
||||
import com.youlai.boot.system.service.WebSocketService;
|
||||
import com.youlai.boot.platform.websocket.service.WebSocketService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -16,7 +16,7 @@ import com.youlai.boot.system.model.form.DictForm;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.system.service.DictItemService;
|
||||
import com.youlai.boot.system.service.DictService;
|
||||
import com.youlai.boot.system.service.WebSocketService;
|
||||
import com.youlai.boot.platform.websocket.service.WebSocketService;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.youlai.boot.system.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
@@ -30,13 +27,7 @@ public class UserOnlineService {
|
||||
private final Map<String, UserOnlineInfo> onlineUsers = new ConcurrentHashMap<>();
|
||||
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
public UserOnlineService(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) {
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package com.youlai.boot.system.service.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* WebSocket 会话清理服务
|
||||
*
|
||||
* 功能:
|
||||
* - 定时清理僵尸会话
|
||||
* - 监控会话状态
|
||||
* - 输出统计信息
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(
|
||||
prefix = "websocket.session-cleanup",
|
||||
name = "enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = true
|
||||
)
|
||||
public class WebSocketSessionCleanupService {
|
||||
|
||||
private final WebSocketServiceImpl webSocketService;
|
||||
|
||||
/**
|
||||
* 定时输出 WebSocket 会话统计信息
|
||||
*
|
||||
* 每 5 分钟执行一次
|
||||
*/
|
||||
@Scheduled(fixedRate = 300000, initialDelay = 60000)
|
||||
public void logSessionStatistics() {
|
||||
try {
|
||||
int onlineUserCount = webSocketService.getOnlineUserCount();
|
||||
int totalSessionCount = webSocketService.getTotalSessionCount();
|
||||
|
||||
log.info("📊 WebSocket 统计 - 在线用户数: {}, 活跃会话数: {}",
|
||||
onlineUserCount, totalSessionCount);
|
||||
|
||||
// 详细信息(仅在有用户在线时输出)
|
||||
if (onlineUserCount > 0) {
|
||||
var onlineUsers = webSocketService.getOnlineUsers();
|
||||
onlineUsers.forEach(user -> {
|
||||
log.debug(" - 用户[{}]: {} 个会话", user.getUsername(), user.getSessionCount());
|
||||
});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("❌ 输出会话统计信息失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*
|
||||
* 每 30 秒执行一次,用于监控服务状态
|
||||
*/
|
||||
@Scheduled(fixedRate = 30000, initialDelay = 10000)
|
||||
public void healthCheck() {
|
||||
try {
|
||||
int onlineUserCount = webSocketService.getOnlineUserCount();
|
||||
int sessionCount = webSocketService.getTotalSessionCount();
|
||||
|
||||
// 异常检测:如果会话数远大于用户数,可能存在会话泄漏
|
||||
if (sessionCount > onlineUserCount * 10 && onlineUserCount > 0) {
|
||||
log.warn("⚠ 检测到异常:会话数({})远大于用户数({}×10),可能存在会话泄漏",
|
||||
sessionCount, onlineUserCount);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("❌ 健康检查失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发在线用户数广播
|
||||
*
|
||||
* 可用于系统启动后的初始化或手动刷新
|
||||
*/
|
||||
public void triggerOnlineCountBroadcast() {
|
||||
try {
|
||||
webSocketService.notifyOnlineUsersChange();
|
||||
log.info("✓ 手动触发在线用户数广播成功");
|
||||
} catch (Exception ex) {
|
||||
log.error("❌ 手动触发在线用户数广播失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user