refactor(ai): 重构AI命令记录模块
- 将AiCommandRecord重命名为AiCommandLog - 更新相关控制器、服务、映射器和实体类
This commit is contained in:
@@ -574,53 +574,37 @@ INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
|
||||
-- ----------------------------
|
||||
-- AI 命令记录表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_command_record`;
|
||||
CREATE TABLE `ai_command_record` (
|
||||
DROP TABLE IF EXISTS `ai_command_log`;
|
||||
CREATE TABLE `ai_command_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
|
||||
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
|
||||
`original_command` text COMMENT '原始命令',
|
||||
-- 解析相关字段
|
||||
`provider` varchar(32) DEFAULT NULL COMMENT 'AI供应商(qwen/openai/deepseek/gemini等)',
|
||||
`model` varchar(64) DEFAULT NULL COMMENT 'AI模型(qwen-plus/qwen-max/gpt-4-turbo等)',
|
||||
`parse_success` tinyint(1) DEFAULT NULL COMMENT '解析是否成功(0-失败, 1-成功)',
|
||||
`ai_provider` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'AI 供应商(qwen/openai/deepseek/gemini等)',
|
||||
`ai_model` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'AI 模型名称(qwen-plus/qwen-max/gpt-4-turbo等)',
|
||||
`parse_status` tinyint DEFAULT '0' COMMENT '解析是否成功(0-失败, 1-成功)',
|
||||
`function_calls` text COMMENT '解析出的函数调用列表(JSON)',
|
||||
`explanation` varchar(500) DEFAULT NULL COMMENT 'AI的理解说明',
|
||||
`confidence` decimal(3,2) DEFAULT NULL COMMENT '置信度(0.00-1.00)',
|
||||
`parse_error_message` text COMMENT '解析错误信息',
|
||||
`input_tokens` int DEFAULT NULL COMMENT '输入Token数量',
|
||||
`output_tokens` int DEFAULT NULL COMMENT '输出Token数量',
|
||||
`total_tokens` int DEFAULT NULL COMMENT '总Token数量',
|
||||
`parse_time` bigint DEFAULT NULL COMMENT '解析耗时(毫秒)',
|
||||
-- 执行相关字段
|
||||
`parse_duration_ms` int DEFAULT NULL COMMENT '解析耗时(毫秒)',
|
||||
`function_name` varchar(255) DEFAULT NULL COMMENT '执行的函数名称',
|
||||
`function_arguments` text COMMENT '函数参数(JSON)',
|
||||
`execute_status` varchar(20) DEFAULT NULL COMMENT '执行状态(pending-待执行, success-成功, failed-失败)',
|
||||
`execute_result` text COMMENT '执行结果(JSON)',
|
||||
`execute_status` tinyint(1) DEFAULT NULL COMMENT '执行状态(0-待执行, 1-成功, -1-失败)',
|
||||
`execute_error_message` text COMMENT '执行错误信息',
|
||||
`affected_rows` int DEFAULT NULL COMMENT '影响的记录数',
|
||||
`is_dangerous` tinyint(1) DEFAULT '0' COMMENT '是否危险操作(0-否, 1-是)',
|
||||
`requires_confirmation` tinyint(1) DEFAULT '0' COMMENT '是否需要确认(0-否, 1-是)',
|
||||
`user_confirmed` tinyint(1) DEFAULT NULL COMMENT '用户是否确认(0-否, 1-是)',
|
||||
`idempotency_key` varchar(128) DEFAULT NULL COMMENT '幂等性令牌(防止重复执行)',
|
||||
`execution_time` bigint DEFAULT NULL COMMENT '执行耗时(毫秒)',
|
||||
-- 通用字段
|
||||
`ip_address` varchar(128) DEFAULT NULL COMMENT 'IP地址',
|
||||
`user_agent` varchar(512) DEFAULT NULL COMMENT '用户代理',
|
||||
`current_route` varchar(255) DEFAULT NULL COMMENT '当前页面路由',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_idempotency_key` (`idempotency_key`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_create_time` (`create_time`),
|
||||
KEY `idx_provider` (`provider`),
|
||||
KEY `idx_model` (`model`),
|
||||
KEY `idx_parse_success` (`parse_success`),
|
||||
KEY `idx_execute_status` (`execute_status`),
|
||||
KEY `idx_is_dangerous` (`is_dangerous`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI命令记录表';
|
||||
KEY `idx_provider` (`ai_provider`),
|
||||
KEY `idx_model` (`ai_model`),
|
||||
KEY `idx_parse_success` (`parse_status`),
|
||||
KEY `idx_execute_status` (`execute_status`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 命令记录表';
|
||||
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
@@ -7,8 +7,8 @@ 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.model.vo.AiCommandLogVO;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandLogService;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -32,7 +32,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
public class AiCommandController {
|
||||
|
||||
private final AiCommandService aiCommandService;
|
||||
private final AiCommandRecordService recordService;
|
||||
private final AiCommandLogService logService;
|
||||
|
||||
@Operation(summary = "解析自然语言命令")
|
||||
@PostMapping("/parse")
|
||||
@@ -72,17 +72,17 @@ public class AiCommandController {
|
||||
|
||||
@Operation(summary = "获取AI命令记录分页列表")
|
||||
@GetMapping("/records")
|
||||
public PageResult<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||
IPage<AiCommandRecordVO> page = recordService.getRecordPage(queryParams);
|
||||
public PageResult<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
|
||||
IPage<AiCommandLogVO> page = logService.getLogPage(queryParams);
|
||||
return PageResult.success(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "撤销命令执行")
|
||||
@PostMapping("/rollback/{recordId}")
|
||||
@PostMapping("/rollback/{logId}")
|
||||
public Result<?> rollbackCommand(
|
||||
@Parameter(description = "记录ID") @PathVariable String recordId
|
||||
@Parameter(description = "记录ID") @PathVariable String logId
|
||||
) {
|
||||
recordService.rollbackCommand(recordId);
|
||||
logService.rollbackCommand(logId);
|
||||
return Result.success("撤销成功");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,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.entity.AiCommandLog;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* AI 命令记录 Mapper
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiCommandRecordMapper extends BaseMapper<AiCommandRecord> {
|
||||
public interface AiCommandLogMapper extends BaseMapper<AiCommandLog> {
|
||||
|
||||
/**
|
||||
* 获取 AI 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandRecordVO> getRecordPage(Page<AiCommandRecordVO> page, AiCommandPageQuery queryParams);
|
||||
IPage<AiCommandLogVO> getLogPage(Page<AiCommandLogVO> page, AiCommandPageQuery queryParams);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ import lombok.EqualsAndHashCode;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* AI 命令记录实体(合并解析和执行记录)
|
||||
* AI 命令记录实体
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("ai_command_record")
|
||||
public class AiCommandRecord extends BaseEntity {
|
||||
@TableName("ai_command_log")
|
||||
public class AiCommandLog extends BaseEntity {
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
@@ -30,13 +30,13 @@ public class AiCommandRecord extends BaseEntity {
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
/** AI 供应商(qwen/openai/deepseek等) */
|
||||
private String provider;
|
||||
private String aiProvider;
|
||||
|
||||
/** AI 模型(qwen-plus/qwen-max/gpt-4-turbo等) */
|
||||
private String model;
|
||||
private String aiModel;
|
||||
|
||||
/** 解析是否成功 */
|
||||
private Boolean parseSuccess;
|
||||
/** 解析状态(0-失败, 1-成功) */
|
||||
private Integer parseStatus;
|
||||
|
||||
/** 解析出的函数调用列表(JSON) */
|
||||
private String functionCalls;
|
||||
@@ -56,11 +56,8 @@ public class AiCommandRecord extends BaseEntity {
|
||||
/** 输出 Token 数量 */
|
||||
private Integer outputTokens;
|
||||
|
||||
/** 总 Token 数量 */
|
||||
private Integer totalTokens;
|
||||
|
||||
/** 解析耗时(毫秒) */
|
||||
private Long parseTime;
|
||||
private Integer parseDurationMs;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
@@ -70,46 +67,15 @@ public class AiCommandRecord extends BaseEntity {
|
||||
/** 函数参数(JSON) */
|
||||
private String functionArguments;
|
||||
|
||||
/** 执行状态:pending, success, failed */
|
||||
private String executeStatus;
|
||||
|
||||
/** 执行结果(JSON) */
|
||||
private String executeResult;
|
||||
/** 执行状态(0-待执行, 1-成功, -1-失败) */
|
||||
private Integer executeStatus;
|
||||
|
||||
/** 执行错误信息 */
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,19 +21,25 @@ public class AiCommandPageQuery extends BasePageQuery {
|
||||
@Schema(description = "关键字(原始命令/函数名称/用户名)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "执行状态(pending-待执行, success-成功, failed-失败)")
|
||||
private String executeStatus;
|
||||
@Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
|
||||
private Integer executeStatus;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "是否危险操作")
|
||||
private Boolean isDangerous;
|
||||
@Schema(description = "解析状态(0-失败, 1-成功)")
|
||||
private Integer parseStatus;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
private List<String> createTime;
|
||||
|
||||
@Schema(description = "函数名称")
|
||||
private String functionName;
|
||||
|
||||
@Schema(description = "AI供应商")
|
||||
private String aiProvider;
|
||||
|
||||
@Schema(description = "AI模型")
|
||||
private String aiModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,14 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* AI命令记录VO(合并解析和执行记录)
|
||||
* AI命令记录VO
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI命令记录VO")
|
||||
public class AiCommandRecordVO implements Serializable {
|
||||
public class AiCommandLogVO implements Serializable {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private String id;
|
||||
@@ -30,13 +33,13 @@ public class AiCommandRecordVO implements Serializable {
|
||||
// ==================== 解析相关字段 ====================
|
||||
|
||||
@Schema(description = "AI供应商")
|
||||
private String provider;
|
||||
private String aiProvider;
|
||||
|
||||
@Schema(description = "AI模型")
|
||||
private String model;
|
||||
private String aiModel;
|
||||
|
||||
@Schema(description = "解析是否成功")
|
||||
private Boolean parseSuccess;
|
||||
@Schema(description = "解析状态(0-失败, 1-成功)")
|
||||
private Integer parseStatus;
|
||||
|
||||
@Schema(description = "解析出的函数调用列表(JSON)")
|
||||
private String functionCalls;
|
||||
@@ -56,11 +59,8 @@ public class AiCommandRecordVO implements Serializable {
|
||||
@Schema(description = "输出Token数量")
|
||||
private Integer outputTokens;
|
||||
|
||||
@Schema(description = "总Token数量")
|
||||
private Integer totalTokens;
|
||||
|
||||
@Schema(description = "解析耗时(毫秒)")
|
||||
private Long parseTime;
|
||||
private Integer parseDurationMs;
|
||||
|
||||
// ==================== 执行相关字段 ====================
|
||||
|
||||
@@ -70,41 +70,17 @@ public class AiCommandRecordVO implements Serializable {
|
||||
@Schema(description = "函数参数(JSON)")
|
||||
private String functionArguments;
|
||||
|
||||
@Schema(description = "执行状态")
|
||||
private String executeStatus;
|
||||
|
||||
@Schema(description = "执行结果(JSON)")
|
||||
private String executeResult;
|
||||
@Schema(description = "执行状态(0-待执行, 1-成功, -1-失败)")
|
||||
private Integer executeStatus;
|
||||
|
||||
@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;
|
||||
@@ -112,9 +88,5 @@ public class AiCommandRecordVO implements Serializable {
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,17 @@ 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.entity.AiCommandLog;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
|
||||
|
||||
/**
|
||||
* AI 命令记录服务接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface AiCommandRecordService extends IService<AiCommandRecord> {
|
||||
public interface AiCommandLogService extends IService<AiCommandLog> {
|
||||
|
||||
/**
|
||||
* 获取命令记录分页列表
|
||||
@@ -17,14 +20,13 @@ public interface AiCommandRecordService extends IService<AiCommandRecord> {
|
||||
* @param queryParams 查询参数
|
||||
* @return 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams);
|
||||
IPage<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 撤销命令执行
|
||||
*
|
||||
* @param recordId 记录ID
|
||||
* @param logId 记录ID
|
||||
*/
|
||||
void rollbackCommand(String recordId);
|
||||
void rollbackCommand(String logId);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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.AiCommandLogMapper;
|
||||
import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
|
||||
import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery;
|
||||
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* AI 命令记录服务实现类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AiCommandLogServiceImpl extends ServiceImpl<AiCommandLogMapper, AiCommandLog>
|
||||
implements AiCommandLogService {
|
||||
|
||||
@Override
|
||||
public IPage<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
|
||||
Page<AiCommandLogVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
return this.baseMapper.getLogPage(page, queryParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollbackCommand(String logId) {
|
||||
AiCommandLog log = this.getById(logId);
|
||||
if (log == null) {
|
||||
throw new RuntimeException("命令记录不存在");
|
||||
}
|
||||
|
||||
if (log.getExecuteStatus() == null || log.getExecuteStatus() != 1) {
|
||||
throw new RuntimeException("只能撤销成功执行的命令");
|
||||
}
|
||||
|
||||
// TODO: 实现具体的回滚逻辑
|
||||
log.info("撤销命令执行: logId={}, function={}", logId, log.getFunctionName());
|
||||
throw new UnsupportedOperationException("回滚功能尚未实现");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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("回滚功能尚未实现");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
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.model.entity.AiCommandLog;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandLogService;
|
||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||
import com.youlai.boot.platform.ai.tools.UserTools;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
@@ -51,7 +50,7 @@ public class AiCommandServiceImpl implements AiCommandService {
|
||||
当无法识别命令时,success=false,并给出 error。
|
||||
""";
|
||||
|
||||
private final AiCommandRecordService recordService;
|
||||
private final AiCommandLogService logService;
|
||||
private final UserTools userTools;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@@ -72,14 +71,13 @@ public class AiCommandServiceImpl implements AiCommandService {
|
||||
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");
|
||||
AiCommandLog log = new AiCommandLog();
|
||||
log.setUserId(userId);
|
||||
log.setUsername(username);
|
||||
log.setOriginalCommand(command);
|
||||
log.setIpAddress(ipAddress);
|
||||
log.setAiProvider("spring-ai");
|
||||
log.setAiModel("auto");
|
||||
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String userPrompt = buildUserPrompt(request);
|
||||
@@ -97,19 +95,20 @@ public class AiCommandServiceImpl implements AiCommandService {
|
||||
|
||||
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);
|
||||
log.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
|
||||
log.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
|
||||
log.setParseStatus(parseResult.success() ? 1 : 0);
|
||||
log.setExplanation(parseResult.explanation());
|
||||
log.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
|
||||
log.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
|
||||
log.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
log.setParseDurationMs((int) duration);
|
||||
|
||||
recordService.save(record);
|
||||
logService.save(log);
|
||||
|
||||
AiParseResponseDTO response = AiParseResponseDTO.builder()
|
||||
.parseLogId(record.getId())
|
||||
.parseLogId(log.getId())
|
||||
.success(parseResult.success())
|
||||
.functionCalls(parseResult.functionCalls())
|
||||
.explanation(parseResult.explanation())
|
||||
@@ -121,17 +120,17 @@ public class AiCommandServiceImpl implements AiCommandService {
|
||||
if (!parseResult.success()) {
|
||||
log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
|
||||
} else {
|
||||
log.info("✅ 解析成功,审计记录ID: {}", record.getId());
|
||||
log.info("✅ 解析成功,审计记录ID: {}", log.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.setParseStatus(0);
|
||||
log.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
|
||||
log.setParseErrorMessage(e.getMessage());
|
||||
log.setParseDurationMs((int) duration);
|
||||
logService.save(log);
|
||||
|
||||
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
||||
@@ -232,98 +231,59 @@ public class AiCommandServiceImpl implements AiCommandService {
|
||||
|
||||
AiFunctionCallDTO functionCall = request.getFunctionCall();
|
||||
|
||||
// 判断是否为危险操作
|
||||
boolean isDangerous = isDangerousOperation(functionCall.getName());
|
||||
|
||||
// 根据解析日志ID获取审计记录,如果不存在则创建新记录
|
||||
AiCommandRecord record;
|
||||
AiCommandLog log;
|
||||
if (StrUtil.isNotBlank(request.getParseLogId())) {
|
||||
// 更新已存在的审计记录(解析阶段已创建)
|
||||
record = recordService.getById(request.getParseLogId());
|
||||
if (record == null) {
|
||||
log = logService.getById(request.getParseLogId());
|
||||
if (log == 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);
|
||||
log = new AiCommandLog();
|
||||
log.setUserId(userId);
|
||||
log.setUsername(username);
|
||||
log.setOriginalCommand(request.getOriginalCommand());
|
||||
log.setIpAddress(ipAddress);
|
||||
logService.save(log);
|
||||
}
|
||||
|
||||
// 更新执行相关字段
|
||||
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");
|
||||
log.setFunctionName(functionCall.getName());
|
||||
log.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
|
||||
log.setExecuteStatus(0); // 0-待执行
|
||||
|
||||
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("该操作已执行,请勿重复提交");
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 执行具体的函数调用
|
||||
Object result = executeFunctionCall(functionCall);
|
||||
|
||||
// 更新执行成功
|
||||
record.setExecuteStatus("success");
|
||||
record.setExecuteResult(JSONUtil.toJsonStr(result));
|
||||
record.setExecutionTime(System.currentTimeMillis() - startTime);
|
||||
log.setExecuteStatus(1); // 1-成功
|
||||
log.setExecuteErrorMessage(null);
|
||||
|
||||
// 更新审计记录
|
||||
recordService.updateById(record);
|
||||
logService.updateById(log);
|
||||
|
||||
log.info("✅ 命令执行成功,审计记录ID: {}", record.getId());
|
||||
log.info("✅ 命令执行成功,审计记录ID: {}", log.getId());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 更新执行失败
|
||||
record.setExecuteStatus("failed");
|
||||
record.setExecuteErrorMessage(e.getMessage());
|
||||
record.setExecutionTime(System.currentTimeMillis() - startTime);
|
||||
log.setExecuteStatus(-1); // -1-失败
|
||||
log.setExecuteErrorMessage(e.getMessage());
|
||||
|
||||
// 更新审计记录
|
||||
recordService.updateById(record);
|
||||
logService.updateById(log);
|
||||
|
||||
log.error("❌ 命令执行失败,审计记录ID: {}", record.getId(), e);
|
||||
log.error("❌ 命令执行失败,审计记录ID: {}", log.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行具体的函数调用
|
||||
*/
|
||||
|
||||
96
src/main/resources/mapper/ai/AiCommandLogMapper.xml
Normal file
96
src/main/resources/mapper/ai/AiCommandLogMapper.xml
Normal file
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.youlai.boot.platform.ai.mapper.AiCommandLogMapper">
|
||||
|
||||
<resultMap id="AiCommandLogVOResult" type="com.youlai.boot.platform.ai.model.vo.AiCommandLogVO">
|
||||
<id property="id" column="id"/>
|
||||
<result property="userId" column="user_id"/>
|
||||
<result property="username" column="username"/>
|
||||
<result property="originalCommand" column="original_command"/>
|
||||
<result property="aiProvider" column="ai_provider"/>
|
||||
<result property="aiModel" column="ai_model"/>
|
||||
<result property="parseStatus" column="parse_status"/>
|
||||
<result property="functionCalls" column="function_calls"/>
|
||||
<result property="explanation" column="explanation"/>
|
||||
<result property="confidence" column="confidence"/>
|
||||
<result property="parseErrorMessage" column="parse_error_message"/>
|
||||
<result property="inputTokens" column="input_tokens"/>
|
||||
<result property="outputTokens" column="output_tokens"/>
|
||||
<result property="parseDurationMs" column="parse_duration_ms"/>
|
||||
<result property="functionName" column="function_name"/>
|
||||
<result property="functionArguments" column="function_arguments"/>
|
||||
<result property="executeStatus" column="execute_status"/>
|
||||
<result property="executeErrorMessage" column="execute_error_message"/>
|
||||
<result property="ipAddress" column="ip_address"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="getLogPage" resultMap="AiCommandLogVOResult">
|
||||
SELECT
|
||||
acl.id,
|
||||
acl.user_id,
|
||||
acl.username,
|
||||
acl.original_command,
|
||||
acl.ai_provider,
|
||||
acl.ai_model,
|
||||
acl.parse_status,
|
||||
acl.function_calls,
|
||||
acl.explanation,
|
||||
acl.confidence,
|
||||
acl.parse_error_message,
|
||||
acl.input_tokens,
|
||||
acl.output_tokens,
|
||||
acl.parse_duration_ms,
|
||||
acl.function_name,
|
||||
acl.function_arguments,
|
||||
acl.execute_status,
|
||||
acl.execute_error_message,
|
||||
acl.ip_address,
|
||||
acl.create_time,
|
||||
acl.update_time
|
||||
FROM ai_command_log acl
|
||||
<where>
|
||||
<if test="queryParams.keywords != null and queryParams.keywords != ''">
|
||||
(
|
||||
acl.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acl.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acl.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="queryParams.executeStatus != null">
|
||||
AND acl.execute_status = #{queryParams.executeStatus}
|
||||
</if>
|
||||
<if test="queryParams.userId != null">
|
||||
AND acl.user_id = #{queryParams.userId}
|
||||
</if>
|
||||
<if test="queryParams.parseStatus != null">
|
||||
AND acl.parse_status = #{queryParams.parseStatus}
|
||||
</if>
|
||||
<if test="queryParams.functionName != null and queryParams.functionName != ''">
|
||||
AND acl.function_name = #{queryParams.functionName}
|
||||
</if>
|
||||
<if test="queryParams.aiProvider != null and queryParams.aiProvider != ''">
|
||||
AND acl.ai_provider = #{queryParams.aiProvider}
|
||||
</if>
|
||||
<if test="queryParams.aiModel != null and queryParams.aiModel != ''">
|
||||
AND acl.ai_model = #{queryParams.aiModel}
|
||||
</if>
|
||||
<if test="
|
||||
queryParams.createTime != null
|
||||
and queryParams.createTime.size() == 2
|
||||
and queryParams.createTime[0] != null
|
||||
and queryParams.createTime[0] != ''
|
||||
and queryParams.createTime[1] != null
|
||||
and queryParams.createTime[1] != ''
|
||||
">
|
||||
AND acl.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY acl.create_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper">
|
||||
|
||||
<resultMap id="AiCommandRecordVOResult" type="com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO">
|
||||
<id property="id" column="id"/>
|
||||
<result property="userId" column="user_id"/>
|
||||
<result property="username" column="username"/>
|
||||
<result property="originalCommand" column="original_command"/>
|
||||
<result property="provider" column="provider"/>
|
||||
<result property="model" column="model"/>
|
||||
<result property="parseSuccess" column="parse_success"/>
|
||||
<result property="functionCalls" column="function_calls"/>
|
||||
<result property="explanation" column="explanation"/>
|
||||
<result property="confidence" column="confidence"/>
|
||||
<result property="parseErrorMessage" column="parse_error_message"/>
|
||||
<result property="inputTokens" column="input_tokens"/>
|
||||
<result property="outputTokens" column="output_tokens"/>
|
||||
<result property="totalTokens" column="total_tokens"/>
|
||||
<result property="parseTime" column="parse_time"/>
|
||||
<result property="functionName" column="function_name"/>
|
||||
<result property="functionArguments" column="function_arguments"/>
|
||||
<result property="executeStatus" column="execute_status"/>
|
||||
<result property="executeResult" column="execute_result"/>
|
||||
<result property="executeErrorMessage" column="execute_error_message"/>
|
||||
<result property="affectedRows" column="affected_rows"/>
|
||||
<result property="isDangerous" column="is_dangerous"/>
|
||||
<result property="requiresConfirmation" column="requires_confirmation"/>
|
||||
<result property="userConfirmed" column="user_confirmed"/>
|
||||
<result property="executionTime" column="execution_time"/>
|
||||
<result property="ipAddress" column="ip_address"/>
|
||||
<result property="userAgent" column="user_agent"/>
|
||||
<result property="currentRoute" column="current_route"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="remark" column="remark"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="getRecordPage" resultMap="AiCommandRecordVOResult">
|
||||
SELECT
|
||||
acr.id,
|
||||
acr.user_id,
|
||||
acr.username,
|
||||
acr.original_command,
|
||||
acr.provider,
|
||||
acr.`model`,
|
||||
acr.parse_success,
|
||||
acr.function_calls,
|
||||
acr.explanation,
|
||||
acr.confidence,
|
||||
acr.parse_error_message,
|
||||
acr.input_tokens,
|
||||
acr.output_tokens,
|
||||
acr.total_tokens,
|
||||
acr.parse_time,
|
||||
acr.function_name,
|
||||
acr.function_arguments,
|
||||
acr.execute_status,
|
||||
acr.execute_result,
|
||||
acr.execute_error_message,
|
||||
acr.affected_rows,
|
||||
acr.is_dangerous,
|
||||
acr.requires_confirmation,
|
||||
acr.user_confirmed,
|
||||
acr.execution_time,
|
||||
acr.ip_address,
|
||||
acr.user_agent,
|
||||
acr.current_route,
|
||||
acr.create_time,
|
||||
acr.update_time,
|
||||
acr.remark
|
||||
FROM ai_command_record acr
|
||||
<where>
|
||||
<if test="queryParams.keywords != null and queryParams.keywords != ''">
|
||||
(
|
||||
acr.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acr.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acr.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="queryParams.executeStatus != null and queryParams.executeStatus != ''">
|
||||
AND acr.execute_status = #{queryParams.executeStatus}
|
||||
</if>
|
||||
<if test="queryParams.userId != null">
|
||||
AND acr.user_id = #{queryParams.userId}
|
||||
</if>
|
||||
<if test="queryParams.isDangerous != null">
|
||||
AND acr.is_dangerous = #{queryParams.isDangerous}
|
||||
</if>
|
||||
<if test="queryParams.functionName != null and queryParams.functionName != ''">
|
||||
AND acr.function_name = #{queryParams.functionName}
|
||||
</if>
|
||||
<if test="
|
||||
queryParams.createTime != null
|
||||
and queryParams.createTime.size() == 2
|
||||
and queryParams.createTime[0] != null
|
||||
and queryParams.createTime[0] != ''
|
||||
and queryParams.createTime[1] != null
|
||||
and queryParams.createTime[1] != ''
|
||||
">
|
||||
AND acr.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY acr.create_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user