知识库创建
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
-- ========================================
|
||||
-- 添加 embedding_model_provider 字段
|
||||
-- ========================================
|
||||
-- 用途:在 tb_ai_knowledge 表中添加向量模型提供商字段
|
||||
-- 执行时间:2025-11-06
|
||||
-- 注意:如果该字段已存在,请忽略此脚本
|
||||
-- ========================================
|
||||
|
||||
USE `school_news`;
|
||||
|
||||
-- 检查并添加 embedding_model_provider 字段
|
||||
ALTER TABLE `tb_ai_knowledge`
|
||||
ADD COLUMN `embedding_model_provider` VARCHAR(100) DEFAULT NULL COMMENT '向量模型提供商'
|
||||
AFTER `embedding_model`;
|
||||
|
||||
-- 完成
|
||||
SELECT 'embedding_model_provider 字段添加成功!' AS message;
|
||||
|
||||
@@ -27,6 +27,7 @@ DROP TABLE IF EXISTS `tb_ai_knowledge`;
|
||||
CREATE TABLE `tb_ai_knowledge` (
|
||||
`id` VARCHAR(50) NOT NULL COMMENT '知识库ID',
|
||||
`title` VARCHAR(255) NOT NULL COMMENT '知识库标题',
|
||||
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '知识库头像',
|
||||
`description` VARCHAR(500) DEFAULT NULL COMMENT '知识库描述',
|
||||
`content` LONGTEXT COMMENT '知识内容(手动添加时使用)',
|
||||
`source_type` INT(4) DEFAULT 1 COMMENT '来源类型(1手动添加 2文件导入 3资源同步)',
|
||||
@@ -38,6 +39,11 @@ CREATE TABLE `tb_ai_knowledge` (
|
||||
`dify_dataset_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify知识库ID(Dataset ID)',
|
||||
`dify_indexing_technique` VARCHAR(50) DEFAULT 'high_quality' COMMENT 'Dify索引方式(high_quality/economy)',
|
||||
`embedding_model` VARCHAR(100) DEFAULT NULL COMMENT '向量模型名称',
|
||||
`embedding_model_provider` VARCHAR(100) DEFAULT NULL COMMENT '向量模型提供商',
|
||||
`rerank_model` VARCHAR(100) DEFAULT NULL COMMENT 'Rerank模型名称',
|
||||
`rerank_model_provider` VARCHAR(100) DEFAULT NULL COMMENT 'Rerank模型提供商',
|
||||
`retrieval_top_k` INT(11) DEFAULT 2 COMMENT '检索Top K(返回前K个结果)',
|
||||
`retrieval_score_threshold` DECIMAL(3,2) DEFAULT 0.00 COMMENT '检索分数阈值(0.00-1.00)',
|
||||
`vector_id` VARCHAR(100) DEFAULT NULL COMMENT '向量ID(用于向量检索)',
|
||||
`document_count` INT(11) DEFAULT 0 COMMENT '文档数量',
|
||||
`total_chunks` INT(11) DEFAULT 0 COMMENT '总分段数',
|
||||
@@ -215,10 +221,10 @@ CREATE TABLE `tb_ai_usage_statistics` (
|
||||
|
||||
-- 插入默认智能体配置
|
||||
INSERT INTO `tb_ai_agent_config`
|
||||
(`id`, `name`, `avatar`, `description`, `connect_internet`, `status`, `creator`, `create_time`)
|
||||
(`id`, `name`, `avatar`, `description`, `connect_internet`,`dify_api_key`, `status`, `creator`, `create_time`)
|
||||
VALUES
|
||||
('agent_default_001', '校园助手', NULL, '我是您的智能校园助手,可以帮助您解答校园相关问题',
|
||||
0, 1, '1', NOW());
|
||||
0, 'app-fwOqGFLTsZtekCQYlOmj9f8x', 1, '1', NOW());
|
||||
|
||||
-- 插入示例知识库(需要配合权限表使用)
|
||||
INSERT INTO `tb_ai_knowledge`
|
||||
|
||||
@@ -65,14 +65,14 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 创建知识库(Dataset)
|
||||
*/
|
||||
public DatasetCreateResponse createDataset(DatasetCreateRequest request, String apiKey) {
|
||||
public DatasetCreateResponse createDataset(DatasetCreateRequest request) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets");
|
||||
|
||||
try {
|
||||
String jsonBody = JSON.toJSONString(request);
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.header("Content-Type", "application/json")
|
||||
.post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
|
||||
.build();
|
||||
@@ -96,13 +96,13 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 查询知识库列表
|
||||
*/
|
||||
public DatasetListResponse listDatasets(int page, int limit, String apiKey) {
|
||||
public DatasetListResponse listDatasets(int page, int limit) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets?page=" + page + "&limit=" + limit);
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
@@ -125,13 +125,13 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 查询知识库详情
|
||||
*/
|
||||
public DatasetDetailResponse getDatasetDetail(String datasetId, String apiKey) {
|
||||
public DatasetDetailResponse getDatasetDetail(String datasetId) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
@@ -155,14 +155,14 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 更新知识库
|
||||
*/
|
||||
public void updateDataset(String datasetId, DatasetUpdateRequest request, String apiKey) {
|
||||
public void updateDataset(String datasetId, DatasetUpdateRequest request) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
|
||||
|
||||
try {
|
||||
String jsonBody = JSON.toJSONString(request);
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.header("Content-Type", "application/json")
|
||||
.patch(RequestBody.create(jsonBody, MediaType.parse("application/json")))
|
||||
.build();
|
||||
@@ -183,13 +183,13 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 删除知识库
|
||||
*/
|
||||
public void deleteDataset(String datasetId, String apiKey) {
|
||||
public void deleteDataset(String datasetId) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.delete()
|
||||
.build();
|
||||
|
||||
@@ -258,8 +258,7 @@ public class DifyApiClient {
|
||||
String datasetId,
|
||||
File file,
|
||||
String originalFilename,
|
||||
DocumentUploadRequest uploadRequest,
|
||||
String apiKey) {
|
||||
DocumentUploadRequest uploadRequest) {
|
||||
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/document/create_by_file");
|
||||
|
||||
@@ -282,7 +281,7 @@ public class DifyApiClient {
|
||||
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.post(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
@@ -305,13 +304,13 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 查询文档处理状态
|
||||
*/
|
||||
public DocumentStatusResponse getDocumentStatus(String datasetId, String batchId, String apiKey) {
|
||||
public DocumentStatusResponse getDocumentStatus(String datasetId, String batchId) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents/" + batchId + "/indexing-status");
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
@@ -334,13 +333,13 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 查询知识库文档列表
|
||||
*/
|
||||
public DocumentListResponse listDocuments(String datasetId, int page, int limit, String apiKey) {
|
||||
public DocumentListResponse listDocuments(String datasetId, int page, int limit) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents?page=" + page + "&limit=" + limit);
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
@@ -363,13 +362,13 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 删除文档
|
||||
*/
|
||||
public void deleteDocument(String datasetId, String documentId, String apiKey) {
|
||||
public void deleteDocument(String datasetId, String documentId) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents/" + documentId);
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.delete()
|
||||
.build();
|
||||
|
||||
@@ -392,14 +391,14 @@ public class DifyApiClient {
|
||||
/**
|
||||
* 从知识库检索相关内容
|
||||
*/
|
||||
public RetrievalResponse retrieveFromDataset(String datasetId, RetrievalRequest request, String apiKey) {
|
||||
public RetrievalResponse retrieveFromDataset(String datasetId, RetrievalRequest request) {
|
||||
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/retrieve");
|
||||
|
||||
try {
|
||||
String jsonBody = JSON.toJSONString(request);
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.header("Content-Type", "application/json")
|
||||
.post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
|
||||
.build();
|
||||
@@ -850,6 +849,7 @@ public class DifyApiClient {
|
||||
|
||||
/**
|
||||
* 获取API密钥(优先使用传入的密钥,否则使用配置的默认密钥)
|
||||
* 用于智能体相关的API
|
||||
*/
|
||||
private String getApiKey(String apiKey) {
|
||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||
@@ -861,6 +861,17 @@ public class DifyApiClient {
|
||||
throw new DifyException("未配置Dify API密钥");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库API密钥(统一使用配置中的knowledgeApiKey)
|
||||
* 用于知识库相关的API
|
||||
*/
|
||||
private String getKnowledgeApiKey() {
|
||||
if (difyConfig.getKnowledgeApiKey() != null && !difyConfig.getKnowledgeApiKey().trim().isEmpty()) {
|
||||
return difyConfig.getKnowledgeApiKey();
|
||||
}
|
||||
throw new DifyException("未配置Dify知识库API密钥");
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止请求的内部类
|
||||
*/
|
||||
@@ -919,5 +930,65 @@ public class DifyApiClient {
|
||||
this.feedback = feedback;
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 模型管理 API =====================
|
||||
|
||||
/**
|
||||
* 获取可用的嵌入模型列表
|
||||
*/
|
||||
public EmbeddingModelResponse getAvailableEmbeddingModels() {
|
||||
String url = difyConfig.getFullApiUrl("/workspaces/current/models/model-types/text-embedding");
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
logger.error("获取嵌入模型列表失败: {} - {}", response.code(), responseBody);
|
||||
throw new DifyException("获取嵌入模型列表失败: " + responseBody);
|
||||
}
|
||||
|
||||
return JSON.parseObject(responseBody, EmbeddingModelResponse.class);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("获取嵌入模型列表异常", e);
|
||||
throw new DifyException("获取嵌入模型列表异常: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的Rerank模型列表
|
||||
*/
|
||||
public RerankModelResponse getAvailableRerankModels() {
|
||||
String url = difyConfig.getFullApiUrl("/workspaces/current/models/model-types/rerank");
|
||||
|
||||
try {
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.header("Authorization", "Bearer " + getKnowledgeApiKey())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||
String responseBody = response.body() != null ? response.body().string() : "";
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
logger.error("获取Rerank模型列表失败: {} - {}", response.code(), responseBody);
|
||||
throw new DifyException("获取Rerank模型列表失败: " + responseBody);
|
||||
}
|
||||
|
||||
return JSON.parseObject(responseBody, RerankModelResponse.class);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("获取Rerank模型列表异常", e);
|
||||
throw new DifyException("获取Rerank模型列表异常: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,18 @@ public class DatasetCreateRequest {
|
||||
@JsonProperty("embedding_model")
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* Embedding模型提供商
|
||||
*/
|
||||
@JsonProperty("embedding_model_provider")
|
||||
private String embeddingModelProvider;
|
||||
|
||||
/**
|
||||
* 检索模型配置(包含 Rerank、Top K、Score 阈值等)
|
||||
*/
|
||||
@JsonProperty("retrieval_model")
|
||||
private RetrievalModel retrievalModel;
|
||||
|
||||
/**
|
||||
* 权限:only_me(仅自己)、all_team_members(团队所有成员)
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.xyzh.ai.client.dto;
|
||||
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -21,5 +22,29 @@ public class DatasetUpdateRequest {
|
||||
* 知识库描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 索引方式(high_quality/economy)
|
||||
*/
|
||||
@JSONField(name = "indexing_technique")
|
||||
private String indexingTechnique;
|
||||
|
||||
/**
|
||||
* Embedding模型
|
||||
*/
|
||||
@JSONField(name = "embedding_model")
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* Embedding模型提供商
|
||||
*/
|
||||
@JSONField(name = "embedding_model_provider")
|
||||
private String embeddingModelProvider;
|
||||
|
||||
/**
|
||||
* 检索模型配置(包含 Rerank、Top K、Score 阈值等)
|
||||
*/
|
||||
@JSONField(name = "retrieval_model")
|
||||
private RetrievalModel retrievalModel;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.xyzh.ai.client.dto;
|
||||
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description Dify嵌入模型响应
|
||||
* @filename EmbeddingModelResponse.java
|
||||
* @author AI Assistant
|
||||
* @since 2025-11-06
|
||||
*/
|
||||
@Data
|
||||
public class EmbeddingModelResponse {
|
||||
|
||||
/**
|
||||
* 模型提供商列表
|
||||
*/
|
||||
@JSONField(name = "data")
|
||||
private List<ModelProvider> data;
|
||||
|
||||
/**
|
||||
* 模型提供商
|
||||
*/
|
||||
@Data
|
||||
public static class ModelProvider {
|
||||
/**
|
||||
* 提供商标识
|
||||
*/
|
||||
@JSONField(name = "provider")
|
||||
private String provider;
|
||||
|
||||
/**
|
||||
* 提供商标签
|
||||
*/
|
||||
@JSONField(name = "label")
|
||||
private Map<String, String> label;
|
||||
|
||||
/**
|
||||
* 小图标
|
||||
*/
|
||||
@JSONField(name = "icon_small")
|
||||
private Map<String, String> iconSmall;
|
||||
|
||||
/**
|
||||
* 大图标
|
||||
*/
|
||||
@JSONField(name = "icon_large")
|
||||
private Map<String, String> iconLarge;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@JSONField(name = "status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 模型列表
|
||||
*/
|
||||
@JSONField(name = "models")
|
||||
private List<Model> models;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型详情
|
||||
*/
|
||||
@Data
|
||||
public static class Model {
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@JSONField(name = "model")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 模型标签
|
||||
*/
|
||||
@JSONField(name = "label")
|
||||
private Map<String, String> label;
|
||||
|
||||
/**
|
||||
* 模型类型
|
||||
*/
|
||||
@JSONField(name = "model_type")
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 特性列表
|
||||
*/
|
||||
@JSONField(name = "features")
|
||||
private List<Object> features;
|
||||
|
||||
/**
|
||||
* 获取来源
|
||||
*/
|
||||
@JSONField(name = "fetch_from")
|
||||
private String fetchFrom;
|
||||
|
||||
/**
|
||||
* 模型属性
|
||||
*/
|
||||
@JSONField(name = "model_properties")
|
||||
private ModelProperties modelProperties;
|
||||
|
||||
/**
|
||||
* 是否已弃用
|
||||
*/
|
||||
@JSONField(name = "deprecated")
|
||||
private Boolean deprecated;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@JSONField(name = "status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 是否启用负载均衡
|
||||
*/
|
||||
@JSONField(name = "load_balancing_enabled")
|
||||
private Boolean loadBalancingEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型属性
|
||||
*/
|
||||
@Data
|
||||
public static class ModelProperties {
|
||||
/**
|
||||
* 上下文大小
|
||||
*/
|
||||
@JSONField(name = "context_size")
|
||||
private Integer contextSize;
|
||||
|
||||
/**
|
||||
* 最大分块数
|
||||
*/
|
||||
@JSONField(name = "max_chunks")
|
||||
private Integer maxChunks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.xyzh.ai.client.dto;
|
||||
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description Dify Rerank模型响应
|
||||
* @filename RerankModelResponse.java
|
||||
* @author AI Assistant
|
||||
* @copyright xyzh
|
||||
* @since 2025-11-06
|
||||
*/
|
||||
@Data
|
||||
public class RerankModelResponse {
|
||||
private List<ModelProvider> data;
|
||||
|
||||
@Data
|
||||
public static class ModelProvider {
|
||||
private String provider;
|
||||
private Map<String, String> label; // e.g., {"en_US": "Cohere", "zh_Hans": "Cohere"}
|
||||
@JSONField(name = "icon_small")
|
||||
private String iconSmall;
|
||||
@JSONField(name = "icon_large")
|
||||
private String iconLarge;
|
||||
private String status; // e.g., "active"
|
||||
private List<Model> models;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Model {
|
||||
private String model; // e.g., "rerank-multilingual-v3.0"
|
||||
private Map<String, String> label;
|
||||
@JSONField(name = "model_type")
|
||||
private String modelType; // e.g., "rerank"
|
||||
private List<String> features;
|
||||
@JSONField(name = "fetch_from")
|
||||
private String fetchFrom;
|
||||
@JSONField(name = "model_properties")
|
||||
private ModelProperties modelProperties;
|
||||
private Boolean deprecated;
|
||||
private String status; // e.g., "active"
|
||||
private String provider; // 模型提供商(可能在 model 数据中)
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ModelProperties {
|
||||
@JSONField(name = "context_size")
|
||||
private Integer contextSize;
|
||||
@JSONField(name = "max_chunks")
|
||||
private Integer maxChunks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.xyzh.ai.client.dto;
|
||||
|
||||
import com.alibaba.fastjson2.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @description Dify检索模型配置(Retrieval Model)
|
||||
* @filename RetrievalModel.java
|
||||
* @author AI Assistant
|
||||
* @copyright xyzh
|
||||
* @since 2025-11-06
|
||||
*/
|
||||
@Data
|
||||
public class RetrievalModel {
|
||||
|
||||
/**
|
||||
* 搜索方法:vector_search(向量搜索)、full_text_search(全文搜索)、hybrid_search(混合搜索)
|
||||
*/
|
||||
@JSONField(name = "search_method")
|
||||
private String searchMethod;
|
||||
|
||||
/**
|
||||
* Rerank模型提供商
|
||||
*/
|
||||
@JSONField(name = "reranking_provider_name")
|
||||
private String rerankingProviderName;
|
||||
|
||||
/**
|
||||
* Rerank模型名称
|
||||
*/
|
||||
@JSONField(name = "reranking_model")
|
||||
private String rerankingModel;
|
||||
|
||||
/**
|
||||
* Rerank是否启用
|
||||
*/
|
||||
@JSONField(name = "reranking_enable")
|
||||
private Boolean rerankingEnable;
|
||||
|
||||
/**
|
||||
* Top K(返回前K个结果)
|
||||
*/
|
||||
@JSONField(name = "top_k")
|
||||
private Integer topK;
|
||||
|
||||
/**
|
||||
* 分数阈值(0.00-1.00)
|
||||
*/
|
||||
@JSONField(name = "score_threshold")
|
||||
private Double scoreThreshold;
|
||||
|
||||
/**
|
||||
* 是否启用分数阈值
|
||||
*/
|
||||
@JSONField(name = "score_threshold_enabled")
|
||||
private Boolean scoreThresholdEnabled;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ public class DifyConfig {
|
||||
// private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f";
|
||||
private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x";
|
||||
|
||||
private String knowledgeApiKey="dataset-HeDK9gHBqPnI4rBZ2q2Hm7rV";
|
||||
|
||||
/**
|
||||
* 请求超时时间(秒)
|
||||
*/
|
||||
|
||||
@@ -30,21 +30,19 @@ public class AiKnowledgeController {
|
||||
|
||||
/**
|
||||
* @description 创建知识库
|
||||
* @param requestBody 请求体(knowledge, permissionType, deptIds, roleIds)
|
||||
* @param knowledge 知识库对象
|
||||
* @return ResultDomain<TbAiKnowledge>
|
||||
* @author AI Assistant
|
||||
* @since 2025-11-04
|
||||
*/
|
||||
@PostMapping
|
||||
public ResultDomain<TbAiKnowledge> createKnowledge(@RequestBody Map<String, Object> requestBody) {
|
||||
TbAiKnowledge knowledge = (TbAiKnowledge) requestBody.get("knowledge");
|
||||
String permissionType = (String) requestBody.get("permissionType");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> deptIds = (List<String>) requestBody.get("deptIds");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> roleIds = (List<String>) requestBody.get("roleIds");
|
||||
public ResultDomain<TbAiKnowledge> createKnowledge(@RequestBody TbAiKnowledge knowledge) {
|
||||
// 默认权限为PUBLIC(公开)
|
||||
String permissionType = "PUBLIC";
|
||||
List<String> deptIds = null;
|
||||
List<String> roleIds = null;
|
||||
|
||||
log.info("创建知识库: permissionType={}", permissionType);
|
||||
log.info("创建知识库: name={}", knowledge.getTitle());
|
||||
return knowledgeService.createKnowledge(knowledge, permissionType, deptIds, roleIds);
|
||||
}
|
||||
|
||||
@@ -109,7 +107,7 @@ public class AiKnowledgeController {
|
||||
* @since 2025-11-04
|
||||
*/
|
||||
@PostMapping("/page")
|
||||
public PageDomain<TbAiKnowledge> pageKnowledges(@RequestBody PageRequest<TbAiKnowledge> pageRequest) {
|
||||
public ResultDomain<TbAiKnowledge> pageKnowledges(@RequestBody PageRequest<TbAiKnowledge> pageRequest) {
|
||||
log.info("分页查询知识库");
|
||||
return knowledgeService.pageKnowledges(pageRequest.getFilter(), pageRequest.getPageParam());
|
||||
}
|
||||
@@ -175,4 +173,28 @@ public class AiKnowledgeController {
|
||||
log.info("获取知识库统计信息: id={}", id);
|
||||
return knowledgeService.getKnowledgeStats(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取可用的嵌入模型列表
|
||||
* @return ResultDomain<Map>
|
||||
* @author AI Assistant
|
||||
* @since 2025-11-06
|
||||
*/
|
||||
@GetMapping("/embedding-models")
|
||||
public ResultDomain<Map<String, Object>> getAvailableEmbeddingModels() {
|
||||
log.info("获取可用的嵌入模型列表");
|
||||
return knowledgeService.getAvailableEmbeddingModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取可用的Rerank模型列表
|
||||
* @return ResultDomain<Map>
|
||||
* @author AI Assistant
|
||||
* @since 2025-11-06
|
||||
*/
|
||||
@GetMapping("/rerank-models")
|
||||
public ResultDomain<Map<String, Object>> getAvailableRerankModels() {
|
||||
log.info("获取可用的Rerank模型列表");
|
||||
return knowledgeService.getAvailableRerankModels();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ 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.EmbeddingModelResponse;
|
||||
import org.xyzh.ai.client.dto.RerankModelResponse;
|
||||
import org.xyzh.ai.client.dto.RetrievalModel;
|
||||
import org.xyzh.ai.config.DifyConfig;
|
||||
import org.xyzh.ai.exception.AiKnowledgeException;
|
||||
import org.xyzh.ai.exception.DifyException;
|
||||
@@ -26,8 +29,11 @@ import org.xyzh.common.dto.user.TbSysUser;
|
||||
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||
import org.xyzh.system.utils.LoginUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -102,11 +108,49 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
}
|
||||
difyRequest.setEmbeddingModel(embeddingModel);
|
||||
|
||||
// 调用Dify API创建知识库
|
||||
DatasetCreateResponse difyResponse = difyApiClient.createDataset(
|
||||
difyRequest,
|
||||
difyConfig.getApiKey()
|
||||
);
|
||||
// 设置模型提供商(从前端传入或使用配置默认值)
|
||||
String provider = knowledge.getEmbeddingModelProvider();
|
||||
|
||||
if (StringUtils.hasText(provider)) {
|
||||
difyRequest.setEmbeddingModelProvider(provider);
|
||||
log.info("创建知识库 - 设置Embedding模型: model={}, provider={}", embeddingModel, provider);
|
||||
}
|
||||
|
||||
// 设置检索模型配置(Rerank、Top K、Score 阈值)
|
||||
RetrievalModel retrievalModel = new RetrievalModel();
|
||||
retrievalModel.setSearchMethod("hybrid_search"); // 默认使用混合搜索
|
||||
|
||||
// Top K 配置
|
||||
if (knowledge.getRetrievalTopK() != null && knowledge.getRetrievalTopK() > 0) {
|
||||
retrievalModel.setTopK(knowledge.getRetrievalTopK());
|
||||
} else {
|
||||
retrievalModel.setTopK(2); // 默认值
|
||||
}
|
||||
|
||||
// Score 阈值配置
|
||||
if (knowledge.getRetrievalScoreThreshold() != null && knowledge.getRetrievalScoreThreshold() >= 0) {
|
||||
retrievalModel.setScoreThreshold(knowledge.getRetrievalScoreThreshold());
|
||||
retrievalModel.setScoreThresholdEnabled(knowledge.getRetrievalScoreThreshold() > 0);
|
||||
} else {
|
||||
retrievalModel.setScoreThreshold(0.0);
|
||||
retrievalModel.setScoreThresholdEnabled(false);
|
||||
}
|
||||
|
||||
// Rerank 模型配置
|
||||
if (StringUtils.hasText(knowledge.getRerankModel())) {
|
||||
retrievalModel.setRerankingEnable(true);
|
||||
retrievalModel.setRerankingModel(knowledge.getRerankModel());
|
||||
retrievalModel.setRerankingProviderName(knowledge.getRerankModelProvider());
|
||||
log.info("创建知识库 - 启用Rerank: model={}, provider={}",
|
||||
knowledge.getRerankModel(), knowledge.getRerankModelProvider());
|
||||
} else {
|
||||
retrievalModel.setRerankingEnable(false);
|
||||
}
|
||||
|
||||
difyRequest.setRetrievalModel(retrievalModel);
|
||||
|
||||
// 调用Dify API创建知识库(使用知识库API Key)
|
||||
DatasetCreateResponse difyResponse = difyApiClient.createDataset(difyRequest);
|
||||
|
||||
difyDatasetId = difyResponse.getId();
|
||||
log.info("Dify知识库创建成功: {} - {}", difyDatasetId, knowledge.getTitle());
|
||||
@@ -122,6 +166,12 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
knowledge.setDifyDatasetId(difyDatasetId);
|
||||
knowledge.setDifyIndexingTechnique(indexingTechnique);
|
||||
knowledge.setEmbeddingModel(embeddingModel);
|
||||
// 保存模型提供商
|
||||
if (StringUtils.hasText(knowledge.getEmbeddingModelProvider())) {
|
||||
knowledge.setEmbeddingModelProvider(knowledge.getEmbeddingModelProvider());
|
||||
}
|
||||
// 保存检索配置(从前端传入,已经在 knowledge 对象中)
|
||||
// retrievalTopK, retrievalScoreThreshold, rerankModel, rerankModelProvider 已设置
|
||||
knowledge.setCreator(currentUser.getID());
|
||||
knowledge.setCreatorDept(deptId);
|
||||
knowledge.setUpdater(currentUser.getID());
|
||||
@@ -143,7 +193,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
if (rows <= 0) {
|
||||
// 回滚:删除Dify中的知识库
|
||||
try {
|
||||
difyApiClient.deleteDataset(difyDatasetId, difyConfig.getApiKey());
|
||||
difyApiClient.deleteDataset(difyDatasetId);
|
||||
} catch (Exception ex) {
|
||||
log.error("回滚删除Dify知识库失败", ex);
|
||||
}
|
||||
@@ -209,28 +259,54 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 5. 如果修改了title或description,同步到Dify
|
||||
// 5. 检测字段变化,同步到Dify
|
||||
boolean needUpdateDify = false;
|
||||
DatasetUpdateRequest updateRequest = new DatasetUpdateRequest();
|
||||
|
||||
// 标题变化
|
||||
if (StringUtils.hasText(knowledge.getTitle()) && !knowledge.getTitle().equals(existing.getTitle())) {
|
||||
needUpdateDify = true;
|
||||
}
|
||||
if (knowledge.getDescription() != null && !knowledge.getDescription().equals(existing.getDescription())) {
|
||||
updateRequest.setName(knowledge.getTitle());
|
||||
needUpdateDify = true;
|
||||
}
|
||||
|
||||
// 描述变化
|
||||
if (knowledge.getDescription() != null && !knowledge.getDescription().equals(existing.getDescription())) {
|
||||
updateRequest.setDescription(knowledge.getDescription());
|
||||
needUpdateDify = true;
|
||||
}
|
||||
|
||||
// 索引方式变化(economy可以升级为high_quality,但不能降级)
|
||||
if (StringUtils.hasText(knowledge.getDifyIndexingTechnique()) &&
|
||||
!knowledge.getDifyIndexingTechnique().equals(existing.getDifyIndexingTechnique())) {
|
||||
// 允许从economy升级到high_quality
|
||||
if ("high_quality".equals(knowledge.getDifyIndexingTechnique()) ||
|
||||
"economy".equals(existing.getDifyIndexingTechnique())) {
|
||||
updateRequest.setIndexingTechnique(knowledge.getDifyIndexingTechnique());
|
||||
needUpdateDify = true;
|
||||
} else {
|
||||
log.warn("不允许从high_quality降级为economy: knowledgeId={}", knowledge.getID());
|
||||
}
|
||||
}
|
||||
|
||||
// Embedding模型变化
|
||||
if (StringUtils.hasText(knowledge.getEmbeddingModel()) &&
|
||||
!knowledge.getEmbeddingModel().equals(existing.getEmbeddingModel())) {
|
||||
updateRequest.setEmbeddingModel(knowledge.getEmbeddingModel());
|
||||
|
||||
// 使用前端传入的模型提供商
|
||||
if (StringUtils.hasText(knowledge.getEmbeddingModelProvider())) {
|
||||
updateRequest.setEmbeddingModelProvider(knowledge.getEmbeddingModelProvider());
|
||||
log.info("更新Embedding模型: model={}, provider={}", knowledge.getEmbeddingModel(), knowledge.getEmbeddingModelProvider());
|
||||
}
|
||||
needUpdateDify = true;
|
||||
}
|
||||
|
||||
// 同步到Dify
|
||||
if (needUpdateDify && StringUtils.hasText(existing.getDifyDatasetId())) {
|
||||
try {
|
||||
DatasetUpdateRequest updateRequest = new DatasetUpdateRequest();
|
||||
// 只设置实际改变的字段
|
||||
if (StringUtils.hasText(knowledge.getTitle())) {
|
||||
updateRequest.setName(knowledge.getTitle());
|
||||
}
|
||||
if (knowledge.getDescription() != null) {
|
||||
updateRequest.setDescription(knowledge.getDescription());
|
||||
}
|
||||
|
||||
difyApiClient.updateDataset(existing.getDifyDatasetId(), updateRequest, difyConfig.getApiKey());
|
||||
log.info("Dify知识库更新成功: {} - {}", existing.getDifyDatasetId(), knowledge.getTitle());
|
||||
difyApiClient.updateDataset(existing.getDifyDatasetId(), updateRequest);
|
||||
log.info("Dify知识库更新成功: datasetId={}, title={}",
|
||||
existing.getDifyDatasetId(), knowledge.getTitle());
|
||||
} catch (DifyException e) {
|
||||
log.error("更新Dify知识库失败,继续更新本地数据", e);
|
||||
// 不阻塞本地更新流程
|
||||
@@ -294,7 +370,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
// 4. 删除Dify中的知识库
|
||||
if (StringUtils.hasText(existing.getDifyDatasetId())) {
|
||||
try {
|
||||
difyApiClient.deleteDataset(existing.getDifyDatasetId(), difyConfig.getApiKey());
|
||||
difyApiClient.deleteDataset(existing.getDifyDatasetId());
|
||||
log.info("Dify知识库删除成功: {}", existing.getDifyDatasetId());
|
||||
} catch (DifyException e) {
|
||||
log.error("删除Dify知识库失败,继续删除本地记录", e);
|
||||
@@ -372,7 +448,8 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam) {
|
||||
public ResultDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam) {
|
||||
ResultDomain<TbAiKnowledge> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||
|
||||
@@ -389,11 +466,13 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
resultPageParam.setTotalElements(total);
|
||||
resultPageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
|
||||
|
||||
return new PageDomain<>(resultPageParam, knowledges);
|
||||
resultDomain.success("查询成功", knowledges);
|
||||
return resultDomain;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("分页查询知识库列表异常", e);
|
||||
return new PageDomain<>();
|
||||
resultDomain.fail("查询失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,8 +496,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
// 2. 从Dify获取最新信息
|
||||
try {
|
||||
DatasetDetailResponse difyDetail = difyApiClient.getDatasetDetail(
|
||||
knowledge.getDifyDatasetId(),
|
||||
difyConfig.getApiKey()
|
||||
knowledge.getDifyDatasetId()
|
||||
);
|
||||
|
||||
// 3. 更新本地信息
|
||||
@@ -561,5 +639,143 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
throw new AiKnowledgeException("创建权限失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的嵌入模型列表
|
||||
*/
|
||||
@Override
|
||||
public ResultDomain<Map<String, Object>> getAvailableEmbeddingModels() {
|
||||
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
// 调用Dify API获取嵌入模型列表
|
||||
EmbeddingModelResponse response =
|
||||
difyApiClient.getAvailableEmbeddingModels();
|
||||
|
||||
if (response == null || response.getData() == null) {
|
||||
resultDomain.fail("获取嵌入模型列表失败");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 转换为前端需要的格式
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("providers", response.getData());
|
||||
|
||||
// 提取所有可用的模型列表(扁平化)
|
||||
List<Map<String, Object>> modelList = new ArrayList<>();
|
||||
for (EmbeddingModelResponse.ModelProvider provider : response.getData()) {
|
||||
if (provider.getModels() != null) {
|
||||
for (EmbeddingModelResponse.Model model : provider.getModels()) {
|
||||
// 只返回可用的、非弃用的模型
|
||||
if (model.getStatus() != null &&
|
||||
!"deprecated".equals(model.getStatus()) &&
|
||||
(model.getDeprecated() == null || !model.getDeprecated())) {
|
||||
|
||||
Map<String, Object> modelInfo = new HashMap<>();
|
||||
modelInfo.put("provider", provider.getProvider());
|
||||
modelInfo.put("model", model.getModel());
|
||||
modelInfo.put("label", model.getLabel());
|
||||
modelInfo.put("modelType", model.getModelType());
|
||||
modelInfo.put("status", model.getStatus());
|
||||
|
||||
// 添加模型属性
|
||||
if (model.getModelProperties() != null) {
|
||||
modelInfo.put("contextSize", model.getModelProperties().getContextSize());
|
||||
modelInfo.put("maxChunks", model.getModelProperties().getMaxChunks());
|
||||
}
|
||||
|
||||
modelList.add(modelInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.put("models", modelList);
|
||||
result.put("total", modelList.size());
|
||||
|
||||
log.info("获取嵌入模型列表成功,共{}个提供商,{}个可用模型",
|
||||
response.getData().size(), modelList.size());
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Map<String, Object>> getAvailableRerankModels() {
|
||||
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
// 调用Dify API获取Rerank模型列表
|
||||
RerankModelResponse response =
|
||||
difyApiClient.getAvailableRerankModels();
|
||||
|
||||
if (response == null || response.getData() == null) {
|
||||
resultDomain.fail("获取Rerank模型列表失败");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 转换为前端需要的格式
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("providers", response.getData());
|
||||
|
||||
// 提取所有可用的模型列表(扁平化)
|
||||
List<Map<String, Object>> modelList = new ArrayList<>();
|
||||
for (RerankModelResponse.ModelProvider provider : response.getData()) {
|
||||
if (provider.getModels() != null) {
|
||||
for (RerankModelResponse.Model model : provider.getModels()) {
|
||||
// 只返回可用的、非弃用的模型
|
||||
if (model.getStatus() != null &&
|
||||
!"deprecated".equals(model.getStatus()) &&
|
||||
(model.getDeprecated() == null || !model.getDeprecated())) {
|
||||
|
||||
Map<String, Object> modelInfo = new HashMap<>();
|
||||
modelInfo.put("provider", provider.getProvider());
|
||||
modelInfo.put("model", model.getModel());
|
||||
modelInfo.put("label", model.getLabel());
|
||||
modelInfo.put("modelType", model.getModelType());
|
||||
modelInfo.put("status", model.getStatus());
|
||||
|
||||
// 添加模型属性
|
||||
if (model.getModelProperties() != null) {
|
||||
modelInfo.put("contextSize", model.getModelProperties().getContextSize());
|
||||
modelInfo.put("maxChunks", model.getModelProperties().getMaxChunks());
|
||||
}
|
||||
|
||||
modelList.add(modelInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.put("models", modelList);
|
||||
result.put("total", modelList.size());
|
||||
|
||||
log.info("获取Rerank模型列表成功,共{}个提供商,{}个可用模型",
|
||||
response.getData().size(), modelList.size());
|
||||
|
||||
resultDomain.success("获取成功", result);
|
||||
return resultDomain;
|
||||
|
||||
} catch (DifyException e) {
|
||||
log.error("获取Rerank模型列表失败", e);
|
||||
resultDomain.fail("获取Rerank模型列表失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("获取Rerank模型列表异常", e);
|
||||
resultDomain.fail("获取Rerank模型列表异常: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -260,8 +260,7 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
|
||||
knowledge.getDifyDatasetId(),
|
||||
fileToUpload,
|
||||
originalFilename,
|
||||
uploadRequest,
|
||||
difyConfig.getApiKey());
|
||||
uploadRequest);
|
||||
|
||||
// 8. 保存到本地数据库
|
||||
TbAiUploadFile uploadFile = new TbAiUploadFile();
|
||||
@@ -378,8 +377,7 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
|
||||
try {
|
||||
difyApiClient.deleteDocument(
|
||||
knowledge.getDifyDatasetId(),
|
||||
file.getDifyDocumentId(),
|
||||
difyConfig.getApiKey());
|
||||
file.getDifyDocumentId());
|
||||
log.info("Dify文档删除成功: {}", file.getDifyDocumentId());
|
||||
} catch (DifyException e) {
|
||||
log.error("删除Dify文档失败,继续删除本地记录", e);
|
||||
@@ -512,8 +510,7 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
|
||||
try {
|
||||
DocumentStatusResponse statusResponse = difyApiClient.getDocumentStatus(
|
||||
knowledge.getDifyDatasetId(),
|
||||
file.getDifyBatchId(),
|
||||
difyConfig.getApiKey());
|
||||
file.getDifyBatchId());
|
||||
|
||||
// 4. 更新本地状态
|
||||
TbAiUploadFile update = new TbAiUploadFile();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.ai.TbAiKnowledge">
|
||||
<id column="id" property="id" jdbcType="VARCHAR"/>
|
||||
<result column="title" property="title" jdbcType="VARCHAR"/>
|
||||
<result column="avatar" property="avatar" jdbcType="VARCHAR"/>
|
||||
<result column="description" property="description" jdbcType="VARCHAR"/>
|
||||
<result column="content" property="content" jdbcType="LONGVARCHAR"/>
|
||||
<result column="source_type" property="sourceType" jdbcType="INTEGER"/>
|
||||
@@ -17,6 +18,11 @@
|
||||
<result column="dify_dataset_id" property="difyDatasetId" jdbcType="VARCHAR"/>
|
||||
<result column="dify_indexing_technique" property="difyIndexingTechnique" jdbcType="VARCHAR"/>
|
||||
<result column="embedding_model" property="embeddingModel" jdbcType="VARCHAR"/>
|
||||
<result column="embedding_model_provider" property="embeddingModelProvider" jdbcType="VARCHAR"/>
|
||||
<result column="rerank_model" property="rerankModel" jdbcType="VARCHAR"/>
|
||||
<result column="rerank_model_provider" property="rerankModelProvider" jdbcType="VARCHAR"/>
|
||||
<result column="retrieval_top_k" property="retrievalTopK" jdbcType="INTEGER"/>
|
||||
<result column="retrieval_score_threshold" property="retrievalScoreThreshold" jdbcType="DECIMAL"/>
|
||||
<result column="vector_id" property="vectorID" jdbcType="VARCHAR"/>
|
||||
<result column="document_count" property="documentCount" jdbcType="INTEGER"/>
|
||||
<result column="total_chunks" property="totalChunks" jdbcType="INTEGER"/>
|
||||
@@ -32,8 +38,9 @@
|
||||
|
||||
<!-- 基础字段 -->
|
||||
<sql id="Base_Column_List">
|
||||
id, title, description, content, source_type, source_id, file_name, file_path,
|
||||
category, tags, dify_dataset_id, dify_indexing_technique, embedding_model,
|
||||
id, title, avatar, description, content, source_type, source_id, file_name, file_path,
|
||||
category, tags, dify_dataset_id, dify_indexing_technique, embedding_model, embedding_model_provider,
|
||||
rerank_model, rerank_model_provider, retrieval_top_k, retrieval_score_threshold,
|
||||
vector_id, document_count, total_chunks, status, creator, creator_dept,
|
||||
updater, create_time, update_time, delete_time, deleted
|
||||
</sql>
|
||||
@@ -161,13 +168,15 @@
|
||||
<!-- insertKnowledge(插入知识库) -->
|
||||
<insert id="insertKnowledge" parameterType="org.xyzh.common.dto.ai.TbAiKnowledge">
|
||||
INSERT INTO tb_ai_knowledge (
|
||||
id, title, description, content, source_type, source_id, file_name, file_path,
|
||||
category, tags, dify_dataset_id, dify_indexing_technique, embedding_model,
|
||||
id, title, avatar, description, content, source_type, source_id, file_name, file_path,
|
||||
category, tags, dify_dataset_id, dify_indexing_technique, embedding_model, embedding_model_provider,
|
||||
rerank_model, rerank_model_provider, retrieval_top_k, retrieval_score_threshold,
|
||||
vector_id, document_count, total_chunks, status, creator, creator_dept,
|
||||
updater, create_time, update_time, deleted
|
||||
) VALUES (
|
||||
#{ID}, #{title}, #{description}, #{content}, #{sourceType}, #{sourceID}, #{fileName}, #{filePath},
|
||||
#{category}, #{tags}, #{difyDatasetId}, #{difyIndexingTechnique}, #{embeddingModel},
|
||||
#{ID}, #{title}, #{avatar}, #{description}, #{content}, #{sourceType}, #{sourceID}, #{fileName}, #{filePath},
|
||||
#{category}, #{tags}, #{difyDatasetId}, #{difyIndexingTechnique}, #{embeddingModel}, #{embeddingModelProvider},
|
||||
#{rerankModel}, #{rerankModelProvider}, #{retrievalTopK}, #{retrievalScoreThreshold},
|
||||
#{vectorID}, #{documentCount}, #{totalChunks}, #{status}, #{creator}, #{creatorDept},
|
||||
#{updater}, #{createTime}, #{updateTime}, #{deleted}
|
||||
)
|
||||
@@ -178,6 +187,7 @@
|
||||
UPDATE tb_ai_knowledge
|
||||
<set>
|
||||
<if test="title != null and title != ''">title = #{title},</if>
|
||||
<if test="avatar != null">avatar = #{avatar},</if>
|
||||
<if test="description != null">description = #{description},</if>
|
||||
<if test="content != null">content = #{content},</if>
|
||||
<if test="sourceType != null">source_type = #{sourceType},</if>
|
||||
@@ -189,6 +199,11 @@
|
||||
<if test="difyDatasetId != null">dify_dataset_id = #{difyDatasetId},</if>
|
||||
<if test="difyIndexingTechnique != null">dify_indexing_technique = #{difyIndexingTechnique},</if>
|
||||
<if test="embeddingModel != null">embedding_model = #{embeddingModel},</if>
|
||||
<if test="embeddingModelProvider != null">embedding_model_provider = #{embeddingModelProvider},</if>
|
||||
<if test="rerankModel != null">rerank_model = #{rerankModel},</if>
|
||||
<if test="rerankModelProvider != null">rerank_model_provider = #{rerankModelProvider},</if>
|
||||
<if test="retrievalTopK != null">retrieval_top_k = #{retrievalTopK},</if>
|
||||
<if test="retrievalScoreThreshold != null">retrieval_score_threshold = #{retrievalScoreThreshold},</if>
|
||||
<if test="vectorID != null">vector_id = #{vectorID},</if>
|
||||
<if test="documentCount != null">document_count = #{documentCount},</if>
|
||||
<if test="totalChunks != null">total_chunks = #{totalChunks},</if>
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.xyzh.common.core.page.PageParam;
|
||||
import org.xyzh.common.dto.ai.TbAiKnowledge;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description AI知识库管理服务接口
|
||||
@@ -65,7 +66,7 @@ public interface AiKnowledgeService {
|
||||
* @param pageParam 分页参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam);
|
||||
ResultDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam);
|
||||
|
||||
/**
|
||||
* 同步Dify知识库信息到本地
|
||||
@@ -103,4 +104,16 @@ public interface AiKnowledgeService {
|
||||
* @return 统计信息
|
||||
*/
|
||||
ResultDomain<TbAiKnowledge> getKnowledgeStats(String knowledgeId);
|
||||
|
||||
/**
|
||||
* 获取可用的嵌入模型列表
|
||||
* @return 嵌入模型列表
|
||||
*/
|
||||
ResultDomain<Map<String, Object>> getAvailableEmbeddingModels();
|
||||
|
||||
/**
|
||||
* 获取可用的Rerank模型列表
|
||||
* @return Rerank模型列表
|
||||
*/
|
||||
ResultDomain<Map<String, Object>> getAvailableRerankModels();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ public class TbAiKnowledge extends BaseDTO {
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* @description 知识库头像
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* @description 知识库描述
|
||||
*/
|
||||
@@ -73,6 +78,31 @@ public class TbAiKnowledge extends BaseDTO {
|
||||
*/
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* @description 向量模型提供商
|
||||
*/
|
||||
private String embeddingModelProvider;
|
||||
|
||||
/**
|
||||
* @description Rerank模型名称
|
||||
*/
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* @description Rerank模型提供商
|
||||
*/
|
||||
private String rerankModelProvider;
|
||||
|
||||
/**
|
||||
* @description 检索Top K(返回前K个结果)
|
||||
*/
|
||||
private Integer retrievalTopK;
|
||||
|
||||
/**
|
||||
* @description 检索分数阈值(0.00-1.00)
|
||||
*/
|
||||
private Double retrievalScoreThreshold;
|
||||
|
||||
/**
|
||||
* @description 向量ID(用于向量检索)
|
||||
*/
|
||||
@@ -116,6 +146,15 @@ public class TbAiKnowledge extends BaseDTO {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public void setAvatar(String avatar) {
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
@@ -260,6 +299,45 @@ public class TbAiKnowledge extends BaseDTO {
|
||||
this.updater = updater;
|
||||
}
|
||||
|
||||
public String getEmbeddingModelProvider() {
|
||||
return embeddingModelProvider;
|
||||
}
|
||||
public void setEmbeddingModelProvider(String embeddingModelProvider) {
|
||||
this.embeddingModelProvider = embeddingModelProvider;
|
||||
}
|
||||
|
||||
public String getRerankModel() {
|
||||
return rerankModel;
|
||||
}
|
||||
|
||||
public void setRerankModel(String rerankModel) {
|
||||
this.rerankModel = rerankModel;
|
||||
}
|
||||
|
||||
public String getRerankModelProvider() {
|
||||
return rerankModelProvider;
|
||||
}
|
||||
|
||||
public void setRerankModelProvider(String rerankModelProvider) {
|
||||
this.rerankModelProvider = rerankModelProvider;
|
||||
}
|
||||
|
||||
public Integer getRetrievalTopK() {
|
||||
return retrievalTopK;
|
||||
}
|
||||
|
||||
public void setRetrievalTopK(Integer retrievalTopK) {
|
||||
this.retrievalTopK = retrievalTopK;
|
||||
}
|
||||
|
||||
public Double getRetrievalScoreThreshold() {
|
||||
return retrievalScoreThreshold;
|
||||
}
|
||||
|
||||
public void setRetrievalScoreThreshold(Double retrievalScoreThreshold) {
|
||||
this.retrievalScoreThreshold = retrievalScoreThreshold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TbAiKnowledge{" +
|
||||
|
||||
@@ -94,16 +94,6 @@ export const knowledgeApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 从Dify同步知识库状态
|
||||
* @param knowledgeId 知识库ID
|
||||
* @returns Promise<ResultDomain<AiKnowledge>>
|
||||
*/
|
||||
async syncFromDify(knowledgeId: string): Promise<ResultDomain<AiKnowledge>> {
|
||||
const response = await api.get<AiKnowledge>(`/ai/knowledge/${knowledgeId}/sync`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置知识库权限
|
||||
* @param params 权限参数
|
||||
@@ -143,6 +133,15 @@ export const knowledgeApi = {
|
||||
async getStats(knowledgeId: string): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>(`/ai/knowledge/${knowledgeId}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可用的嵌入模型列表
|
||||
* @returns Promise<ResultDomain<any>>
|
||||
*/
|
||||
async getAvailableEmbeddingModels(): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>('/ai/knowledge/embedding-models');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -47,26 +47,48 @@ export interface AiAgentConfig extends BaseDTO {
|
||||
* AI知识库实体
|
||||
*/
|
||||
export interface AiKnowledge extends BaseDTO {
|
||||
/** 知识库名称 */
|
||||
name?: string;
|
||||
/** 知识库标题 */
|
||||
title?: string;
|
||||
/** 知识库头像 */
|
||||
avatar?: string;
|
||||
/** 知识库描述 */
|
||||
description?: string;
|
||||
/** 索引方式(high_quality=高质量, economy=经济) */
|
||||
indexingTechnique?: string;
|
||||
/** Embedding模型 */
|
||||
embeddingModel?: string;
|
||||
/** 内容 */
|
||||
content?: string;
|
||||
/** 来源类型 */
|
||||
sourceType?: number;
|
||||
/** 来源ID */
|
||||
sourceID?: string;
|
||||
/** 文件名 */
|
||||
fileName?: string;
|
||||
/** 文件路径 */
|
||||
filePath?: string;
|
||||
/** 分类 */
|
||||
category?: string;
|
||||
/** 标签 */
|
||||
tags?: string;
|
||||
/** Dify数据集ID */
|
||||
difyDatasetId?: string;
|
||||
/** 同步状态(0未同步 1已同步 2同步失败) */
|
||||
syncStatus?: number;
|
||||
/** Dify索引方式(high_quality=高质量, economy=经济) */
|
||||
difyIndexingTechnique?: string;
|
||||
/** Embedding模型 */
|
||||
embeddingModel?: string;
|
||||
/** Embedding模型提供商 */
|
||||
embeddingModelProvider?: string;
|
||||
/** 向量ID */
|
||||
vectorID?: string;
|
||||
/** 文档数量 */
|
||||
documentCount?: number;
|
||||
/** 字符数 */
|
||||
characterCount?: number;
|
||||
/** 创建者部门 */
|
||||
creatorDept?: string;
|
||||
/** 总分段数 */
|
||||
totalChunks?: number;
|
||||
/** 状态(0禁用 1启用) */
|
||||
status?: number;
|
||||
/** 创建者 */
|
||||
creator?: string;
|
||||
/** 创建者部门 */
|
||||
creatorDept?: string;
|
||||
/** 更新者 */
|
||||
updater?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,556 @@
|
||||
<template>
|
||||
<div class="knowledge-basic-container">
|
||||
<!-- 查看模式 -->
|
||||
<div v-if="type === 'view'" class="view-mode">
|
||||
<div class="info-section">
|
||||
<div class="avatar-section">
|
||||
<img v-if="knowledge?.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
|
||||
<div v-else class="default-avatar">📚</div>
|
||||
</div>
|
||||
<div class="info-details">
|
||||
<h2 class="knowledge-name">{{ knowledge?.title || '未命名' }}</h2>
|
||||
<p class="knowledge-description">{{ knowledge?.description || '暂无描述' }}</p>
|
||||
<div class="meta-info">
|
||||
<div class="meta-item">
|
||||
<span class="label">状态:</span>
|
||||
<el-tag :type="knowledge?.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ knowledge?.status === 1 ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="meta-item" v-if="knowledge?.difyDatasetId">
|
||||
<span class="label">Dify数据集ID:</span>
|
||||
<span class="value">{{ knowledge.difyDatasetId }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">索引方式:</span>
|
||||
<span class="value">{{ getIndexingText(knowledge?.difyIndexingTechnique) }}</span>
|
||||
</div>
|
||||
<div class="meta-item" v-if="knowledge?.difyIndexingTechnique == 'high_quality'">
|
||||
<span class="label">Embedding模型:</span>
|
||||
<span class="value">{{ knowledge?.embeddingModel || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">文档数量:</span>
|
||||
<span class="value">{{ knowledge?.documentCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">创建时间:</span>
|
||||
<span class="value">{{ formatDate(knowledge?.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑模式 -->
|
||||
<div v-else class="edit-mode">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="knowledge-form"
|
||||
>
|
||||
<el-form-item label="知识库标题" prop="title" required>
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
placeholder="请输入知识库标题"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
:disabled="props.type === 'view'"
|
||||
/>
|
||||
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="知识库头像" prop="avatar">
|
||||
<FileUpload
|
||||
:cover-url="formData.avatar"
|
||||
@update:cover-url="handleAvatarUpdate"
|
||||
:as-dialog="false"
|
||||
list-type="cover"
|
||||
accept="image/*"
|
||||
:max-size="2"
|
||||
module="ai-knowledge"
|
||||
tip="点击上传知识库头像"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="知识库描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入知识库描述,介绍知识库的内容、用途等..."
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="索引方式" prop="difyIndexingTechnique" required>
|
||||
<el-radio-group v-model="formData.difyIndexingTechnique">
|
||||
<el-radio value="high_quality">
|
||||
<span class="radio-label">高质量</span>
|
||||
<span class="radio-desc">更精确的检索结果,消耗更多tokens</span>
|
||||
</el-radio>
|
||||
<el-radio
|
||||
value="economy"
|
||||
:disabled="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'"
|
||||
>
|
||||
<span class="radio-label">经济</span>
|
||||
<span class="radio-desc">快速检索,消耗较少tokens</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'" class="form-tip">
|
||||
高质量模式不能降级为经济模式
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.difyIndexingTechnique == 'high_quality'" label="Embedding模型" prop="embeddingModel">
|
||||
<el-select
|
||||
v-model="formData.embeddingModel"
|
||||
placeholder="请选择Embedding模型(可选)"
|
||||
clearable
|
||||
filterable
|
||||
:loading="modelsLoading"
|
||||
@change="handleModelChange"
|
||||
>
|
||||
<el-option-group
|
||||
v-for="provider in embeddingModels"
|
||||
:key="provider.provider"
|
||||
:label="provider.label || provider.provider"
|
||||
>
|
||||
<el-option
|
||||
v-for="model in provider.models"
|
||||
:key="model.model"
|
||||
:label="getModelLabel(model)"
|
||||
:value="model.model"
|
||||
:data-provider="model.provider"
|
||||
>
|
||||
<span>{{ getModelLabel(model) }}</span>
|
||||
<span v-if="model.contextSize" style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
|
||||
上下文: {{ model.contextSize }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
<div v-if="type === 'edit'" class="form-tip form-tip-success">
|
||||
可以切换Embedding模型以优化检索效果
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Dify数据集ID" prop="difyDatasetId">
|
||||
<el-input
|
||||
v-model="formData.difyDatasetId"
|
||||
placeholder="可选,如需关联现有Dify数据集请填写"
|
||||
:disabled="type === 'edit'"
|
||||
/>
|
||||
<div v-if="type === 'edit'" class="form-tip">
|
||||
Dify数据集ID创建后不可修改
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch
|
||||
v-model="formData.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
{{ type === 'add' ? '创建知识库' : '保存修改' }}
|
||||
</el-button>
|
||||
<el-button @click="handleCancel">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { FileUpload } from '@/components/file';
|
||||
import type { AiKnowledge } from '@/types/ai';
|
||||
import { knowledgeApi } from '@/apis/ai';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
defineOptions({
|
||||
name: 'KnowledgeBasic'
|
||||
});
|
||||
|
||||
interface KnowledgeBasicProps {
|
||||
type: 'view' | 'add' | 'edit';
|
||||
knowledge?: AiKnowledge | null;
|
||||
}
|
||||
|
||||
const props = defineProps<KnowledgeBasicProps>();
|
||||
const emit = defineEmits<{
|
||||
success: [knowledge: AiKnowledge];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const modelsLoading = ref(false);
|
||||
const embeddingModels = ref<any[]>([]);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<Partial<AiKnowledge>>({
|
||||
title: '',
|
||||
avatar: '',
|
||||
description: '',
|
||||
difyIndexingTechnique: 'high_quality',
|
||||
embeddingModel: '',
|
||||
embeddingModelProvider: '',
|
||||
difyDatasetId: '',
|
||||
status: 1
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入知识库标题', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '标题长度在2-100个字符之间', trigger: 'blur' }
|
||||
],
|
||||
difyIndexingTechnique: [
|
||||
{ required: true, message: '请选择索引方式', trigger: 'change' }
|
||||
]
|
||||
};
|
||||
|
||||
// 监听知识库数据变化
|
||||
watch(() => props.knowledge, (newVal) => {
|
||||
if (newVal && (props.type === 'view' || props.type === 'edit')) {
|
||||
Object.assign(formData, {
|
||||
id: newVal.id,
|
||||
title: newVal.title,
|
||||
avatar: newVal.avatar,
|
||||
description: newVal.description,
|
||||
difyIndexingTechnique: newVal.difyIndexingTechnique || 'high_quality',
|
||||
embeddingModel: newVal.embeddingModel,
|
||||
embeddingModelProvider: newVal.embeddingModelProvider,
|
||||
difyDatasetId: newVal.difyDatasetId,
|
||||
status: newVal.status ?? 1
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 处理头像更新
|
||||
function handleAvatarUpdate(val: string) {
|
||||
formData.avatar = val;
|
||||
}
|
||||
|
||||
// 加载嵌入模型列表
|
||||
async function loadEmbeddingModels() {
|
||||
try {
|
||||
modelsLoading.value = true;
|
||||
const result = await knowledgeApi.getAvailableEmbeddingModels();
|
||||
if (result.success && result.data) {
|
||||
// 按提供商分组
|
||||
const providers = result.data.providers || [];
|
||||
embeddingModels.value = providers.map((provider: any) => ({
|
||||
provider: provider.provider,
|
||||
label: provider.label?.zh_Hans || provider.label?.en_US || provider.provider,
|
||||
models: (provider.models || []).map((model: any) => ({
|
||||
model: model.model,
|
||||
provider: model.provider || provider.provider, // 添加 provider 到 model 对象
|
||||
label: model.label?.zh_Hans || model.label?.en_US || model.model,
|
||||
contextSize: model.model_properties?.context_size,
|
||||
status: model.status
|
||||
}))
|
||||
}));
|
||||
} else {
|
||||
ElMessage.warning('获取嵌入模型列表失败,使用默认选项');
|
||||
// 设置默认模型列表
|
||||
embeddingModels.value = [{
|
||||
provider: 'openai',
|
||||
label: 'OpenAI',
|
||||
models: [
|
||||
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
|
||||
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
|
||||
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
|
||||
]
|
||||
}];
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载嵌入模型列表失败:', error);
|
||||
// 设置默认模型列表
|
||||
embeddingModels.value = [{
|
||||
provider: 'openai',
|
||||
label: 'OpenAI',
|
||||
models: [
|
||||
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
|
||||
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
|
||||
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
|
||||
]
|
||||
}];
|
||||
} finally {
|
||||
modelsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模型显示标签
|
||||
function getModelLabel(model: any): string {
|
||||
return model.label || model.model;
|
||||
}
|
||||
|
||||
// 处理模型变化
|
||||
function handleModelChange(modelName: string) {
|
||||
if (!modelName) {
|
||||
formData.embeddingModelProvider = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找选中模型的提供商
|
||||
for (const providerGroup of embeddingModels.value) {
|
||||
const foundModel = providerGroup.models.find((m: any) => m.model === modelName);
|
||||
if (foundModel) {
|
||||
formData.embeddingModelProvider = foundModel.provider;
|
||||
console.log('选择模型:', modelName, '提供商:', foundModel.provider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载模型列表
|
||||
loadEmbeddingModels();
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
const valid = await formRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
if (props.type === 'add') {
|
||||
// 创建知识库
|
||||
const result = await knowledgeApi.createKnowledge(formData as AiKnowledge);
|
||||
if (result.success && result.data) {
|
||||
ElMessage.success('创建成功');
|
||||
emit('success', result.data);
|
||||
} else {
|
||||
ElMessage.error(result.message || '创建失败');
|
||||
}
|
||||
} else if (props.type === 'edit') {
|
||||
// 更新知识库
|
||||
const result = await knowledgeApi.updateKnowledge(formData as AiKnowledge);
|
||||
if (result.success && result.data) {
|
||||
ElMessage.success('保存成功');
|
||||
emit('success', result.data);
|
||||
} else {
|
||||
ElMessage.error(result.message || '保存失败');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('提交失败:', error);
|
||||
ElMessage.error(props.type === 'add' ? '创建失败' : '保存失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// 获取索引方式文本
|
||||
function getIndexingText(technique: string | undefined): string {
|
||||
switch (technique) {
|
||||
case 'high_quality':
|
||||
return '高质量';
|
||||
case 'economy':
|
||||
return '经济';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.knowledge-basic-container {
|
||||
padding: 24px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 14px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
// 查看模式样式
|
||||
.view-mode {
|
||||
.info-section {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #F8F9FA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.default-avatar {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.knowledge-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.knowledge-description {
|
||||
font-size: 14px;
|
||||
color: #667085;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.label {
|
||||
color: #667085;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #101828;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑模式样式
|
||||
.edit-mode {
|
||||
.knowledge-form {
|
||||
max-width: 600px;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-radio) {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
height: auto;
|
||||
|
||||
.el-radio__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.radio-desc {
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #F59E0B;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-tip-success {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #F3F3F5;
|
||||
|
||||
.el-button {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 12px 24px;
|
||||
|
||||
&.el-button--primary {
|
||||
background: #E7000B;
|
||||
border-color: #E7000B;
|
||||
|
||||
&:hover {
|
||||
background: #C90009;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-switch) {
|
||||
.el-switch__label {
|
||||
font-size: 14px;
|
||||
color: #667085;
|
||||
|
||||
&.is-active {
|
||||
color: #E7000B;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="knowledge-card" @click="handleClick">
|
||||
<div class="card-header">
|
||||
<!-- 头像 -->
|
||||
<div class="knowledge-avatar">
|
||||
<img v-if="knowledge.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
|
||||
<div v-else class="default-avatar">📚</div>
|
||||
</div>
|
||||
|
||||
<!-- 标题和状态 -->
|
||||
<div class="knowledge-title-section">
|
||||
<h3 class="knowledge-title">{{ knowledge.title }}</h3>
|
||||
<span
|
||||
class="knowledge-status"
|
||||
:class="knowledge.status === 1 ? 'status-active' : 'status-inactive'"
|
||||
>
|
||||
{{ knowledge.status === 1 ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div class="knowledge-description">
|
||||
{{ knowledge.description || '暂无描述' }}
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="knowledge-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">文档数量</span>
|
||||
<span class="stat-value">{{ knowledge.documentCount || 0 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">状态</span>
|
||||
<span class="stat-value">
|
||||
{{ getStatusText(knowledge.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="knowledge-actions" @click.stop>
|
||||
<el-button size="small" @click="handleEdit">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AiKnowledge } from '@/types/ai';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
defineOptions({
|
||||
name: 'KnowledgeCard'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
knowledge: AiKnowledge;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['click', 'edit', 'delete', 'sync']);
|
||||
|
||||
function handleClick() {
|
||||
emit('click', props.knowledge);
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
emit('edit', props.knowledge);
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
emit('delete', props.knowledge);
|
||||
}
|
||||
|
||||
function getStatusText(status?: number) {
|
||||
switch (status) {
|
||||
case 0: return '禁用';
|
||||
case 1: return '启用';
|
||||
case 2: return '处理中';
|
||||
default: return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.knowledge-card {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #E7000B;
|
||||
box-shadow: 0 4px 12px rgba(231, 0, 11, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.knowledge-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #F3F3F5;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.default-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-title-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.knowledge-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.knowledge-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&.status-active {
|
||||
background: #DCFCE7;
|
||||
color: #008236;
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
background: #FEF2F2;
|
||||
color: #DC2626;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-description {
|
||||
font-size: 14px;
|
||||
color: #6B7280;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.knowledge-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #F3F3F5;
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #9CA3AF;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
:deep(.el-button) {
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
&.el-button--default {
|
||||
background: #F3F3F5;
|
||||
border-color: transparent;
|
||||
color: #0A0A0A;
|
||||
|
||||
&:hover {
|
||||
background: #E5E5E7;
|
||||
}
|
||||
}
|
||||
|
||||
&.el-button--danger {
|
||||
background: #FEF2F2;
|
||||
border-color: transparent;
|
||||
color: #DC2626;
|
||||
|
||||
&:hover {
|
||||
background: #FEE2E2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,639 @@
|
||||
<template>
|
||||
<div class="knowledge-info-panel" v-if="knowledge">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="panel-header">
|
||||
<el-button text @click="$emit('close')">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回列表
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button @click="handleEdit">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleUpload">
|
||||
<el-icon><Upload /></el-icon>
|
||||
上传文档
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识库信息卡片 -->
|
||||
<div class="knowledge-header">
|
||||
<div class="knowledge-avatar">
|
||||
<el-image
|
||||
v-if="knowledge.avatar"
|
||||
:src="knowledge.avatar"
|
||||
fit="cover"
|
||||
/>
|
||||
<el-icon v-else :size="48"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="knowledge-details">
|
||||
<h2 class="knowledge-name">{{ knowledge.title }}</h2>
|
||||
<p class="knowledge-description">{{ knowledge.description || '暂无描述' }}</p>
|
||||
<div class="knowledge-meta">
|
||||
<el-tag :type="knowledge.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ knowledge.status === 1 ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
<span class="meta-item">
|
||||
<el-icon><Clock /></el-icon>
|
||||
{{ formatDate(knowledge.createTime) }}
|
||||
</span>
|
||||
<span class="meta-item" v-if="knowledge.difyDatasetId">
|
||||
<el-icon><Link /></el-icon>
|
||||
Dify: {{ knowledge.difyDatasetId }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ documents.length }}</div>
|
||||
<div class="stat-label">上传文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文档列表 -->
|
||||
<div class="documents-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">文档列表</h3>
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索文档..."
|
||||
clearable
|
||||
style="width: 300px;"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="filteredDocuments"
|
||||
v-loading="loading"
|
||||
style="width: 100%"
|
||||
:empty-text="loading ? '加载中...' : '暂无文档'"
|
||||
>
|
||||
<el-table-column prop="fileName" label="文件名" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="file-name-cell">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ row.fileName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileType" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.fileType || 'unknown' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileSize" label="大小" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.fileSize) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 2 ? 'success' : row.status === 1 ? 'warning' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewSegments(row)">
|
||||
查看分段
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="handleDownload(row)">
|
||||
下载
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDeleteDocument(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 上传对话框 -->
|
||||
<el-dialog
|
||||
v-model="uploadDialogVisible"
|
||||
title="上传文档到知识库"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:file-list="fileList"
|
||||
drag
|
||||
multiple
|
||||
accept=".txt,.md,.pdf,.doc,.docx"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 txt/md/pdf/doc/docx 格式文件,单个文件不超过50MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmUpload" :loading="uploading">
|
||||
确认上传
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 文档分段对话框 -->
|
||||
<DocumentSegmentDialog
|
||||
v-if="selectedDocument && selectedDocument.difyDocumentId && props.knowledge?.difyDatasetId"
|
||||
:model-value="segmentDialogVisible"
|
||||
:dataset-id="props.knowledge.difyDatasetId"
|
||||
:document-id="selectedDocument.difyDocumentId"
|
||||
@update:model-value="segmentDialogVisible = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { UploadUserFile, UploadInstance } from 'element-plus';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Upload,
|
||||
Refresh,
|
||||
Edit,
|
||||
Document,
|
||||
Clock,
|
||||
Link,
|
||||
Files,
|
||||
Paperclip,
|
||||
Search,
|
||||
UploadFilled
|
||||
} from '@element-plus/icons-vue';
|
||||
import type { AiKnowledge, AiUploadFile } from '@/types/ai';
|
||||
import { fileUploadApi, knowledgeApi } from '@/apis/ai';
|
||||
import { DocumentSegmentDialog } from './';
|
||||
|
||||
defineOptions({
|
||||
name: 'KnowledgeInfo'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
knowledge: AiKnowledge | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
refresh: [];
|
||||
edit: [knowledge: AiKnowledge];
|
||||
}>();
|
||||
|
||||
// 数据状态
|
||||
const documents = ref<AiUploadFile[]>([]);
|
||||
const loading = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const uploadDialogVisible = ref(false);
|
||||
const segmentDialogVisible = ref(false);
|
||||
const selectedDocument = ref<AiUploadFile | null>(null);
|
||||
const uploading = ref(false);
|
||||
|
||||
// 上传相关
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const fileList = ref<UploadUserFile[]>([]);
|
||||
|
||||
// 计算属性
|
||||
const filteredDocuments = computed(() => {
|
||||
if (!searchQuery.value) return documents.value;
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return documents.value.filter(doc =>
|
||||
doc.fileName?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// 监听知识库变化
|
||||
watch(() => props.knowledge, (newVal) => {
|
||||
if (newVal?.id) {
|
||||
loadDocuments();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 加载文档列表
|
||||
async function loadDocuments() {
|
||||
if (!props.knowledge?.id) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const result = await fileUploadApi.listFilesByKnowledge(props.knowledge.id);
|
||||
if (result.success) {
|
||||
documents.value = (result.dataList || []) as AiUploadFile[];
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载文档列表失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载文档列表失败:', error);
|
||||
ElMessage.error('加载文档列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑知识库
|
||||
function handleEdit() {
|
||||
if (props.knowledge) {
|
||||
emit('edit', props.knowledge);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传
|
||||
function handleUpload() {
|
||||
uploadDialogVisible.value = true;
|
||||
fileList.value = [];
|
||||
}
|
||||
|
||||
// 文件变化
|
||||
function handleFileChange(file: any, fileListParam: any) {
|
||||
fileList.value = fileListParam;
|
||||
}
|
||||
|
||||
// 确认上传
|
||||
async function handleConfirmUpload() {
|
||||
if (fileList.value.length === 0) {
|
||||
ElMessage.warning('请选择要上传的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.knowledge?.id) {
|
||||
ElMessage.error('知识库ID不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uploading.value = true;
|
||||
const files = fileList.value
|
||||
.map(f => f.raw)
|
||||
.filter(f => f !== null && f !== undefined) as File[];
|
||||
|
||||
// 逐个上传文件
|
||||
for (const file of files) {
|
||||
const result = await fileUploadApi.uploadFile(props.knowledge.id, file);
|
||||
if (!result.success) {
|
||||
ElMessage.error(`文件 ${file.name} 上传失败: ${result.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success('文件上传成功');
|
||||
uploadDialogVisible.value = false;
|
||||
fileList.value = [];
|
||||
await loadDocuments();
|
||||
emit('refresh');
|
||||
} catch (error: any) {
|
||||
console.error('上传文件失败:', error);
|
||||
ElMessage.error('上传文件失败');
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查看分段
|
||||
function handleViewSegments(document: AiUploadFile) {
|
||||
selectedDocument.value = document;
|
||||
segmentDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
function handleDownload(document: AiUploadFile) {
|
||||
if (document.sysFileId) {
|
||||
window.open(`/api/file/download/${document.sysFileId}`, '_blank');
|
||||
} else if (document.filePath) {
|
||||
window.open(`/api/file/download/${document.filePath}`, '_blank');
|
||||
} else {
|
||||
ElMessage.warning('文件路径不存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文档
|
||||
async function handleDeleteDocument(document: AiUploadFile) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除文档"${document.fileName}"吗?此操作不可恢复。`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
if (document.id) {
|
||||
const result = await fileUploadApi.deleteFile(document.id);
|
||||
if (result.success) {
|
||||
ElMessage.success('删除成功');
|
||||
await loadDocuments();
|
||||
emit('refresh');
|
||||
} else {
|
||||
ElMessage.error(result.message || '删除失败');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除文档失败:', error);
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatDate(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number | undefined): string {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
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">
|
||||
.knowledge-info-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #FFFFFF;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
background: #FFFFFF;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
|
||||
&.el-button--primary {
|
||||
background: #E7000B;
|
||||
border-color: #E7000B;
|
||||
|
||||
&:hover {
|
||||
background: #C90009;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #F8F9FA 0%, #FFFFFF 100%);
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
|
||||
.knowledge-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 12px;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.el-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: #E7000B;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.knowledge-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.knowledge-description {
|
||||
font-size: 14px;
|
||||
color: #667085;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.knowledge-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
|
||||
.stat-card {
|
||||
background: #F8F9FA;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.documents-section {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.el-icon {
|
||||
color: #E7000B;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-table__header th {
|
||||
background: #F8F9FA;
|
||||
color: #667085;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
|
||||
.el-dialog__header {
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.el-upload-dragger {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as KnowledgeCard } from './KnowledgeCard.vue';
|
||||
export { default as KnowledgeInfo } from './KnowledgeInfo.vue';
|
||||
export { default as KnowledgeBasic } from './KnowledgeBasic.vue';
|
||||
export { default as DocumentSegmentDialog } from './DocumentSegmentDialog.vue';
|
||||
@@ -679,6 +679,19 @@ async function sendMessage() {
|
||||
// 调用API
|
||||
isGenerating.value = true;
|
||||
|
||||
// 立即创建一个空的AI消息,用于显示加载动画
|
||||
messages.value.push({
|
||||
id: `temp-ai-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
conversationID: currentConversation.value?.id || '',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString()
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
let aiMessageContent = '';
|
||||
|
||||
@@ -710,6 +723,12 @@ async function sendMessage() {
|
||||
// 保存AI消息的数据库ID(task_id),用于停止生成
|
||||
currentMessageId.value = initData.messageId;
|
||||
console.log('[保存MessageID(TaskID)]', initData.messageId);
|
||||
|
||||
// 更新最后一条AI消息的临时ID为真实的数据库ID
|
||||
const lastMessage = messages.value[messages.value.length - 1];
|
||||
if (lastMessage && lastMessage.role === 'assistant') {
|
||||
lastMessage.id = initData.messageId;
|
||||
}
|
||||
},
|
||||
onMessage: (chunk: string) => {
|
||||
// 确保AI消息已创建(即使内容为空)
|
||||
@@ -900,6 +919,11 @@ async function regenerateMessage(messageId: string) {
|
||||
// 保存AI消息的数据库ID(task_id),用于停止生成
|
||||
currentMessageId.value = initData.messageId;
|
||||
console.log('[保存MessageID(TaskID)-重新生成]', initData.messageId);
|
||||
|
||||
// 如果后端返回了新的messageId,更新消息对象的ID
|
||||
if (initData.messageId !== messageId) {
|
||||
messages.value[messageIndex].id = initData.messageId;
|
||||
}
|
||||
},
|
||||
onMessage: (chunk: string) => {
|
||||
// 累加内容(包括空chunk,因为后端可能分块发送)
|
||||
@@ -907,10 +931,9 @@ async function regenerateMessage(messageId: string) {
|
||||
aiMessageContent += chunk;
|
||||
}
|
||||
|
||||
// 找到对应消息并更新
|
||||
const msgIndex = messages.value.findIndex(m => m.id === messageId);
|
||||
if (msgIndex !== -1) {
|
||||
messages.value[msgIndex].content = aiMessageContent;
|
||||
// 直接使用messageIndex更新消息内容
|
||||
if (messageIndex !== -1) {
|
||||
messages.value[messageIndex].content = aiMessageContent;
|
||||
}
|
||||
|
||||
nextTick(() => scrollToBottom());
|
||||
|
||||
Reference in New Issue
Block a user