知识库上传、分段

This commit is contained in:
2025-11-07 14:38:51 +08:00
parent b98450df96
commit 8d87b00678
19 changed files with 687 additions and 478 deletions

View File

@@ -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 '更新时间',

View File

@@ -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<String, Object> 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<String, Object> 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,6 +313,7 @@ public class DifyApiClient {
throw new DifyException("上传文档失败: " + responseBody);
}
logger.info("文档上传成功: datasetId={}, file={}", datasetId, originalFilename);
return JSON.parseObject(responseBody, DocumentUploadResponse.class);
}
} catch (IOException 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();

View File

@@ -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<DocumentInfo> data;
private List<Document> data;
@JsonProperty("has_more")
@JSONField(name = "has_more")
private Boolean hasMore;
private Integer limit;
@@ -25,61 +26,71 @@ public class DocumentListResponse {
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;
}
}

View File

@@ -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
@@ -13,6 +13,21 @@ import lombok.Data;
@Data
public class DocumentUploadResponse {
/**
* 文档详细信息
*/
private Document document;
/**
* 批次ID用于查询处理状态
*/
private String batch;
/**
* 文档详细信息
*/
@Data
public static class Document {
/**
* 文档ID
*/
@@ -23,11 +38,6 @@ public class DocumentUploadResponse {
*/
private String name;
/**
* 批次ID用于查询处理状态
*/
private String batch;
/**
* 位置(序号)
*/
@@ -36,25 +46,100 @@ public class DocumentUploadResponse {
/**
* 数据源类型
*/
@JsonProperty("data_source_type")
@JSONField(name = "data_source_type")
private String dataSourceType;
/**
* 索引状态
* 数据源信息
*/
@JsonProperty("indexing_status")
private String indexingStatus;
@JSONField(name = "data_source_info")
private Object dataSourceInfo;
/**
* 创建时间
* 数据集处理规则ID
*/
@JsonProperty("created_at")
private Long createdAt;
@JSONField(name = "dataset_process_rule_id")
private String datasetProcessRuleId;
/**
* 创建来源
*/
@JSONField(name = "created_from")
private String createdFrom;
/**
* 创建人
*/
@JsonProperty("created_by")
@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;
}
}

View File

@@ -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<Map>
* @author AI Assistant
* @since 2025-11-07
*/
@GetMapping("/{knowledgeId}/documents")
public ResultDomain<Map<String, Object>> 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);
}
}

View File

@@ -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<String> getDocumentSegments(
public ResultDomain<JSONObject> getDocumentSegments(
@PathVariable(name = "datasetId") String datasetId,
@PathVariable(name = "documentId") String documentId) {
ResultDomain<String> result = new ResultDomain<>();
ResultDomain<JSONObject> 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<String> 更新结果
* @author AI Assistant
* @since 2025-11-07
*/
@PostMapping("/datasets/{datasetId}/documents/status/{action}")
public ResultDomain<String> updateDocumentStatus(
@PathVariable(name = "datasetId") String datasetId,
@PathVariable(name = "action") String action,
@RequestBody Map<String, Object> requestBody) {
log.info("更新文档状态: datasetId={}, action={}, documentIds={}",
datasetId, action, requestBody.get("document_ids"));
ResultDomain<String> 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<String> documentIds = (List<String>) 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;
}
}
}

View File

@@ -82,4 +82,11 @@ public interface AiUploadFileMapper extends BaseMapper<TbAiUploadFile> {
* @return 插入行数
*/
int batchInsertUploadFiles(@Param("files") List<TbAiUploadFile> files);
/**
* 根据Dify文档ID查询文件
* @param difyDocumentId Dify文档ID
* @return TbAiUploadFile 文件记录
*/
TbAiUploadFile selectFileByDifyDocumentId(@Param("difyDocumentId") String difyDocumentId);
}

View File

