diff --git a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql index c031ad9..d1563e6 100644 --- a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql +++ b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql @@ -144,6 +144,8 @@ CREATE TABLE `tb_ai_upload_file` ( `dify_upload_file_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify上传文件ID(对话中上传的文件)', `chunk_count` INT(11) DEFAULT 0 COMMENT '分段数量', `status` INT(4) DEFAULT 0 COMMENT '状态(0上传中 1处理中 2已完成 3失败)', + `enabled` TINYINT(1) DEFAULT 1 COMMENT '是否启用(0否 1是)', + `display_status` VARCHAR(50) DEFAULT NULL COMMENT '显示状态(indexing/completed/failed)', `error_message` VARCHAR(500) DEFAULT NULL COMMENT '错误信息', `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java index 439ccf2..6dfb20a 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java @@ -19,6 +19,8 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; /** @@ -253,6 +255,7 @@ public class DifyApiClient { /** * 上传文档到知识库(通过文件) + * 根据 Dify API 文档: POST /datasets/{dataset_id}/document/create-by-file */ public DocumentUploadResponse uploadDocumentByFile( String datasetId, @@ -260,24 +263,41 @@ public class DifyApiClient { String originalFilename, DocumentUploadRequest uploadRequest) { - String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/document/create_by_file"); + String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/document/create-by-file"); try { + // 构建 data JSON 字符串(包含所有元数据) + Map dataMap = new HashMap<>(); + if (uploadRequest.getName() != null) { + dataMap.put("name", uploadRequest.getName()); + } + if (uploadRequest.getIndexingTechnique() != null) { + dataMap.put("indexing_technique", uploadRequest.getIndexingTechnique()); + } + + // process_rule 是必填字段,如果没有提供则使用默认配置 + if (uploadRequest.getProcessRule() != null) { + dataMap.put("process_rule", uploadRequest.getProcessRule()); + } else { + // 默认分段规则 + Map defaultProcessRule = new HashMap<>(); + defaultProcessRule.put("mode", "automatic"); + dataMap.put("process_rule", defaultProcessRule); + } + + // 默认设置文档形式和语言 + dataMap.put("doc_form", "text_model"); + dataMap.put("doc_language", "Chinese"); + + String dataJson = JSON.toJSONString(dataMap); + logger.info("上传文档到知识库: datasetId={}, file={}, data={}", datasetId, originalFilename, dataJson); + + // 构建 multipart/form-data 请求体 MultipartBody.Builder bodyBuilder = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", originalFilename, - RequestBody.create(file, MediaType.parse("application/octet-stream"))); - - // 添加其他参数 - if (uploadRequest.getName() != null) { - bodyBuilder.addFormDataPart("name", uploadRequest.getName()); - } - if (uploadRequest.getIndexingTechnique() != null) { - bodyBuilder.addFormDataPart("indexing_technique", uploadRequest.getIndexingTechnique()); - } - if (uploadRequest.getProcessRule() != null) { - bodyBuilder.addFormDataPart("process_rule", JSON.toJSONString(uploadRequest.getProcessRule())); - } + RequestBody.create(file, MediaType.parse("application/octet-stream"))) + .addFormDataPart("data", dataJson); Request httpRequest = new Request.Builder() .url(url) @@ -293,7 +313,8 @@ public class DifyApiClient { throw new DifyException("上传文档失败: " + responseBody); } - return JSON.parseObject(responseBody, DocumentUploadResponse.class); + logger.info("文档上传成功: datasetId={}, file={}", datasetId, originalFilename); + return JSON.parseObject(responseBody, DocumentUploadResponse.class); } } catch (IOException e) { logger.error("上传文档异常", e); @@ -710,16 +731,19 @@ public class DifyApiClient { /** * 通用 GET 请求 * @param path API路径 - * @param apiKey API密钥 + * @param apiKey API密钥(为null时使用知识库API Key) * @return JSON响应字符串 */ public String get(String path, String apiKey) { String url = difyConfig.getFullApiUrl(path); try { + // 如果apiKey为null,使用知识库API Key(因为通用方法主要用于知识库相关操作) + String actualApiKey = apiKey != null ? apiKey : getKnowledgeApiKey(); + Request httpRequest = new Request.Builder() .url(url) - .header("Authorization", "Bearer " + getApiKey(apiKey)) + .header("Authorization", "Bearer " + actualApiKey) .get() .build(); @@ -743,19 +767,22 @@ public class DifyApiClient { * 通用 POST 请求 * @param path API路径 * @param requestBody 请求体(JSON字符串或Map) - * @param apiKey API密钥 + * @param apiKey API密钥(为null时使用知识库API Key) * @return JSON响应字符串 */ public String post(String path, Object requestBody, String apiKey) { String url = difyConfig.getFullApiUrl(path); try { + // 如果apiKey为null,使用知识库API Key + String actualApiKey = apiKey != null ? apiKey : getKnowledgeApiKey(); + String jsonBody = requestBody instanceof String ? (String) requestBody : JSON.toJSONString(requestBody); Request httpRequest = new Request.Builder() .url(url) - .header("Authorization", "Bearer " + getApiKey(apiKey)) + .header("Authorization", "Bearer " + actualApiKey) .header("Content-Type", "application/json") .post(RequestBody.create(jsonBody, MediaType.parse("application/json"))) .build(); @@ -780,19 +807,22 @@ public class DifyApiClient { * 通用 PATCH 请求 * @param path API路径 * @param requestBody 请求体(JSON字符串或Map) - * @param apiKey API密钥 + * @param apiKey API密钥(为null时使用知识库API Key) * @return JSON响应字符串 */ public String patch(String path, Object requestBody, String apiKey) { String url = difyConfig.getFullApiUrl(path); try { + // 如果apiKey为null,使用知识库API Key + String actualApiKey = apiKey != null ? apiKey : getKnowledgeApiKey(); + String jsonBody = requestBody instanceof String ? (String) requestBody : JSON.toJSONString(requestBody); Request httpRequest = new Request.Builder() .url(url) - .header("Authorization", "Bearer " + getApiKey(apiKey)) + .header("Authorization", "Bearer " + actualApiKey) .header("Content-Type", "application/json") .patch(RequestBody.create(jsonBody, MediaType.parse("application/json"))) .build(); @@ -816,16 +846,19 @@ public class DifyApiClient { /** * 通用 DELETE 请求 * @param path API路径 - * @param apiKey API密钥 + * @param apiKey API密钥(为null时使用知识库API Key) * @return JSON响应字符串 */ public String delete(String path, String apiKey) { String url = difyConfig.getFullApiUrl(path); try { + // 如果apiKey为null,使用知识库API Key + String actualApiKey = apiKey != null ? apiKey : getKnowledgeApiKey(); + Request httpRequest = new Request.Builder() .url(url) - .header("Authorization", "Bearer " + getApiKey(apiKey)) + .header("Authorization", "Bearer " + actualApiKey) .delete() .build(); diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java index 38b535b..cb4c9c5 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java @@ -1,22 +1,23 @@ package org.xyzh.ai.client.dto; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.alibaba.fastjson2.annotation.JSONField; import lombok.Data; + import java.util.List; /** - * @description 文档列表响应 + * @description Dify文档列表响应 * @filename DocumentListResponse.java * @author AI Assistant * @copyright xyzh - * @since 2025-11-04 + * @since 2025-11-07 */ @Data public class DocumentListResponse { - - private List data; - @JsonProperty("has_more") + private List data; + + @JSONField(name = "has_more") private Boolean hasMore; private Integer limit; @@ -24,62 +25,72 @@ public class DocumentListResponse { private Integer total; private Integer page; - + + /** + * 文档信息 + */ @Data - public static class DocumentInfo { + public static class Document { private String id; private Integer position; - @JsonProperty("data_source_type") + @JSONField(name = "data_source_type") private String dataSourceType; - @JsonProperty("data_source_info") + @JSONField(name = "data_source_info") private DataSourceInfo dataSourceInfo; - @JsonProperty("dataset_process_rule_id") + @JSONField(name = "dataset_process_rule_id") private String datasetProcessRuleId; private String name; - @JsonProperty("created_from") + @JSONField(name = "created_from") private String createdFrom; - @JsonProperty("created_by") + @JSONField(name = "created_by") private String createdBy; - @JsonProperty("created_at") + @JSONField(name = "created_at") private Long createdAt; - @JsonProperty("indexing_status") + private Integer tokens; + + @JSONField(name = "indexing_status") private String indexingStatus; private String error; private Boolean enabled; - @JsonProperty("disabled_at") + @JSONField(name = "disabled_at") private Long disabledAt; - @JsonProperty("disabled_by") + @JSONField(name = "disabled_by") private String disabledBy; private Boolean archived; - @JsonProperty("word_count") + @JSONField(name = "display_status") + private String displayStatus; + + @JSONField(name = "word_count") private Integer wordCount; - @JsonProperty("hit_count") + @JSONField(name = "hit_count") private Integer hitCount; - @JsonProperty("doc_form") + @JSONField(name = "doc_form") private String docForm; } - + + /** + * 数据源信息 + */ @Data public static class DataSourceInfo { - @JsonProperty("upload_file_id") + @JSONField(name = "upload_file_id") private String uploadFileId; } } - diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java index 8dba361..b46e4bc 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java @@ -1,10 +1,10 @@ package org.xyzh.ai.client.dto; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.alibaba.fastjson2.annotation.JSONField; import lombok.Data; /** - * @description 文档上传响应 + * @description 文档上传响应(根据 Dify API 返回结构) * @filename DocumentUploadResponse.java * @author AI Assistant * @copyright xyzh @@ -14,14 +14,9 @@ import lombok.Data; public class DocumentUploadResponse { /** - * 文档ID + * 文档详细信息 */ - private String id; - - /** - * 文档名称 - */ - private String name; + private Document document; /** * 批次ID(用于查询处理状态) @@ -29,32 +24,122 @@ public class DocumentUploadResponse { private String batch; /** - * 位置(序号) + * 文档详细信息 */ - private Integer position; + @Data + public static class Document { + /** + * 文档ID + */ + private String id; - /** - * 数据源类型 - */ - @JsonProperty("data_source_type") - private String dataSourceType; + /** + * 文档名称 + */ + private String name; - /** - * 索引状态 - */ - @JsonProperty("indexing_status") - private String indexingStatus; + /** + * 位置(序号) + */ + private Integer position; - /** - * 创建时间 - */ - @JsonProperty("created_at") - private Long createdAt; + /** + * 数据源类型 + */ + @JSONField(name = "data_source_type") + private String dataSourceType; - /** - * 创建人 - */ - @JsonProperty("created_by") - private String createdBy; + /** + * 数据源信息 + */ + @JSONField(name = "data_source_info") + private Object dataSourceInfo; + + /** + * 数据集处理规则ID + */ + @JSONField(name = "dataset_process_rule_id") + private String datasetProcessRuleId; + + /** + * 创建来源 + */ + @JSONField(name = "created_from") + private String createdFrom; + + /** + * 创建人 + */ + @JSONField(name = "created_by") + private String createdBy; + + /** + * 创建时间(时间戳) + */ + @JSONField(name = "created_at") + private Long createdAt; + + /** + * Token数量 + */ + private Integer tokens; + + /** + * 索引状态 + */ + @JSONField(name = "indexing_status") + private String indexingStatus; + + /** + * 错误信息 + */ + private String error; + + /** + * 是否启用 + */ + private Boolean enabled; + + /** + * 禁用时间 + */ + @JSONField(name = "disabled_at") + private Long disabledAt; + + /** + * 禁用人 + */ + @JSONField(name = "disabled_by") + private String disabledBy; + + /** + * 是否归档 + */ + private Boolean archived; + + /** + * 显示状态 + */ + @JSONField(name = "display_status") + private String displayStatus; + + /** + * 字数 + */ + @JSONField(name = "word_count") + private Integer wordCount; + + /** + * 命中次数 + */ + @JSONField(name = "hit_count") + private Integer hitCount; + + /** + * 文档形式 + */ + @JSONField(name = "doc_form") + private String docForm; + } } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java index e2f9e27..21f67c9 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java @@ -197,4 +197,22 @@ public class AiKnowledgeController { log.info("获取可用的Rerank模型列表"); return knowledgeService.getAvailableRerankModels(); } + + /** + * @description 获取知识库文档列表 + * @param knowledgeId 知识库ID + * @param page 页码(从1开始,默认1) + * @param limit 每页数量(默认20) + * @return ResultDomain + * @author AI Assistant + * @since 2025-11-07 + */ + @GetMapping("/{knowledgeId}/documents") + public ResultDomain> getDocumentList( + @PathVariable(name = "knowledgeId") String knowledgeId, + @RequestParam(required = false, defaultValue = "1", name = "page") Integer page, + @RequestParam(required = false, defaultValue = "20", name = "limit") Integer limit) { + log.info("获取文档列表: knowledgeId={}, page={}, limit={}", knowledgeId, page, limit); + return knowledgeService.getDocumentList(knowledgeId, page, limit); + } } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/DifyProxyController.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/DifyProxyController.java index a8ebeee..7b0b1c3 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/DifyProxyController.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/DifyProxyController.java @@ -4,8 +4,15 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.xyzh.ai.client.DifyApiClient; +import org.xyzh.ai.mapper.AiUploadFileMapper; import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.dto.ai.TbAiUploadFile; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; + +import java.util.Date; +import java.util.List; import java.util.Map; /** @@ -23,6 +30,11 @@ public class DifyProxyController { @Autowired private DifyApiClient difyApiClient; + @Autowired + private AiUploadFileMapper uploadFileMapper; + + + // ===================== 文档分段管理 API ===================== /** @@ -34,18 +46,19 @@ public class DifyProxyController { * @since 2025-11-04 */ @GetMapping("/datasets/{datasetId}/documents/{documentId}/segments") - public ResultDomain getDocumentSegments( + public ResultDomain getDocumentSegments( @PathVariable(name = "datasetId") String datasetId, @PathVariable(name = "documentId") String documentId) { - ResultDomain result = new ResultDomain<>(); + ResultDomain result = new ResultDomain<>(); log.info("获取文档分段列表: datasetId={}, documentId={}", datasetId, documentId); try { // 调用Dify API(使用默认配置的API Key) String path = "/datasets/" + datasetId + "/documents/" + documentId + "/segments"; String response = difyApiClient.get(path, null); + JSONObject jsonObject = JSONObject.parseObject(response); - result.success("获取文档分段列表成功", response); + result.success("获取文档分段列表成功", JSONArray.parseArray(jsonObject.getJSONArray("data").toJSONString(), JSONObject.class)); return result; } catch (Exception e) { log.error("获取文档分段列表失败", e); @@ -197,5 +210,66 @@ public class DifyProxyController { return result; } } + + /** + * @description 更新文档启用/禁用状态 + * @param datasetId Dify数据集ID + * @param action 操作类型(enable/disable/archive/un_archive) + * @param requestBody 请求体(包含document_ids数组) + * @return ResultDomain 更新结果 + * @author AI Assistant + * @since 2025-11-07 + */ + @PostMapping("/datasets/{datasetId}/documents/status/{action}") + public ResultDomain updateDocumentStatus( + @PathVariable(name = "datasetId") String datasetId, + @PathVariable(name = "action") String action, + @RequestBody Map requestBody) { + + log.info("更新文档状态: datasetId={}, action={}, documentIds={}", + datasetId, action, requestBody.get("document_ids")); + + ResultDomain result = new ResultDomain<>(); + try { + // 1. 调用Dify API(使用默认配置的API Key) + String path = "/datasets/" + datasetId + "/documents/status/" + action; + String response = difyApiClient.patch(path, requestBody, null); + + // 2. 同步更新本地数据库 + @SuppressWarnings("unchecked") + List documentIds = (List) requestBody.get("document_ids"); + if (documentIds != null && !documentIds.isEmpty()) { + Boolean enabled = null; + if ("enable".equals(action)) { + enabled = true; + } else if ("disable".equals(action)) { + enabled = false; + } + + if (enabled != null) { + for (String documentId : documentIds) { + try { + TbAiUploadFile file = uploadFileMapper.selectFileByDifyDocumentId(documentId); + if (file != null) { + file.setEnabled(enabled); + file.setUpdateTime(new Date()); + uploadFileMapper.updateUploadFile(file); + log.info("本地数据库更新成功: documentId={}, enabled={}", documentId, enabled); + } + } catch (Exception e) { + log.warn("更新本地数据库失败: documentId={}, error={}", documentId, e.getMessage()); + } + } + } + } + + result.success("更新文档状态成功", response); + return result; + } catch (Exception e) { + log.error("更新文档状态失败", e); + result.fail("更新文档状态失败: " + e.getMessage()); + return result; + } + } } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/mapper/AiUploadFileMapper.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/mapper/AiUploadFileMapper.java index 12c5499..c76731b 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/mapper/AiUploadFileMapper.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/mapper/AiUploadFileMapper.java @@ -82,4 +82,11 @@ public interface AiUploadFileMapper extends BaseMapper { * @return 插入行数 */ int batchInsertUploadFiles(@Param("files") List files); + + /** + * 根据Dify文档ID查询文件 + * @param difyDocumentId Dify文档ID + * @return TbAiUploadFile 文件记录 + */ + TbAiUploadFile selectFileByDifyDocumentId(@Param("difyDocumentId") String difyDocumentId); } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java index df6f388..42544cd 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java @@ -10,6 +10,7 @@ import org.xyzh.ai.client.dto.DatasetCreateRequest; import org.xyzh.ai.client.dto.DatasetCreateResponse; import org.xyzh.ai.client.dto.DatasetDetailResponse; import org.xyzh.ai.client.dto.DatasetUpdateRequest; +import org.xyzh.ai.client.dto.DocumentListResponse; import org.xyzh.ai.client.dto.EmbeddingModelResponse; import org.xyzh.ai.client.dto.RerankModelResponse; import org.xyzh.ai.client.dto.RetrievalModel; @@ -109,7 +110,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { embeddingModel = difyConfig.getDataset().getDefaultEmbeddingModel(); } difyRequest.setEmbeddingModel(embeddingModel); - + // 设置模型提供商(从前端传入或使用配置默认值) String provider = knowledge.getEmbeddingModelProvider(); @@ -322,7 +323,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { } needUpdateDify = true; } - + // 检索配置变化(Rerank、Top K、Score阈值) boolean retrievalConfigChanged = false; @@ -875,5 +876,73 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { } } + /** + * 获取知识库文档列表 + * @param knowledgeId 知识库ID + * @param page 页码 + * @param limit 每页数量 + * @return 文档列表 + */ + @Override + public ResultDomain> getDocumentList(String knowledgeId, Integer page, Integer limit) { + ResultDomain> resultDomain = new ResultDomain<>(); + + try { + // 查询知识库信息 + TbAiKnowledge knowledge = knowledgeMapper.selectKnowledgeById(knowledgeId); + if (knowledge == null || knowledge.getDeleted()) { + resultDomain.fail("知识库不存在"); + return resultDomain; + } + + // 检查权限 + ResultDomain permissionCheck = checkKnowledgePermission(knowledgeId, "READ"); + if (!permissionCheck.isSuccess() || !permissionCheck.getData()) { + resultDomain.fail("无权限访问该知识库"); + return resultDomain; + } + + // 检查是否有 Dify 数据集ID + if (!StringUtils.hasText(knowledge.getDifyDatasetId())) { + resultDomain.fail("知识库未关联Dify数据集"); + return resultDomain; + } + + // 设置默认值 + int pageNum = page != null && page > 0 ? page : 1; + int pageSize = limit != null && limit > 0 ? limit : 20; + + // 调用 Dify API 获取文档列表 + DocumentListResponse response = difyApiClient.listDocuments( + knowledge.getDifyDatasetId(), + pageNum, + pageSize + ); + + // 构造返回结果 + Map result = new HashMap<>(); + result.put("data", response.getData()); + result.put("total", response.getTotal()); + result.put("page", response.getPage()); + result.put("limit", response.getLimit()); + result.put("hasMore", response.getHasMore()); + + log.info("获取文档列表成功: knowledgeId={}, page={}, total={}", + knowledgeId, pageNum, response.getTotal()); + + resultDomain.success("获取成功", result); + return resultDomain; + + } catch (DifyException e) { + log.error("获取文档列表失败", e); + resultDomain.fail("获取文档列表失败: " + e.getMessage()); + return resultDomain; + } catch (Exception e) { + log.error("获取文档列表异常", e); + resultDomain.fail("获取文档列表异常: " + e.getMessage()); + return resultDomain; + } + } + } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java index 447334a..5c87ceb 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java @@ -13,7 +13,6 @@ import org.xyzh.ai.client.dto.DocumentUploadRequest; import org.xyzh.ai.client.dto.DocumentUploadResponse; import org.xyzh.ai.config.DifyConfig; import org.xyzh.ai.exception.DifyException; -import org.xyzh.ai.exception.FileProcessException; import org.xyzh.ai.mapper.AiAgentConfigMapper; import org.xyzh.ai.mapper.AiKnowledgeMapper; import org.xyzh.ai.mapper.AiUploadFileMapper; @@ -30,10 +29,6 @@ import org.xyzh.common.dto.user.TbSysUser; import org.xyzh.system.utils.LoginUtil; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -272,10 +267,12 @@ public class AiUploadFileServiceImpl implements AiUploadFileService { uploadFile.setFilePath(sysFile.getFilePath()); // 保存系统文件的相对路径 uploadFile.setFileSize(file.getSize()); uploadFile.setFileType(getFileExtension(originalFilename)); - uploadFile.setDifyDocumentId(difyResponse.getId()); + uploadFile.setDifyDocumentId(difyResponse.getDocument().getId()); uploadFile.setDifyBatchId(difyResponse.getBatch()); - uploadFile.setStatus(1); // 1=处理中 - uploadFile.setChunkCount(0); + uploadFile.setStatus(difyResponse.getDocument().getIndexingStatus() == "completed" ? 2 : 1); // 1=处理中 + uploadFile.setChunkCount(difyResponse.getDocument().getWordCount() != null ? difyResponse.getDocument().getWordCount() : 0); + uploadFile.setEnabled(difyResponse.getDocument().getEnabled() != null ? difyResponse.getDocument().getEnabled() : true); + uploadFile.setDisplayStatus(difyResponse.getDocument().getDisplayStatus()); uploadFile.setCreateTime(new Date()); uploadFile.setUpdateTime(new Date()); uploadFile.setDeleted(false); @@ -384,10 +381,7 @@ public class AiUploadFileServiceImpl implements AiUploadFileService { } } - // 4. 获取当前用户 - TbSysUser currentUser = LoginUtil.getCurrentUser(); - - // 5. 逻辑删除本地记录 + // 4. 逻辑删除本地记录 TbAiUploadFile deleteEntity = new TbAiUploadFile(); deleteEntity.setID(fileId); @@ -530,8 +524,8 @@ public class AiUploadFileServiceImpl implements AiUploadFileService { update.setStatus(1); // 处理中 } - if (docStatus.getCompletedSegments() != null) { - update.setChunkCount(docStatus.getCompletedSegments()); + if (docStatus.getTotalSegments() != null) { + update.setChunkCount(docStatus.getTotalSegments()); } } diff --git a/schoolNewsServ/ai/src/main/resources/mapper/AiUploadFileMapper.xml b/schoolNewsServ/ai/src/main/resources/mapper/AiUploadFileMapper.xml index 6ef9def..b7bc23f 100644 --- a/schoolNewsServ/ai/src/main/resources/mapper/AiUploadFileMapper.xml +++ b/schoolNewsServ/ai/src/main/resources/mapper/AiUploadFileMapper.xml @@ -22,6 +22,8 @@ + + @@ -32,7 +34,7 @@ id, user_id, knowledge_id, conversation_id, message_id, sys_file_id, file_name, file_path, file_size, file_type, mime_type, extracted_text, dify_document_id, dify_batch_id, dify_upload_file_id, - chunk_count, status, error_message, + chunk_count, status, error_message, enabled, display_status, create_time, update_time, delete_time, deleted @@ -68,12 +70,12 @@ INSERT INTO tb_ai_upload_file ( id, user_id, knowledge_id, conversation_id, message_id, sys_file_id, file_name, file_path, file_size, file_type, mime_type, extracted_text, dify_document_id, dify_batch_id, dify_upload_file_id, - chunk_count, status, error_message, + chunk_count, status, error_message, enabled, display_status, create_time, update_time, deleted ) VALUES ( #{ID}, #{userID}, #{knowledgeId}, #{conversationID}, #{messageID}, #{sysFileId}, #{fileName}, #{filePath}, #{fileSize}, #{fileType}, #{mimeType}, #{extractedText}, #{difyDocumentId}, #{difyBatchId}, #{difyUploadFileId}, - #{chunkCount}, #{status}, #{errorMessage}, + #{chunkCount}, #{status}, #{errorMessage}, #{enabled}, #{displayStatus}, #{createTime}, #{updateTime}, #{deleted} ) @@ -99,6 +101,8 @@ chunk_count = #{chunkCount}, status = #{status}, error_message = #{errorMessage}, + enabled = #{enabled}, + display_status = #{displayStatus}, update_time = #{updateTime}, WHERE id = #{ID} AND deleted = 0 @@ -193,19 +197,29 @@ ORDER BY create_time ASC + + + INSERT INTO tb_ai_upload_file ( id, user_id, knowledge_id, conversation_id, message_id, sys_file_id, file_name, file_path, file_size, file_type, mime_type, dify_document_id, dify_batch_id, dify_upload_file_id, - chunk_count, status, create_time, update_time, deleted + chunk_count, status, enabled, display_status, create_time, update_time, deleted ) VALUES ( #{file.ID}, #{file.userID}, #{file.knowledgeId}, #{file.conversationID}, #{file.messageID}, #{file.sysFileId}, #{file.fileName}, #{file.filePath}, #{file.fileSize}, #{file.fileType}, #{file.mimeType}, #{file.difyDocumentId}, #{file.difyBatchId}, #{file.difyUploadFileId}, - #{file.chunkCount}, #{file.status}, #{file.createTime}, #{file.updateTime}, #{file.deleted} + #{file.chunkCount}, #{file.status}, #{file.enabled}, #{file.displayStatus}, #{file.createTime}, #{file.updateTime}, #{file.deleted} ) diff --git a/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java b/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java index 27cd294..ece9bd5 100644 --- a/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java +++ b/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java @@ -116,4 +116,13 @@ public interface AiKnowledgeService { * @return Rerank模型列表 */ ResultDomain> getAvailableRerankModels(); + + /** + * 获取知识库文档列表 + * @param knowledgeId 知识库ID + * @param page 页码(从1开始) + * @param limit 每页数量 + * @return 文档列表 + */ + ResultDomain> getDocumentList(String knowledgeId, Integer page, Integer limit); } diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiUploadFile.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiUploadFile.java index 47b04fe..a10bee4 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiUploadFile.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiUploadFile.java @@ -97,6 +97,16 @@ public class TbAiUploadFile extends BaseDTO { * @description 错误信息 */ private String errorMessage; + + /** + * @description 是否启用 + */ + private Boolean enabled; + + /** + * @description 显示状态 + */ + private String displayStatus; public String getUserID() { return userID; @@ -233,6 +243,21 @@ public class TbAiUploadFile extends BaseDTO { public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public String getDisplayStatus() { + return displayStatus; + } + + public void setDisplayStatus(String displayStatus) { + this.displayStatus = displayStatus; + } @Override public String toString() { diff --git a/schoolNewsWeb/src/apis/ai/document-segment.ts b/schoolNewsWeb/src/apis/ai/document-segment.ts index 58e465a..9ded4fc 100644 --- a/schoolNewsWeb/src/apis/ai/document-segment.ts +++ b/schoolNewsWeb/src/apis/ai/document-segment.ts @@ -7,7 +7,6 @@ import { api } from '@/apis/index'; import type { ResultDomain } from '@/types'; import type { - DifySegmentListResponse, DifyChildChunkListResponse, DifyChildChunkResponse, SegmentUpdateRequest, @@ -22,13 +21,13 @@ export const documentSegmentApi = { * 获取文档的所有分段(父级) * @param datasetId Dify数据集ID * @param documentId Dify文档ID - * @returns Promise> + * @returns Promise> 后端直接返回分段数组 */ async getDocumentSegments( datasetId: string, documentId: string - ): Promise> { - const response = await api.get( + ): Promise> { + const response = await api.get( `/ai/dify/datasets/${datasetId}/documents/${documentId}/segments` ); return response.data; @@ -131,14 +130,14 @@ export const documentSegmentApi = { // 1. 获取所有父级分段 const segmentsResult = await this.getDocumentSegments(datasetId, documentId); - if (!segmentsResult.success || !segmentsResult.data?.data) { + if (!segmentsResult.success || !segmentsResult.dataList) { throw new Error('获取分段列表失败'); } // 2. 对每个父级分段,获取其子块 const allChunks: any[] = []; - for (const segment of segmentsResult.data.data) { + for (const segment of segmentsResult.dataList) { try { const chunksResult = await this.getChildChunks( datasetId, diff --git a/schoolNewsWeb/src/apis/ai/file-upload.ts b/schoolNewsWeb/src/apis/ai/file-upload.ts index 9f83944..0d45fea 100644 --- a/schoolNewsWeb/src/apis/ai/file-upload.ts +++ b/schoolNewsWeb/src/apis/ai/file-upload.ts @@ -89,8 +89,10 @@ export const fileUploadApi = { * @param knowledgeId 知识库ID * @returns Promise> */ - async listFilesByKnowledge(knowledgeId: string): Promise> { - const response = await api.get(`/ai/file/knowledge/${knowledgeId}`); + async listFilesByKnowledge(knowledgeId: string): Promise> { + const response = await api.get('/ai/file/list', { + knowledgeId + }); return response.data; }, @@ -138,5 +140,25 @@ export const fileUploadApi = { showLoading: false }); return response.data; + }, + + /** + * 更新文档启用/禁用状态 + * @param datasetId Dify数据集ID + * @param documentId Dify文档ID + * @param enabled 是否启用 + * @returns Promise> + */ + async updateDocumentStatus( + datasetId: string, + documentId: string, + enabled: boolean + ): Promise> { + const action = enabled ? 'enable' : 'disable'; + const response = await api.post( + `/ai/dify/datasets/${datasetId}/documents/status/${action}`, + { document_ids: [documentId] } + ); + return response.data; } }; diff --git a/schoolNewsWeb/src/apis/ai/knowledge.ts b/schoolNewsWeb/src/apis/ai/knowledge.ts index 78373a4..3c08cef 100644 --- a/schoolNewsWeb/src/apis/ai/knowledge.ts +++ b/schoolNewsWeb/src/apis/ai/knowledge.ts @@ -151,6 +151,20 @@ export const knowledgeApi = { async getAvailableRerankModels(): Promise> { const response = await api.get('/ai/knowledge/rerank-models'); return response.data; + }, + + /** + * 获取知识库文档列表 + * @param knowledgeId 知识库ID + * @param page 页码(从1开始) + * @param limit 每页数量 + * @returns Promise> + */ + async getDocumentList(knowledgeId: string, page = 1, limit = 20): Promise> { + const response = await api.get(`/ai/knowledge/${knowledgeId}/documents`, { + page, limit + }); + return response.data; } }; diff --git a/schoolNewsWeb/src/types/ai/index.ts b/schoolNewsWeb/src/types/ai/index.ts index 8d2f46c..e046e94 100644 --- a/schoolNewsWeb/src/types/ai/index.ts +++ b/schoolNewsWeb/src/types/ai/index.ts @@ -125,10 +125,18 @@ export interface AiUploadFile extends BaseDTO { uploadStatus?: number; /** 向量化状态(0待处理 1处理中 2已完成 3失败) */ vectorStatus?: number; + /** 状态(1处理中 2已完成 3失败) */ + status?: number; /** 分片数 */ segmentCount?: number; + /** 分段数(tokens) */ + chunkCount?: number; /** 错误信息 */ errorMessage?: string; + /** 是否启用 */ + enabled?: boolean; + /** 显示状态 */ + displayStatus?: string; } /** diff --git a/schoolNewsWeb/src/views/admin/manage/ai/components/DocumentSegmentDialog.vue b/schoolNewsWeb/src/views/admin/manage/ai/components/DocumentSegmentDialog.vue index 3d16296..45e5535 100644 --- a/schoolNewsWeb/src/views/admin/manage/ai/components/DocumentSegmentDialog.vue +++ b/schoolNewsWeb/src/views/admin/manage/ai/components/DocumentSegmentDialog.vue @@ -25,64 +25,43 @@
- 分段 {{ index + 1 }} + 分段 {{ segment.position }} {{ segment.word_count }} 字 · {{ segment.tokens }} tokens -
- + - 编辑 - - + - 删除 - + {{ getStatusText(segment.status) }} +
- +
-
- -
- 取消 - - 保存 - -
-
-
+
{{ segment.content }}
-
+
+ + +
+ + + 创建时间: {{ formatTimestamp(segment.created_at) }} + + + + 完成时间: {{ formatTimestamp(segment.completed_at) }} + + + + 命中次数: {{ segment.hit_count }} + +
@@ -102,64 +97,21 @@ - - - - - - - - - - - - - - - - -