知识库上传、分段
This commit is contained in:
@@ -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 '更新时间',
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@JsonProperty("has_more")
|
||||
private List<Document> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
size="small"
|
||||
@click="editSegment(segment)"
|
||||
:icon="Edit"
|
||||
<div class="segment-status">
|
||||
<el-tag
|
||||
:type="segment.enabled ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="deleteSegment(segment)"
|
||||
:icon="Delete"
|
||||
{{ segment.enabled ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="getStatusType(segment.status)"
|
||||
size="small"
|
||||
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 getStatusType(status: string): 'success' | 'info' | 'warning' | 'danger' {
|
||||
const typeMap: Record<string, 'success' | 'info' | 'warning' | 'danger'> = {
|
||||
'completed': 'success',
|
||||
'indexing': 'warning',
|
||||
'error': 'danger',
|
||||
'paused': 'info'
|
||||
};
|
||||
return typeMap[status] || 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消编辑
|
||||
* 获取状态文本
|
||||
*/
|
||||
function cancelEdit() {
|
||||
editingSegmentId.value = null;
|
||||
editingContent.value = '';
|
||||
function getStatusText(status: string): string {
|
||||
const textMap: Record<string, string> = {
|
||||
'completed': '已完成',
|
||||
'indexing': '索引中',
|
||||
'error': '错误',
|
||||
'paused': '已暂停'
|
||||
};
|
||||
return textMap[status] || status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分段
|
||||
* 格式化时间戳
|
||||
*/
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
ElMessage.success('保存成功');
|
||||
cancelEdit();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('保存分段失败:', error);
|
||||
ElMessage.error(error.message || '保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
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 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 showAddSegment() {
|
||||
// 如果有分段,默认选择第一个作为父级
|
||||
if (segments.value.length > 0) {
|
||||
selectedParentSegment.value = segments.value[0].parentSegmentId;
|
||||
}
|
||||
addDialogVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新分段
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
<el-button @click="handleCancel">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 字段验证通过');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }">
|
||||
<el-tag
|
||||
:type="row.status === 2 ? 'success' : row.status === 1 ? 'warning' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<el-tag
|
||||
:type="row.enabled ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ 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">
|
||||
|
||||
Reference in New Issue
Block a user