@@ -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;
@@ -875,5 +876,73 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
}
}
/**
* 获取知识库文档列表
* @param knowledgeId 知识库ID
* @param page 页码
* @param limit 每页数量
* @return 文档列表
*/
@Override
public ResultDomain<Map<String, Object>> getDocumentList(String knowledgeId, Integer page, Integer limit) {
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
try {
// 查询知识库信息
TbAiKnowledge knowledge = knowledgeMapper.selectKnowledgeById(knowledgeId);
if (knowledge == null || knowledge.getDeleted()) {
resultDomain.fail("知识库不存在");
return resultDomain;
}
// 检查权限
ResultDomain<Boolean> 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<String, Object> 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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -22,6 +22,8 @@
<result column="chunk_count" property="chunkCount" jdbcType="INTEGER"/>
<result column="status" property="status" jdbcType="INTEGER"/>
<result column="error_message" property="errorMessage" jdbcType="VARCHAR"/>
<result column="enabled" property="enabled" jdbcType="BOOLEAN"/>
<result column="display_status" property="displayStatus" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
@@ -32,7 +34,7 @@
<sql id="Base_Column_List">
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
</sql>
@@ -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}
)
</insert>
@@ -99,6 +101,8 @@
<if test="chunkCount != null">chunk_count = #{chunkCount},</if>
<if test="status != null">status = #{status},</if>
<if test="errorMessage != null">error_message = #{errorMessage},</if>
<if test="enabled != null">enabled = #{enabled},</if>
<if test="displayStatus != null">display_status = #{displayStatus},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</set>
WHERE id = #{ID} AND deleted = 0
@@ -193,19 +197,29 @@
ORDER BY create_time ASC
</select>
<!-- selectFileByDifyDocumentId根据Dify文档ID查询文件 -->
<select id="selectFileByDifyDocumentId" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM tb_ai_upload_file
WHERE dify_document_id = #{difyDocumentId}
AND deleted = 0
LIMIT 1
</select>
<!-- batchInsertUploadFiles批量插入文件记录 -->
<insert id="batchInsertUploadFiles" parameterType="java.util.List">
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
<foreach collection="files" item="file" separator=",">
(
#{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}
)
</foreach>
</insert>

View File

@@ -116,4 +116,13 @@ public interface AiKnowledgeService {
* @return Rerank模型列表
*/
ResultDomain<Map<String, Object>> getAvailableRerankModels();
/**
* 获取知识库文档列表
* @param knowledgeId 知识库ID
* @param page 页码从1开始
* @param limit 每页数量
* @return 文档列表
*/
ResultDomain<Map<String, Object>> getDocumentList(String knowledgeId, Integer page, Integer limit);
}

View File

