本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

194 lines
8.0 KiB

package com.wok.supportbot.controller;
import com.wok.supportbot.entity.LlmCallTrace;
import com.wok.supportbot.service.LlmCallTraceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* LLM 调用追踪接口(仅 admin 可访问)。
* <p>
* 提供调用记录的分页查询、详情、聚合统计与清理。
* 端点仅挂管理路径(/llm-trace),绝不进入 /ai/** 或 /open-api/**(SDK 开放路径),
* 防止 conversation_id 跨租户越权。
*/
@Slf4j
@RestController
@RequestMapping("/llm-trace")
@PreAuthorize("hasRole('admin')")
public class LlmCallTraceController {
@Autowired
private LlmCallTraceService llmCallTraceService;
/** 清理的最小保留天数(防止误清全部) */
private static final int MIN_KEEP_DAYS = 1;
/** 清理的最大保留天数(防止超过 PostgreSQL make_interval 合理范围) */
private static final int MAX_KEEP_DAYS = 3650;
/**
* 分页查询调用记录(列表不含大 TEXT 字段)。
*/
@GetMapping("/list")
public ResponseEntity<Map<String, Object>> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) Long roleId,
@RequestParam(required = false) String conversationId,
@RequestParam(required = false) String intent,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String errorType,
@RequestParam(required = false) String sortField,
@RequestParam(required = false) String sortOrder) {
try {
Map<String, Object> result = llmCallTraceService.pageQuery(
page, size, roleId, conversationId, intent, startTime, endTime, keyword, errorType, sortField, sortOrder);
Map<String, Object> data = new LinkedHashMap<>();
data.put("success", true);
data.put("data", result.get("records"));
data.put("total", result.get("total"));
data.put("page", result.get("page"));
data.put("size", result.get("size"));
data.put("pages", result.get("pages"));
return ResponseEntity.ok(data);
} catch (Exception e) {
log.error("查询 LLM 调用追踪失败", e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 单条详情(含完整 system prompt)。
*/
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> detail(@PathVariable Long id) {
try {
LlmCallTrace trace = llmCallTraceService.getDetail(id);
if (trace == null) {
return ResponseEntity.status(404).body(Map.of(
"success", false,
"message", "调用记录不存在"
));
}
return ResponseEntity.ok(Map.of("success", true, "data", trace));
} catch (Exception e) {
log.error("查询 LLM 调用追踪详情失败: id={}", id, e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "查询失败:" + e.getMessage()
));
}
}
/**
* 清理 N 天前的记录(POST,避免 DELETE + RequestBody 路径冲突)。
* 保留天数设下限,防止误清全部。
*/
@PostMapping("/clean")
public ResponseEntity<Map<String, Object>> clean(@RequestBody Map<String, Object> body) {
try {
Object raw = body.get("keepDays");
int keepDays = raw instanceof Number n ? n.intValue() : Integer.parseInt(String.valueOf(raw));
if (keepDays < MIN_KEEP_DAYS || keepDays > MAX_KEEP_DAYS) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "保留天数必须在 " + MIN_KEEP_DAYS + " ~ " + MAX_KEEP_DAYS + " 天之间"
));
}
int deleted = llmCallTraceService.cleanBefore(keepDays);
log.info("管理员清理 LLM 调用追踪:操作人={}, 保留 {} 天, 删除 {} 条", currentUser(), keepDays, deleted);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "清理完成",
"deleted", deleted
));
} catch (Exception e) {
log.error("清理 LLM 调用追踪失败", e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "清理失败:" + e.getMessage()
));
}
}
/**
* 聚合统计(按角色或模型分组)。
*/
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> stats(@RequestParam(defaultValue = "role") String groupBy) {
try {
List<Map<String, Object>> rows = llmCallTraceService.stats(groupBy);
return ResponseEntity.ok(Map.of("success", true, "data", rows));
} catch (Exception e) {
log.error("查询 LLM 调用追踪统计失败", e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "统计失败:" + e.getMessage()
));
}
}
/**
* 可选错误类型列表(供前端筛选下拉)。
*/
@GetMapping("/error-types")
public ResponseEntity<Map<String, Object>> errorTypes() {
List<Map<String, String>> types = List.of(
Map.of("label", "LLM 调用失败", "value", "LLM_API"),
Map.of("label", "MCP 工具异常", "value", "MCP"),
Map.of("label", "熔断降级", "value", "CIRCUIT_BREAK"),
Map.of("label", "参数校验", "value", "VALIDATION"),
Map.of("label", "未知", "value", "UNKNOWN")
);
return ResponseEntity.ok(Map.of("success", true, "data", types));
}
/**
* 时间维度趋势统计(按小时或天聚合)。
*/
@GetMapping("/trend")
public ResponseEntity<Map<String, Object>> trend(
@RequestParam(defaultValue = "HOUR") String groupBy,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime) {
try {
List<Map<String, Object>> rows = llmCallTraceService.trend(groupBy, startTime, endTime);
return ResponseEntity.ok(Map.of("success", true, "data", rows));
} catch (Exception e) {
log.error("查询 LLM 调用追踪趋势失败", e);
return ResponseEntity.status(500).body(Map.of(
"success", false,
"message", "统计失败:" + e.getMessage()
));
}
}
/**
* 获取当前登录用户名(用于审计日志,best-effort)。
*/
private String currentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getName() : "unknown";
}
}