fix: 修复PR审查意见 - 新增单文档删除接口、删除BotController、优化imports、修复i18n

# Conflicts:
#	main/manager-api/src/main/resources/i18n/messages_de_DE.properties
#	main/manager-api/src/main/resources/i18n/messages_en_US.properties
#	main/manager-api/src/main/resources/i18n/messages_vi_VN.properties
This commit is contained in:
GZH
2026-03-04 17:45:58 +08:00
parent 5913dbbb7b
commit f7bf29a5e4
8 changed files with 64 additions and 175 deletions
@@ -2,7 +2,9 @@ package xiaozhi.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
/**
* RestTemplate配置
@@ -12,8 +14,8 @@ public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
org.springframework.http.client.JdkClientHttpRequestFactory factory = new org.springframework.http.client.JdkClientHttpRequestFactory();
factory.setReadTimeout(java.time.Duration.ofSeconds(30));
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory();
factory.setReadTimeout(Duration.ofSeconds(30));
return new RestTemplate(factory);
}
}
@@ -1,148 +0,0 @@
//package xiaozhi.modules.knowledge.controller;
//
//import java.util.HashMap;
//import java.util.Map;
//
//import org.apache.commons.lang3.StringUtils;
//import org.springframework.http.MediaType;
//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.RestController;
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
//
//import io.swagger.v3.oas.annotations.Operation;
//import io.swagger.v3.oas.annotations.tags.Tag;
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import xiaozhi.common.exception.ErrorCode;
//import xiaozhi.common.exception.RenException;
//import xiaozhi.modules.agent.entity.AgentEntity;
//import xiaozhi.modules.agent.service.AgentService;
//import xiaozhi.modules.knowledge.dto.bot.BotDTO;
//import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapter;
//import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapterFactory;
//import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
//import xiaozhi.modules.model.entity.ModelConfigEntity;
//import xiaozhi.modules.model.service.ModelConfigService;
//
//@Tag(name = "外部机器人服务")
//@RestController
//@RequestMapping("/api/v1") // 保持与外部 API 一致的风格,或者映射到 RAGFlow 风格
//@RequiredArgsConstructor
//@Slf4j
//public class BotController {//todo:这个类还有待完成,待完善。这里的agent和其他模块的agent不一样,这里的agent是存在于ragflow中等agent/工作流,不独立存在,需要借助rag作为平台进行交流交互
//
// private final AgentService agentService;
// private final ModelConfigService modelConfigService;
// private final KnowledgeBaseService knowledgeBaseService;
//
// // SearchBot (暂未完全实现,预留接口)
// @Operation(summary = "SearchBot 提问")
// @PostMapping(value = "/searchbots/ask", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
// public SseEmitter searchBotAsk(@RequestBody BotDTO.SearchAskReq request) {
// // TODO: SearchBot 通常需要指定 Dataset 或 KnowledgeBase
// // 这里需要明确 SearchBot 的 ID 来源。
// // 暂时返回未实现
// throw new RenException("SearchBot API 暂未开放");
// }
//
// // AgentBot
// @Operation(summary = "AgentBot 对话 (流式)")
// @PostMapping(value = "/agentbots/{id}/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
// public SseEmitter agentBotCompletion(@PathVariable("id") String agentId,
// @RequestBody BotDTO.AgentCompletionReq request) {
// SseEmitter emitter = new SseEmitter(5 * 60 * 1000L); // 5 min timeout
//
// new Thread(() -> {
// try {
// // 1. 获取本地 Agent
// // 注意:这里的 id 是本地 AgentId
// AgentEntity agent = agentService.getAgentById(agentId); // getAgentById actually returns VO but we need
// // Entity fields
// // 直接查 DB 或用 Service
// // Service returns AgentInfoVO, let's better use getById from BaseService (which
// // returns Entity) if exposed
// // AgentService extends BaseService<AgentEntity>
// AgentEntity entity = agentService.selectById(agentId);
//
// if (entity == null) {
// throw new RenException(ErrorCode.AGENT_NOT_FOUND);
// }
//
// // 2. 获取关联的 RAG 配置
// String llmModelId = entity.getLlmModelId();
// if (StringUtils.isBlank(llmModelId)) {
// throw new RenException("Agent未配置LLM模型");
// }
//
// // 3. 所有的 AgentBot 请求都通过关联的 RAGConfig 下发
// Map<String, Object> ragConfig = knowledgeBaseService.getRAGConfig(llmModelId);
// String adapterType = (String) ragConfig.getOrDefault("type", "ragflow");
// KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
//
// // 4. 发起请求
// // 注意:这里需要传递给 RAGFlow 的 Agent ID 可能是 config 中的某些字段,
// // 或者我们这里构建的 Agent 实际上是 RAGFlow 中的 Agent
// // 根据 Deep DiveChatService 是自己组装 prompt 调用 chat/completions。
// // 而 BotController 似乎旨在暴露 RAGFlow 的 AgentBot 能力。
// // 如果我们是“Java主导”,那么其实这里的逻辑应该和 ChatService 类似:
// // 即:我们自己组装参数,调用 RAGFlow 的 Chat/Completion 接口,而不是调用 AgentBot 接口。
// // 除非 RAGFlow 侧真的有一个 "Agent" 对象对应我们的 Agent。
// // 鉴于 Step 8 中我们决定 "Java-Side Master",且 RAGFlow 是推理引擎,
// // 我们应该复用 ChatService 的逻辑,或者调用 POST /chat/completions (RAGFlow)
//
// // 但为了符合 BotDTO 的定义 (/agentbots/{id}/completions),我们这里模拟这个行为
// // 且尽量复用 ChatService 的流式能力。
//
// // 这里的 adapter.postAgentBotCompletion 实现的是调用 /api/v1/agentbots/{id}/completions
// // 这要求 RAGFlow 侧必须存在这个 ID。
// // 但我们的 Agent 是本地的。
// // **修正策略**
// // 如果 RAGFlow 侧没有对应 Agent,我们不能调这个接口。
// // 我们应该降级为调用 Chat 接口 (/chat/completions),带上 system prompt。
//
// // 出于本步骤目标 "Bot Service (Public Access)",我们为了兼容外部 Bot 协议,
// // 应该要把本地请求转换为 RAGFlow 的 Chat 请求。
//
// Map<String, Object> chatBody = new HashMap<>();
// chatBody.put("messages", java.util.List.of(
// Map.of("role", "user", "content", request.getQuestion())));
// chatBody.put("stream", request.getStream());
// chatBody.put("model", entity.getAgentName()); // Mock name
// if (StringUtils.isNotBlank(entity.getSystemPrompt())) {
// chatBody.put("system_prompt", entity.getSystemPrompt());
// }
//
// adapter.postStream("/api/v1/chat/completions", chatBody, line -> {
// try {
// if (line.startsWith("data:")) {
// String dataContent = line.substring(5).trim();
// if (!"[DONE]".equals(dataContent)) {
// emitter.send(SseEmitter.event().data(dataContent));
// }
// }
// } catch (Exception e) {
// log.error("SSE Error", e);
// throw new RuntimeException(e);
// }
// });
//
// emitter.send(SseEmitter.event().data("[DONE]"));
// emitter.complete();
//
// } catch (Exception e) {
// log.error("AgentBot Error", e);
// try {
// emitter.send(SseEmitter.event().name("error").data(e.getMessage()));
// emitter.completeWithError(e);
// } catch (Exception ex) {
// // ignore
// }
// }
// }).start();
//
// return emitter;
// }
//}
@@ -26,6 +26,7 @@ import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ToolUtil;
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
import xiaozhi.modules.knowledge.service.KnowledgeManagerService;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.security.user.SecurityUser;
@@ -36,7 +37,7 @@ import xiaozhi.modules.security.user.SecurityUser;
public class KnowledgeBaseController {
private final KnowledgeBaseService knowledgeBaseService;
private final xiaozhi.modules.knowledge.service.KnowledgeManagerService knowledgeManagerService;
private final KnowledgeManagerService knowledgeManagerService;
@GetMapping
@Operation(summary = "分页查询知识库列表")
@@ -126,6 +126,20 @@ public class KnowledgeFilesController {
return new Result<>();
}
@DeleteMapping("/documents/{document_id}")
@Operation(summary = "删除单个文档")
@RequiresPermissions("sys:role:normal")
public Result<Void> deleteSingle(@PathVariable("dataset_id") String datasetId,
@PathVariable("document_id") String documentId) {
// 验证知识库权限
validateKnowledgeBasePermission(datasetId);
DocumentDTO.BatchIdReq req = new DocumentDTO.BatchIdReq();
req.setIds(java.util.Collections.singletonList(documentId));
knowledgeFilesService.deleteDocuments(datasetId, req);
return new Result<>();
}
@PostMapping("/chunks")
@Operation(summary = "解析文档(切块)")
@RequiresPermissions("sys:role:normal")
@@ -8,6 +8,9 @@ import xiaozhi.modules.knowledge.dto.dataset.DatasetDTO;
import xiaozhi.common.page.PageData;
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
import xiaozhi.modules.knowledge.dto.document.DocumentDTO;
import xiaozhi.modules.knowledge.dto.document.ChunkDTO;
import xiaozhi.modules.knowledge.dto.document.RetrievalDTO;
import java.util.function.Consumer;
/**
* 知识库API适配器抽象基类
@@ -105,9 +108,9 @@ public abstract class KnowledgeBaseAdapter {
* @param req 列表请求参数 (分页、关键词等)
* @return 切片列表VO
*/
public abstract xiaozhi.modules.knowledge.dto.document.ChunkDTO.ListVO listChunks(String datasetId,
public abstract ChunkDTO.ListVO listChunks(String datasetId,
String documentId,
xiaozhi.modules.knowledge.dto.document.ChunkDTO.ListReq req);
ChunkDTO.ListReq req);
/**
* 召回测试 - 从知识库中检索相关切片
@@ -115,8 +118,8 @@ public abstract class KnowledgeBaseAdapter {
* @param req 检索测试请求参数
* @return 召回测试结果
*/
public abstract xiaozhi.modules.knowledge.dto.document.RetrievalDTO.ResultVO retrievalTest(
xiaozhi.modules.knowledge.dto.document.RetrievalDTO.TestReq req);
public abstract RetrievalDTO.ResultVO retrievalTest(
RetrievalDTO.TestReq req);
/**
* 测试连接
@@ -186,7 +189,7 @@ public abstract class KnowledgeBaseAdapter {
* @param body 请求体
* @param onData 数据回调
*/
public abstract void postStream(String endpoint, Object body, java.util.function.Consumer<String> onData);
public abstract void postStream(String endpoint, Object body, Consumer<String> onData);
/**
* SearchBot 提问
@@ -197,7 +200,7 @@ public abstract class KnowledgeBaseAdapter {
* @return 响应对象
*/
public abstract Object postSearchBotAsk(Map<String, Object> config, Object body,
java.util.function.Consumer<String> onData);
Consumer<String> onData);
/**
* AgentBot 对话
@@ -208,5 +211,5 @@ public abstract class KnowledgeBaseAdapter {
* @param onData 数据回调
*/
public abstract void postAgentBotCompletion(Map<String, Object> config, String agentId, Object body,
java.util.function.Consumer<String> onData);
Consumer<String> onData);
}
@@ -13,6 +13,21 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import java.util.Locale;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -45,8 +60,8 @@ public class RAGFlowClient {
this.objectMapper = new ObjectMapper();
// [Reinforce] 兼容 RAGFlow 返回的 RFC 1123 日期格式 (如: Tue, 10 Feb 2026 10:27:35 GMT)
this.objectMapper
.setDateFormat(new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", java.util.Locale.US));
this.objectMapper.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
.setDateFormat(new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US));
this.objectMapper.setTimeZone(TimeZone.getTimeZone("GMT"));
// 优先从 Spring 上下文中获取池化的 RestTemplate Bean (Issue 3: 连接池化)
RestTemplate pooledTemplate = null;
@@ -62,7 +77,7 @@ public class RAGFlowClient {
} else {
// 兜底方案:配置超时并创建简单 RestTemplate
log.info("RAGFlowClient 初始化: 使用独立 RestTemplate (Debug Mode)");
org.springframework.http.client.SimpleClientHttpRequestFactory factory = new org.springframework.http.client.SimpleClientHttpRequestFactory();
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(timeoutSeconds * 1000);
factory.setReadTimeout(timeoutSeconds * 1000);
this.restTemplate = new RestTemplate(factory);
@@ -194,10 +209,10 @@ public class RAGFlowClient {
if (v != null) {
try {
sb.append(k).append("=")
.append(java.net.URLEncoder.encode(v.toString(),
.append(URLEncoder.encode(v.toString(),
StandardCharsets.UTF_8.name()))
.append("&");
} catch (java.io.UnsupportedEncodingException e) {
} catch (UnsupportedEncodingException e) {
log.warn("参数编码失败: k={}, v={}", k, v);
sb.append(k).append("=").append(v).append("&");
}
@@ -217,32 +232,32 @@ public class RAGFlowClient {
* @param body 请求体
* @param onData 数据回调(每收到一行数据调用一次)
*/
public void postStream(String endpoint, Object body, java.util.function.Consumer<String> onData) {
public void postStream(String endpoint, Object body, Consumer<String> onData) {
try {
String url = buildUrl(endpoint, null);
log.debug("POST STREAM {}", url);
String jsonBody = objectMapper.writeValueAsString(body);
java.net.http.HttpClient httpClient = java.net.http.HttpClient.newBuilder()
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(DEFAULT_TIMEOUT))
.build();
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create(url))
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
.build();
// 发送请求并处理流式响应
httpClient.send(request, java.net.http.HttpResponse.BodyHandlers.ofInputStream())
httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream())
.body()
.transferTo(new java.io.OutputStream() {
private final java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
.transferTo(new OutputStream() {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@Override
public void write(int b) throws java.io.IOException {
public void write(int b) throws IOException {
if (b == '\n') {
String line = buffer.toString(StandardCharsets.UTF_8);
if (!line.trim().isEmpty()) {
@@ -8,6 +8,7 @@ import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.AbstractResource;
@@ -515,7 +516,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
}
@Override
public void postStream(String endpoint, Object body, java.util.function.Consumer<String> onData) {
public void postStream(String endpoint, Object body, Consumer<String> onData) {
try {
getClient().postStream(endpoint, body, onData);
} catch (Exception e) {
@@ -526,7 +527,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
@Override
public Object postSearchBotAsk(Map<String, Object> config, Object body,
java.util.function.Consumer<String> onData) {
Consumer<String> onData) {
// SearchBot 实际上是 Dataset 检索的一种封装,或者是未公开的 API?
// 假设 RAGFlow 没有显式的 /searchbots 接口供 SDK 调用,而是 Dataset Retrieval 或者 Chat。
// 但根据 BotDTO,它是 /api/v1/searchbots/ask (假设)
@@ -547,7 +548,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
@Override
public void postAgentBotCompletion(Map<String, Object> config, String agentId, Object body,
java.util.function.Consumer<String> onData) {
Consumer<String> onData) {
// AgentBot 对应 /api/v1/agentbots/{id}/completions
try {
getClient().postStream("/api/v1/agentbots/" + agentId + "/completions", body, onData);
@@ -206,3 +206,4 @@
10197=T\u00EAn th\u1EB9\uFFFD kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
10198=Kh\u00F4ng t\u00ECm th\u1EA5y th\u1EB9\uFFFD
10199=T\u1EC7p \u0111ang \u0111\u01B0\u1EE3c ph\u00E2n t\u00EDch, thao t\u00E1c n\u00E0y kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3