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
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";
|
|
}
|
|
}
|