|
|
@@ -1,5 +1,6 @@
|
|
|
package com.etotem.cfc.controller.admin;
|
|
|
|
|
|
+import com.etotem.cfc.common.AdminRoles;
|
|
|
import com.etotem.cfc.common.Result;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
@@ -10,8 +11,14 @@ import org.springframework.http.MediaType;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
|
|
|
/**
|
|
|
* 向量知识库管理 Controller
|
|
|
@@ -23,26 +30,80 @@ public class KnowledgeBaseVectorController {
|
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(KnowledgeBaseVectorController.class);
|
|
|
|
|
|
+ /** 允许维护向量知识库的角色(与统一知识库一致:平台角色 + 知识管理员 + 管家) */
|
|
|
+ private static final Set<String> ALLOWED_ROLES = new HashSet<>(Arrays.asList(
|
|
|
+ "admin", "operator", "finance", "knowledge_admin", "butler"));
|
|
|
+
|
|
|
@Value("${python.base-url:http://localhost:9000}")
|
|
|
private String langgraphBaseUrl;
|
|
|
|
|
|
private final RestTemplate restTemplate = new RestTemplate();
|
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private List<String> roles(HttpServletRequest request) {
|
|
|
+ Object rolesAttr = request.getAttribute("roles");
|
|
|
+ if (rolesAttr instanceof List) {
|
|
|
+ return (List<String>) rolesAttr;
|
|
|
+ }
|
|
|
+ Object roleAttr = request.getAttribute("role");
|
|
|
+ if (roleAttr != null) {
|
|
|
+ return Collections.singletonList(roleAttr.toString());
|
|
|
+ }
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean allowed(HttpServletRequest request) {
|
|
|
+ List<String> r = roles(request);
|
|
|
+ if (r == null || r.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (String raw : r) {
|
|
|
+ if (ALLOWED_ROLES.contains(AdminRoles.normalize(raw))) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
private HttpHeaders createJsonHeaders() {
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
return headers;
|
|
|
}
|
|
|
|
|
|
+ /** 同步失败重试(契约 SC-A-08 边界):HTTP 调用失败时按 maxRetries 重试 */
|
|
|
+ private Map<String, Object> postWithRetry(String url, HttpEntity<?> entity, int maxRetries) {
|
|
|
+ Exception last = null;
|
|
|
+ for (int attempt = 1; attempt <= Math.max(1, maxRetries); attempt++) {
|
|
|
+ try {
|
|
|
+ return restTemplate.postForEntity(url, entity, Map.class).getBody();
|
|
|
+ } catch (Exception e) {
|
|
|
+ last = e;
|
|
|
+ if (attempt < Math.max(1, maxRetries)) {
|
|
|
+ log.warn("调用[{}]第 {}/{} 次失败,将重试: {}", url, attempt, Math.max(1, maxRetries), e.getMessage());
|
|
|
+ try {
|
|
|
+ Thread.sleep((long) (300L * attempt));
|
|
|
+ } catch (InterruptedException ignored) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new RuntimeException("调用[" + url + "]重试 " + Math.max(1, maxRetries) + " 次后仍失败", last);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取向量库统计信息
|
|
|
*/
|
|
|
@PostMapping("/stats")
|
|
|
- public Result<Map<String, Object>> getStats() {
|
|
|
+ public Result<Map<String, Object>> getStats(HttpServletRequest request) {
|
|
|
+ if (!allowed(request)) {
|
|
|
+ return Result.error("无权限维护知识库");
|
|
|
+ }
|
|
|
try {
|
|
|
String url = langgraphBaseUrl + "/api/v1/knowledge-base/stats";
|
|
|
HttpEntity<String> entity = new HttpEntity<>(createJsonHeaders());
|
|
|
- Map<String, Object> response = restTemplate.postForEntity(url, entity, Map.class).getBody();
|
|
|
+ Map<String, Object> response = postWithRetry(url, entity, 1);
|
|
|
return Result.success(response);
|
|
|
} catch (Exception e) {
|
|
|
log.warn("获取向量库统计失败: {}", e.getMessage());
|
|
|
@@ -54,11 +115,14 @@ public class KnowledgeBaseVectorController {
|
|
|
* 分页列出向量库文档
|
|
|
*/
|
|
|
@PostMapping("/documents")
|
|
|
- public Result<Map<String, Object>> listDocuments(@RequestBody Map<String, Object> params) {
|
|
|
+ public Result<Map<String, Object>> listDocuments(@RequestBody Map<String, Object> params, HttpServletRequest request) {
|
|
|
+ if (!allowed(request)) {
|
|
|
+ return Result.error("无权限维护知识库");
|
|
|
+ }
|
|
|
try {
|
|
|
String url = langgraphBaseUrl + "/api/v1/knowledge-base/documents";
|
|
|
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(params, createJsonHeaders());
|
|
|
- Map<String, Object> response = restTemplate.postForEntity(url, entity, Map.class).getBody();
|
|
|
+ Map<String, Object> response = postWithRetry(url, entity, 1);
|
|
|
return Result.success(response);
|
|
|
} catch (Exception e) {
|
|
|
log.warn("列出文档失败: {}", e.getMessage());
|
|
|
@@ -70,7 +134,10 @@ public class KnowledgeBaseVectorController {
|
|
|
* 删除指定 source 的所有文档
|
|
|
*/
|
|
|
@PostMapping("/delete-source")
|
|
|
- public Result<Map<String, Object>> deleteBySource(@RequestBody Map<String, String> params) {
|
|
|
+ public Result<Map<String, Object>> deleteBySource(@RequestBody Map<String, String> params, HttpServletRequest request) {
|
|
|
+ if (!allowed(request)) {
|
|
|
+ return Result.error("无权限维护知识库");
|
|
|
+ }
|
|
|
String source = params.get("source");
|
|
|
if (source == null || source.isEmpty()) {
|
|
|
return Result.error("source 不能为空");
|
|
|
@@ -87,14 +154,26 @@ public class KnowledgeBaseVectorController {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 触发完整同步
|
|
|
+ * 触发完整同步(接受重试配置参数 maxRetries,契约 SC-A-08 边界:同步失败重试)
|
|
|
*/
|
|
|
@PostMapping("/sync")
|
|
|
- public Result<Map<String, Object>> triggerSync() {
|
|
|
+ public Result<Map<String, Object>> triggerSync(@RequestBody(required = false) Map<String, Object> params,
|
|
|
+ HttpServletRequest request) {
|
|
|
+ if (!allowed(request)) {
|
|
|
+ return Result.error("无权限维护知识库");
|
|
|
+ }
|
|
|
+ int maxRetries = 3;
|
|
|
+ if (params != null && params.get("maxRetries") != null) {
|
|
|
+ try {
|
|
|
+ maxRetries = ((Number) params.get("maxRetries")).intValue();
|
|
|
+ } catch (Exception e) {
|
|
|
+ maxRetries = 3;
|
|
|
+ }
|
|
|
+ }
|
|
|
try {
|
|
|
String url = langgraphBaseUrl + "/api/v1/knowledge-base/sync";
|
|
|
HttpEntity<String> entity = new HttpEntity<>(createJsonHeaders());
|
|
|
- Map<String, Object> response = restTemplate.postForEntity(url, entity, Map.class).getBody();
|
|
|
+ Map<String, Object> response = postWithRetry(url, entity, maxRetries);
|
|
|
return Result.success(response);
|
|
|
} catch (Exception e) {
|
|
|
log.warn("触发同步失败: {}", e.getMessage());
|
|
|
@@ -106,7 +185,10 @@ public class KnowledgeBaseVectorController {
|
|
|
* 更新单个文档
|
|
|
*/
|
|
|
@PostMapping("/update-document")
|
|
|
- public Result<Map<String, Object>> updateDocument(@RequestBody Map<String, Object> params) {
|
|
|
+ public Result<Map<String, Object>> updateDocument(@RequestBody Map<String, Object> params, HttpServletRequest request) {
|
|
|
+ if (!allowed(request)) {
|
|
|
+ return Result.error("无权限维护知识库");
|
|
|
+ }
|
|
|
String docId = (String) params.get("docId");
|
|
|
if (docId == null || docId.isEmpty()) {
|
|
|
return Result.error("docId 不能为空");
|