@@ -98,6 +98,16 @@ public class TbAiUploadFile extends BaseDTO {
*/
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() {

View File

@@ -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<ResultDomain<DifySegmentListResponse>>
* @returns Promise<ResultDomain<DifySegment[]>> 后端直接返回分段数组
*/
async getDocumentSegments(
datasetId: string,
documentId: string
): Promise<ResultDomain<DifySegmentListResponse>> {
const response = await api.get<DifySegmentListResponse>(
): Promise<ResultDomain<any>> {
const response = await api.get<any>(
`/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,

View File

@@ -89,8 +89,10 @@ export const fileUploadApi = {
* @param knowledgeId 知识库ID
* @returns Promise<ResultDomain<AiUploadFile[]>>
*/
async listFilesByKnowledge(knowledgeId: string): Promise<ResultDomain<AiUploadFile[]>> {
const response = await api.get<AiUploadFile[]>(`/ai/file/knowledge/${knowledgeId}`);
async listFilesByKnowledge(knowledgeId: string): Promise<ResultDomain<AiUploadFile>> {
const response = await api.get<AiUploadFile>('/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<ResultDomain<void>>
*/
async updateDocumentStatus(
datasetId: string,
documentId: string,
enabled: boolean
): Promise<ResultDomain<void>> {
const action = enabled ? 'enable' : 'disable';
const response = await api.post<void>(
`/ai/dify/datasets/${datasetId}/documents/status/${action}`,
{ document_ids: [documentId] }
);
return response.data;
}
};

View File

@@ -151,6 +151,20 @@ export const knowledgeApi = {
async getAvailableRerankModels(): Promise<ResultDomain<any>> {
const response = await api.get<any>('/ai/knowledge/rerank-models');
return response.data;
},
/**
* 获取知识库文档列表
* @param knowledgeId 知识库ID
* @param page 页码从1开始
* @param limit 每页数量
* @returns Promise<ResultDomain<any>>
*/
async getDocumentList(knowledgeId: string, page = 1, limit = 20): Promise<ResultDomain<any>> {
const response = await api.get<any>(`/ai/knowledge/${knowledgeId}/documents`, {
page, limit
});
return response.data;
}
};

View File

@@ -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;
}
/**

View File

@@ -25,64 +25,43 @@
<!-- 分段列表 -->
<div class="segment-list" v-loading="loading">
<div
v-for="(segment, index) in segments"
v-for="segment in segments"
:key="segment.id"
class="segment-item"
>
<div class="segment-header">
<span class="segment-index">分段 {{ index + 1 }}</span>
<span class="segment-index">分段 {{ segment.position }}</span>
<span class="segment-info">
{{ segment.word_count }} · {{ segment.tokens }} tokens
</span>
<div class="segment-actions">
<el-button
<div class="segment-status">
<el-tag
:type="segment.enabled ? 'success' : 'info'"
size="small"
@click="editSegment(segment)"
:icon="Edit"
>
编辑
</el-button>
<el-button
{{ segment.enabled ? '已启用' : '已禁用' }}
</el-tag>
<el-tag
:type="getStatusType(segment.status)"
size="small"
type="danger"
@click="deleteSegment(segment)"
:icon="Delete"
style="margin-left: 8px;"
>
删除
</el-button>
{{ getStatusText(segment.status) }}
</el-tag>
</div>
</div>
<!-- 分段内容显示/编辑 -->
<!-- 分段内容显示只读 -->
<div class="segment-content">
<div v-if="editingSegmentId === segment.id" class="segment-editor">
<el-input
v-model="editingContent"
type="textarea"
:rows="6"
placeholder="编辑分段内容"
/>
<div class="editor-actions">
<el-button size="small" @click="cancelEdit">取消</el-button>
<el-button
size="small"
type="primary"
@click="saveSegment(segment)"
:loading="saving"
>
保存
</el-button>
</div>
</div>
<div v-else class="segment-text">
<div class="segment-text">
{{ segment.content }}
</div>
</div>
<!-- 关键词标签 -->
<div class="segment-keywords" v-if="segment.parentKeywords?.length">
<div class="segment-keywords" v-if="segment.keywords?.length">
<el-tag
v-for="keyword in segment.parentKeywords"
v-for="keyword in segment.keywords"
:key="keyword"
size="small"
style="margin-right: 8px;"
@@ -90,6 +69,22 @@
{{ keyword }}
</el-tag>
</div>
<!-- 分段元数据 -->
<div class="segment-meta">
<span class="meta-item">
<el-icon><Clock /></el-icon>
创建时间: {{ formatTimestamp(segment.created_at) }}
</span>
<span class="meta-item" v-if="segment.completed_at">
<el-icon><Check /></el-icon>
完成时间: {{ formatTimestamp(segment.completed_at) }}
</span>
<span class="meta-item">
<el-icon><View /></el-icon>
命中次数: {{ segment.hit_count }}
</span>
</div>
</div>
<!-- 空状态 -->
@@ -102,64 +97,21 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">关闭</el-button>
<el-button type="primary" @click="showAddSegment" :icon="Plus">
添加分段
<el-button
type="primary"
@click="loadSegments"
>
刷新
</el-button>
</div>
</template>
<!-- 添加分段对话框 -->
<el-dialog
v-model="addDialogVisible"
title="添加新分段"
width="600px"
append-to-body
>
<el-form label-position="top">
<el-form-item label="选择父分段(可选)">
<el-select
v-model="selectedParentSegment"
placeholder="选择一个分段作为父级"
style="width: 100%"
clearable
>
<el-option
v-for="(seg, idx) in segments"
:key="seg.id"
:label="`分段 ${idx + 1}: ${seg.content.substring(0, 30)}...`"
:value="seg.parentSegmentId"
/>
</el-select>
</el-form-item>
<el-form-item label="分段内容" required>
<el-input
v-model="newSegmentContent"
type="textarea"
:rows="8"
placeholder="请输入分段内容"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="createNewSegment"
:loading="creating"
>
创建
</el-button>
</template>
</el-dialog>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Edit, Delete, Plus } from '@element-plus/icons-vue';
import { ref, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Clock, Check, View } from '@element-plus/icons-vue';
import { documentSegmentApi } from '../../../../../apis/ai';
defineOptions({
@@ -186,19 +138,8 @@ const visible = computed({
// 数据状态
const loading = ref(false);
const saving = ref(false);
const creating = ref(false);
const segments = ref<any[]>([]);
// 编辑状态
const editingSegmentId = ref<string | null>(null);
const editingContent = ref('');
// 添加分段状态
const addDialogVisible = ref(false);
const selectedParentSegment = ref<string>('');
const newSegmentContent = ref('');
// 统计信息
const totalSegments = computed(() => segments.value.length);
const totalWords = computed(() =>
@@ -208,11 +149,9 @@ const totalTokens = computed(() =>
segments.value.reduce((sum, seg) => sum + (seg.tokens || 0), 0)
);
// 监听对话框显示状态,加载数据
watch(visible, async (val) => {
if (val) {
await loadSegments();
}
// 组件挂载时自动加载数据v-if 确保每次打开都会重新挂载)
onMounted(() => {
loadSegments();
});
/**
@@ -222,13 +161,19 @@ async function loadSegments() {
try {
loading.value = true;
// 使用辅助方法获取所有分段和子块
const allChunks = await documentSegmentApi.getAllSegmentsWithChunks(
// 直接获取父级分段列表Dify的分段本身就是主要内容
const result = await documentSegmentApi.getDocumentSegments(
props.datasetId,
props.documentId
);
segments.value = allChunks;
if (result.success && result.dataList) {
// 直接使用父级分段数据后端已经从Dify响应中提取了data数组
segments.value = result.dataList || [];
} else {
segments.value = [];
ElMessage.error(result.message || '加载分段失败');
}
} catch (error: any) {
console.error('加载分段失败:', error);
ElMessage.error(error.message || '加载分段失败');
@@ -238,147 +183,47 @@ async function loadSegments() {
}
/**
* 编辑分段
* 获取状态类型
*/
function editSegment(segment: any) {
editingSegmentId.value = segment.id;
editingContent.value = segment.content;
}
/**
* 取消编辑
*/
function cancelEdit() {
editingSegmentId.value = null;
editingContent.value = '';
}
/**
* 保存分段
*/
async function saveSegment(segment: any) {
if (!editingContent.value.trim()) {
ElMessage.warning('分段内容不能为空');
return;
}
try {
saving.value = true;
const result = await documentSegmentApi.updateChildChunk(
props.datasetId,
props.documentId,
segment.segment_id,
segment.id,
editingContent.value
);
if (result.success && result.data?.data) {
// 更新本地数据
const index = segments.value.findIndex(s => s.id === segment.id);
if (index !== -1) {
segments.value[index] = {
...segments.value[index],
...result.data.data
function getStatusType(status: string): 'success' | 'info' | 'warning' | 'danger' {
const typeMap: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
'completed': 'success',
'indexing': 'warning',
'error': 'danger',
'paused': 'info'
};
}
ElMessage.success('保存成功');
cancelEdit();
}
} catch (error: any) {
console.error('保存分段失败:', error);
ElMessage.error(error.message || '保存失败');
} finally {
saving.value = false;
}
return typeMap[status] || 'info';
}
/**
* 删除分段
* 获取状态文本
*/
async function deleteSegment(segment: any) {
try {
await ElMessageBox.confirm(
'确定要删除这个分段吗?此操作不可恢复。',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
);
const result = await documentSegmentApi.deleteChildChunk(
props.datasetId,
props.documentId,
segment.segment_id,
segment.id
);
if (result.success) {
segments.value = segments.value.filter(s => s.id !== segment.id);
ElMessage.success('删除成功');
}
} catch (error: any) {
if (error !== 'cancel') {
console.error('删除分段失败:', error);
ElMessage.error(error.message || '删除失败');
}
}
function getStatusText(status: string): string {
const textMap: Record<string, string> = {
'completed': '已完成',
'indexing': '索引中',
'error': '错误',
'paused': '已暂停'
};
return textMap[status] || status;
}
/**
* 显示添加分段对话框
* 格式化时间戳
*/
function showAddSegment() {
// 如果有分段,默认选择第一个作为父级
if (segments.value.length > 0) {
selectedParentSegment.value = segments.value[0].parentSegmentId;
}
addDialogVisible.value = true;
function formatTimestamp(timestamp: number): string {
if (!timestamp) return '-';
const date = new Date(timestamp * 1000);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
/**
* 创建新分段
*/
async function createNewSegment() {
if (!newSegmentContent.value.trim()) {
ElMessage.warning('请输入分段内容');
return;
}
if (!selectedParentSegment.value) {
ElMessage.warning('请选择一个父分段');
return;
}
try {
creating.value = true;
const result = await documentSegmentApi.createChildChunk(
props.datasetId,
props.documentId,
selectedParentSegment.value,
newSegmentContent.value
);
if (result.success && result.data?.data) {
ElMessage.success('创建成功');
addDialogVisible.value = false;
newSegmentContent.value = '';
selectedParentSegment.value = '';
// 重新加载列表
await loadSegments();
}
} catch (error: any) {
console.error('创建分段失败:', error);
ElMessage.error(error.message || '创建失败');
} finally {
creating.value = false;
}
}
</script>
<style lang="scss" scoped>
@@ -510,42 +355,9 @@ async function createNewSegment() {
white-space: nowrap;
}
.segment-actions {
.segment-status {
display: flex;
gap: 8px;
:deep(.el-button) {
border-radius: 8px;
font-weight: 500;
letter-spacing: -0.01em;
padding: 5px 12px;
&.el-button--small {
font-size: 14px;
}
&.el-button--default {
background: #F3F3F5;
border-color: transparent;
color: #0A0A0A;
&:hover {
background: #E5E5E7;
border-color: transparent;
}
}
&.el-button--danger {
background: #FEF2F2;
border-color: transparent;
color: #DC2626;
&:hover {
background: #FEE2E2;
border-color: transparent;
}
}
}
}
}
@@ -562,63 +374,6 @@ async function createNewSegment() {
word-break: break-word;
letter-spacing: -0.01em;
}
.segment-editor {
:deep(.el-textarea) {
.el-textarea__inner {
background: #F3F3F5;
border: 1px solid transparent;
border-radius: 8px;
padding: 12px;
font-size: 14px;
line-height: 1.6;
color: #0A0A0A;
letter-spacing: -0.01em;
&:hover {
border-color: rgba(231, 0, 11, 0.2);
}
&:focus {
border-color: #E7000B;
background: #FFFFFF;
}
}
}
.editor-actions {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: flex-end;
:deep(.el-button) {
border-radius: 8px;
font-weight: 500;
letter-spacing: -0.01em;
&.el-button--default {
background: #F3F3F5;
border-color: transparent;
color: #0A0A0A;
&:hover {
background: #E5E5E7;
}
}
&.el-button--primary {
background: #E7000B;
border-color: #E7000B;
&:hover {
background: #C90009;
border-color: #C90009;
}
}
}
}
}
}
.segment-keywords {
@@ -639,6 +394,29 @@ async function createNewSegment() {
}
}
.segment-meta {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 16px;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
.meta-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #667085;
letter-spacing: -0.01em;
.el-icon {
font-size: 14px;
color: #9CA3AF;
}
}
}
.empty-state {
text-align: center;
padding: 80px 20px;

View File

@@ -362,7 +362,9 @@ watch(() => props.knowledge, (newVal) => {
watch(() => formData.rerankingEnable, () => {
if (formRef.value) {
// 触发 rerankModel 字段的验证
formRef.value.validateField('rerankModel', () => {});
formRef.value.validateField('rerankModel', () => {
console.log('rerankModel 字段验证通过');
});
}
});

View File

@@ -54,24 +54,28 @@
<el-icon color="#409EFF"><Document /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ knowledge.documentCount || 0 }}</div>
<div class="stat-label">文档数</div>
<div class="stat-value">{{ documents.length }}</div>
<div class="stat-label">文档数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon color="#67C23A"><Files /></el-icon>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon color="#E6A23C"><Paperclip /></el-icon>
<el-icon color="#67C23A"><Check /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ documents.length }}</div>
<div class="stat-label">上传文件</div>
<div class="stat-value">{{ enabledDocumentsCount }}</div>
<div class="stat-label">启用的文档</div>
</div>
</div>
<!-- <div class="stat-card">
<div class="stat-icon">
<el-icon color="#E6A23C"><Files /></el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ knowledge.totalChunks || 0 }}</div>
<div class="stat-label">总分段数</div>
</div>
</div> -->
</div>
<!-- 文档列表 -->
@@ -104,7 +108,7 @@
</div>
</template>
</el-table-column>
<el-table-column prop="fileType" label="类型" width="100">
<el-table-column prop="fileType" label="文件类型" width="100">
<template #default="{ row }">
<el-tag size="small">{{ row.fileType || 'unknown' }}</el-tag>
</template>
@@ -114,24 +118,40 @@
{{ formatFileSize(row.fileSize) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<el-table-column label="状态" width="120">
<template #default="{ row }">
<div style="display: flex; gap: 8px;">
<el-tag
:type="row.status === 2 ? 'success' : row.status === 1 ? 'warning' : 'danger'"
:type="row.enabled ? 'success' : 'info'"
size="small"
>
{{ getStatusText(row.status) }}
{{ row.enabled ? '已启用' : '已禁用' }}
</el-tag>
<el-tag
v-if="row.displayStatus === 'indexing'"
type="warning"
size="small"
>
索引中
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="chunkCount" label="分段数" width="100" />
<el-table-column prop="chunkCount" label="数" width="100" />
<el-table-column prop="createTime" label="上传时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-switch
:model-value="row.enabled"
:active-text="row.enabled ? '已启用' : '已禁用'"
:loading="row._switching"
@change="handleToggleEnabled(row, $event)"
style="--el-switch-on-color: #67C23A; margin-right: 12px;"
/>
<el-button link type="primary" @click="handleViewSegments(row)">
查看分段
</el-button>
@@ -199,18 +219,16 @@ import type { UploadUserFile, UploadInstance } from 'element-plus';
import {
ArrowLeft,
Upload,
Refresh,
Edit,
Document,
Clock,
Link,
Files,
Paperclip,
Search,
UploadFilled
UploadFilled,
Check
} from '@element-plus/icons-vue';
import type { AiKnowledge, AiUploadFile } from '@/types/ai';
import { fileUploadApi, knowledgeApi } from '@/apis/ai';
import { fileUploadApi } from '@/apis/ai';
import { DocumentSegmentDialog } from './';
defineOptions({
@@ -235,6 +253,11 @@ const searchQuery = ref('');
const uploadDialogVisible = ref(false);
const segmentDialogVisible = ref(false);
const selectedDocument = ref<AiUploadFile | null>(null);
// 计算启用的文档数量(从 Dify 返回的 enabled 字段)
const enabledDocumentsCount = computed(() => {
return documents.value.filter(doc => doc.enabled === true).length;
});
const uploading = ref(false);
// 上传相关
@@ -257,12 +280,13 @@ watch(() => props.knowledge, (newVal) => {
}
}, { immediate: true });
// 加载文档列表
// 加载文档列表(从本地数据库查询,方便下载)
async function loadDocuments() {
if (!props.knowledge?.id) return;
try {
loading.value = true;
// 使用本地数据库的文件列表接口(包含 sys_file_id 和 file_path方便下载
const result = await fileUploadApi.listFilesByKnowledge(props.knowledge.id);
if (result.success) {
documents.value = (result.dataList || []) as AiUploadFile[];
@@ -351,6 +375,39 @@ function handleDownload(document: AiUploadFile) {
}
}
// 切换文档启用/禁用状态
async function handleToggleEnabled(document: AiUploadFile, enabled: boolean) {
if (!props.knowledge?.difyDatasetId || !document.difyDocumentId) {
ElMessage.error('文档信息不完整');
return;
}
try {
// 添加loading状态
(document as any)._switching = true;
// 调用 Dify API 更新文档状态
const result = await fileUploadApi.updateDocumentStatus(
props.knowledge.difyDatasetId,
document.difyDocumentId,
enabled
);
if (result.success) {
// 更新本地状态
document.enabled = enabled;
ElMessage.success(enabled ? '已启用文档' : '已禁用文档');
} else {
ElMessage.error(result.message || '更新失败');
}
} catch (error: any) {
console.error('更新文档状态失败:', error);
ElMessage.error('更新失败');
} finally {
(document as any)._switching = false;
}
}
// 删除文档
async function handleDeleteDocument(document: AiUploadFile) {
try {
@@ -404,18 +461,6 @@ function formatFileSize(bytes: number | undefined): string {
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
}
function getStatusText(status: number | undefined): string {
switch (status) {
case 1:
return '处理中';
case 2:
return '已完成';
case 3:
return '失败';
default:
return '未知';
}
}
</script>
<style scoped lang="scss">