知识库创建

This commit is contained in:
2025-11-06 19:08:20 +08:00
parent 0bb4853d54
commit d9947e273c
23 changed files with 2563 additions and 1018 deletions

View File

@@ -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;

View File

@@ -27,6 +27,7 @@ DROP TABLE IF EXISTS `tb_ai_knowledge`;
CREATE TABLE `tb_ai_knowledge` ( CREATE TABLE `tb_ai_knowledge` (
`id` VARCHAR(50) NOT NULL COMMENT '知识库ID', `id` VARCHAR(50) NOT NULL COMMENT '知识库ID',
`title` VARCHAR(255) NOT NULL COMMENT '知识库标题', `title` VARCHAR(255) NOT NULL COMMENT '知识库标题',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '知识库头像',
`description` VARCHAR(500) DEFAULT NULL COMMENT '知识库描述', `description` VARCHAR(500) DEFAULT NULL COMMENT '知识库描述',
`content` LONGTEXT COMMENT '知识内容(手动添加时使用)', `content` LONGTEXT COMMENT '知识内容(手动添加时使用)',
`source_type` INT(4) DEFAULT 1 COMMENT '来源类型1手动添加 2文件导入 3资源同步', `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知识库IDDataset ID', `dify_dataset_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify知识库IDDataset ID',
`dify_indexing_technique` VARCHAR(50) DEFAULT 'high_quality' COMMENT 'Dify索引方式high_quality/economy', `dify_indexing_technique` VARCHAR(50) DEFAULT 'high_quality' COMMENT 'Dify索引方式high_quality/economy',
`embedding_model` VARCHAR(100) DEFAULT NULL COMMENT '向量模型名称', `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用于向量检索', `vector_id` VARCHAR(100) DEFAULT NULL COMMENT '向量ID用于向量检索',
`document_count` INT(11) DEFAULT 0 COMMENT '文档数量', `document_count` INT(11) DEFAULT 0 COMMENT '文档数量',
`total_chunks` 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` 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 VALUES
('agent_default_001', '校园助手', NULL, '我是您的智能校园助手,可以帮助您解答校园相关问题', ('agent_default_001', '校园助手', NULL, '我是您的智能校园助手,可以帮助您解答校园相关问题',
0, 1, '1', NOW()); 0, 'app-fwOqGFLTsZtekCQYlOmj9f8x', 1, '1', NOW());
-- 插入示例知识库(需要配合权限表使用) -- 插入示例知识库(需要配合权限表使用)
INSERT INTO `tb_ai_knowledge` INSERT INTO `tb_ai_knowledge`

View File

@@ -65,14 +65,14 @@ public class DifyApiClient {
/** /**
* 创建知识库Dataset * 创建知识库Dataset
*/ */
public DatasetCreateResponse createDataset(DatasetCreateRequest request, String apiKey) { public DatasetCreateResponse createDataset(DatasetCreateRequest request) {
String url = difyConfig.getFullApiUrl("/datasets"); String url = difyConfig.getFullApiUrl("/datasets");
try { try {
String jsonBody = JSON.toJSONString(request); String jsonBody = JSON.toJSONString(request);
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, MediaType.parse("application/json"))) .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
.build(); .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); String url = difyConfig.getFullApiUrl("/datasets?page=" + page + "&limit=" + limit);
try { try {
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.get() .get()
.build(); .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); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
try { try {
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.get() .get()
.build(); .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); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
try { try {
String jsonBody = JSON.toJSONString(request); String jsonBody = JSON.toJSONString(request);
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.patch(RequestBody.create(jsonBody, MediaType.parse("application/json"))) .patch(RequestBody.create(jsonBody, MediaType.parse("application/json")))
.build(); .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); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
try { try {
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.delete() .delete()
.build(); .build();
@@ -258,8 +258,7 @@ public class DifyApiClient {
String datasetId, String datasetId,
File file, File file,
String originalFilename, String originalFilename,
DocumentUploadRequest uploadRequest, DocumentUploadRequest uploadRequest) {
String apiKey) {
String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/document/create_by_file"); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/document/create_by_file");
@@ -282,7 +281,7 @@ public class DifyApiClient {
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.post(bodyBuilder.build()) .post(bodyBuilder.build())
.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"); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents/" + batchId + "/indexing-status");
try { try {
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.get() .get()
.build(); .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); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents?page=" + page + "&limit=" + limit);
try { try {
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.get() .get()
.build(); .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); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents/" + documentId);
try { try {
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.delete() .delete()
.build(); .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"); String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/retrieve");
try { try {
String jsonBody = JSON.toJSONString(request); String jsonBody = JSON.toJSONString(request);
Request httpRequest = new Request.Builder() Request httpRequest = new Request.Builder()
.url(url) .url(url)
.header("Authorization", "Bearer " + getApiKey(apiKey)) .header("Authorization", "Bearer " + getKnowledgeApiKey())
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.post(RequestBody.create(jsonBody, MediaType.parse("application/json"))) .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
.build(); .build();
@@ -850,6 +849,7 @@ public class DifyApiClient {
/** /**
* 获取API密钥优先使用传入的密钥否则使用配置的默认密钥 * 获取API密钥优先使用传入的密钥否则使用配置的默认密钥
* 用于智能体相关的API
*/ */
private String getApiKey(String apiKey) { private String getApiKey(String apiKey) {
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
@@ -861,6 +861,17 @@ public class DifyApiClient {
throw new DifyException("未配置Dify API密钥"); 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; 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);
}
}
} }

View File

@@ -35,6 +35,18 @@ public class DatasetCreateRequest {
@JsonProperty("embedding_model") @JsonProperty("embedding_model")
private String embeddingModel; 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团队所有成员 * 权限only_me仅自己、all_team_members团队所有成员
*/ */

View File

@@ -1,5 +1,6 @@
package org.xyzh.ai.client.dto; package org.xyzh.ai.client.dto;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data; import lombok.Data;
/** /**
@@ -21,5 +22,29 @@ public class DatasetUpdateRequest {
* 知识库描述 * 知识库描述
*/ */
private String description; 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;
} }

View File

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

View File

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

View File

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

View File

@@ -33,6 +33,8 @@ public class DifyConfig {
// private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f"; // private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f";
private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x"; private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x";
private String knowledgeApiKey="dataset-HeDK9gHBqPnI4rBZ2q2Hm7rV";
/** /**
* 请求超时时间(秒) * 请求超时时间(秒)
*/ */

View File

@@ -30,21 +30,19 @@ public class AiKnowledgeController {
/** /**
* @description 创建知识库 * @description 创建知识库
* @param requestBody 请求体knowledge, permissionType, deptIds, roleIds * @param knowledge 知识库对象
* @return ResultDomain<TbAiKnowledge> * @return ResultDomain<TbAiKnowledge>
* @author AI Assistant * @author AI Assistant
* @since 2025-11-04 * @since 2025-11-04
*/ */
@PostMapping @PostMapping
public ResultDomain<TbAiKnowledge> createKnowledge(@RequestBody Map<String, Object> requestBody) { public ResultDomain<TbAiKnowledge> createKnowledge(@RequestBody TbAiKnowledge knowledge) {
TbAiKnowledge knowledge = (TbAiKnowledge) requestBody.get("knowledge"); // 默认权限为PUBLIC公开
String permissionType = (String) requestBody.get("permissionType"); String permissionType = "PUBLIC";
@SuppressWarnings("unchecked") List<String> deptIds = null;
List<String> deptIds = (List<String>) requestBody.get("deptIds"); List<String> roleIds = null;
@SuppressWarnings("unchecked")
List<String> roleIds = (List<String>) requestBody.get("roleIds");
log.info("创建知识库: permissionType={}", permissionType); log.info("创建知识库: name={}", knowledge.getTitle());
return knowledgeService.createKnowledge(knowledge, permissionType, deptIds, roleIds); return knowledgeService.createKnowledge(knowledge, permissionType, deptIds, roleIds);
} }
@@ -109,7 +107,7 @@ public class AiKnowledgeController {
* @since 2025-11-04 * @since 2025-11-04
*/ */
@PostMapping("/page") @PostMapping("/page")
public PageDomain<TbAiKnowledge> pageKnowledges(@RequestBody PageRequest<TbAiKnowledge> pageRequest) { public ResultDomain<TbAiKnowledge> pageKnowledges(@RequestBody PageRequest<TbAiKnowledge> pageRequest) {
log.info("分页查询知识库"); log.info("分页查询知识库");
return knowledgeService.pageKnowledges(pageRequest.getFilter(), pageRequest.getPageParam()); return knowledgeService.pageKnowledges(pageRequest.getFilter(), pageRequest.getPageParam());
} }
@@ -175,4 +173,28 @@ public class AiKnowledgeController {
log.info("获取知识库统计信息: id={}", id); log.info("获取知识库统计信息: id={}", id);
return knowledgeService.getKnowledgeStats(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();
}
} }

View File

@@ -10,6 +10,9 @@ import org.xyzh.ai.client.dto.DatasetCreateRequest;
import org.xyzh.ai.client.dto.DatasetCreateResponse; import org.xyzh.ai.client.dto.DatasetCreateResponse;
import org.xyzh.ai.client.dto.DatasetDetailResponse; import org.xyzh.ai.client.dto.DatasetDetailResponse;
import org.xyzh.ai.client.dto.DatasetUpdateRequest; 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.config.DifyConfig;
import org.xyzh.ai.exception.AiKnowledgeException; import org.xyzh.ai.exception.AiKnowledgeException;
import org.xyzh.ai.exception.DifyException; 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.common.vo.UserDeptRoleVO;
import org.xyzh.system.utils.LoginUtil; import org.xyzh.system.utils.LoginUtil;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -101,12 +107,50 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
embeddingModel = difyConfig.getDataset().getDefaultEmbeddingModel(); embeddingModel = difyConfig.getDataset().getDefaultEmbeddingModel();
} }
difyRequest.setEmbeddingModel(embeddingModel); difyRequest.setEmbeddingModel(embeddingModel);
// 设置模型提供商(从前端传入或使用配置默认值)
String provider = knowledge.getEmbeddingModelProvider();
// 调用Dify API创建知识库 if (StringUtils.hasText(provider)) {
DatasetCreateResponse difyResponse = difyApiClient.createDataset( difyRequest.setEmbeddingModelProvider(provider);
difyRequest, log.info("创建知识库 - 设置Embedding模型: model={}, provider={}", embeddingModel, provider);
difyConfig.getApiKey() }
);
// 设置检索模型配置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(); difyDatasetId = difyResponse.getId();
log.info("Dify知识库创建成功: {} - {}", difyDatasetId, knowledge.getTitle()); log.info("Dify知识库创建成功: {} - {}", difyDatasetId, knowledge.getTitle());
@@ -122,6 +166,12 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
knowledge.setDifyDatasetId(difyDatasetId); knowledge.setDifyDatasetId(difyDatasetId);
knowledge.setDifyIndexingTechnique(indexingTechnique); knowledge.setDifyIndexingTechnique(indexingTechnique);
knowledge.setEmbeddingModel(embeddingModel); knowledge.setEmbeddingModel(embeddingModel);
// 保存模型提供商
if (StringUtils.hasText(knowledge.getEmbeddingModelProvider())) {
knowledge.setEmbeddingModelProvider(knowledge.getEmbeddingModelProvider());
}
// 保存检索配置(从前端传入,已经在 knowledge 对象中)
// retrievalTopK, retrievalScoreThreshold, rerankModel, rerankModelProvider 已设置
knowledge.setCreator(currentUser.getID()); knowledge.setCreator(currentUser.getID());
knowledge.setCreatorDept(deptId); knowledge.setCreatorDept(deptId);
knowledge.setUpdater(currentUser.getID()); knowledge.setUpdater(currentUser.getID());
@@ -143,7 +193,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
if (rows <= 0) { if (rows <= 0) {
// 回滚删除Dify中的知识库 // 回滚删除Dify中的知识库
try { try {
difyApiClient.deleteDataset(difyDatasetId, difyConfig.getApiKey()); difyApiClient.deleteDataset(difyDatasetId);
} catch (Exception ex) { } catch (Exception ex) {
log.error("回滚删除Dify知识库失败", ex); log.error("回滚删除Dify知识库失败", ex);
} }
@@ -209,28 +259,54 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
return resultDomain; return resultDomain;
} }
// 5. 如果修改了title或description同步到Dify // 5. 检测字段变化同步到Dify
boolean needUpdateDify = false; boolean needUpdateDify = false;
DatasetUpdateRequest updateRequest = new DatasetUpdateRequest();
// 标题变化
if (StringUtils.hasText(knowledge.getTitle()) && !knowledge.getTitle().equals(existing.getTitle())) { if (StringUtils.hasText(knowledge.getTitle()) && !knowledge.getTitle().equals(existing.getTitle())) {
updateRequest.setName(knowledge.getTitle());
needUpdateDify = true; needUpdateDify = true;
} }
// 描述变化
if (knowledge.getDescription() != null && !knowledge.getDescription().equals(existing.getDescription())) { 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; needUpdateDify = true;
} }
// 同步到Dify
if (needUpdateDify && StringUtils.hasText(existing.getDifyDatasetId())) { if (needUpdateDify && StringUtils.hasText(existing.getDifyDatasetId())) {
try { try {
DatasetUpdateRequest updateRequest = new DatasetUpdateRequest(); difyApiClient.updateDataset(existing.getDifyDatasetId(), updateRequest);
// 只设置实际改变的字段 log.info("Dify知识库更新成功: datasetId={}, title={}",
if (StringUtils.hasText(knowledge.getTitle())) { existing.getDifyDatasetId(), 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());
} catch (DifyException e) { } catch (DifyException e) {
log.error("更新Dify知识库失败继续更新本地数据", e); log.error("更新Dify知识库失败继续更新本地数据", e);
// 不阻塞本地更新流程 // 不阻塞本地更新流程
@@ -294,7 +370,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
// 4. 删除Dify中的知识库 // 4. 删除Dify中的知识库
if (StringUtils.hasText(existing.getDifyDatasetId())) { if (StringUtils.hasText(existing.getDifyDatasetId())) {
try { try {
difyApiClient.deleteDataset(existing.getDifyDatasetId(), difyConfig.getApiKey()); difyApiClient.deleteDataset(existing.getDifyDatasetId());
log.info("Dify知识库删除成功: {}", existing.getDifyDatasetId()); log.info("Dify知识库删除成功: {}", existing.getDifyDatasetId());
} catch (DifyException e) { } catch (DifyException e) {
log.error("删除Dify知识库失败继续删除本地记录", e); log.error("删除Dify知识库失败继续删除本地记录", e);
@@ -372,7 +448,8 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
} }
@Override @Override
public PageDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam) { public ResultDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam) {
ResultDomain<TbAiKnowledge> resultDomain = new ResultDomain<>();
try { try {
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole(); List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
@@ -389,11 +466,13 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
resultPageParam.setTotalElements(total); resultPageParam.setTotalElements(total);
resultPageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize())); resultPageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
return new PageDomain<>(resultPageParam, knowledges); resultDomain.success("查询成功", knowledges);
return resultDomain;
} catch (Exception e) { } catch (Exception e) {
log.error("分页查询知识库列表异常", e); log.error("分页查询知识库列表异常", e);
return new PageDomain<>(); resultDomain.fail("查询失败: " + e.getMessage());
return resultDomain;
} }
} }
@@ -417,8 +496,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
// 2. 从Dify获取最新信息 // 2. 从Dify获取最新信息
try { try {
DatasetDetailResponse difyDetail = difyApiClient.getDatasetDetail( DatasetDetailResponse difyDetail = difyApiClient.getDatasetDetail(
knowledge.getDifyDatasetId(), knowledge.getDifyDatasetId()
difyConfig.getApiKey()
); );
// 3. 更新本地信息 // 3. 更新本地信息
@@ -561,5 +639,143 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
throw new AiKnowledgeException("创建权限失败: " + e.getMessage()); 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;
}
}
} }

View File

@@ -260,8 +260,7 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
knowledge.getDifyDatasetId(), knowledge.getDifyDatasetId(),
fileToUpload, fileToUpload,
originalFilename, originalFilename,
uploadRequest, uploadRequest);
difyConfig.getApiKey());
// 8. 保存到本地数据库 // 8. 保存到本地数据库
TbAiUploadFile uploadFile = new TbAiUploadFile(); TbAiUploadFile uploadFile = new TbAiUploadFile();
@@ -378,8 +377,7 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
try { try {
difyApiClient.deleteDocument( difyApiClient.deleteDocument(
knowledge.getDifyDatasetId(), knowledge.getDifyDatasetId(),
file.getDifyDocumentId(), file.getDifyDocumentId());
difyConfig.getApiKey());
log.info("Dify文档删除成功: {}", file.getDifyDocumentId()); log.info("Dify文档删除成功: {}", file.getDifyDocumentId());
} catch (DifyException e) { } catch (DifyException e) {
log.error("删除Dify文档失败继续删除本地记录", e); log.error("删除Dify文档失败继续删除本地记录", e);
@@ -512,8 +510,7 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
try { try {
DocumentStatusResponse statusResponse = difyApiClient.getDocumentStatus( DocumentStatusResponse statusResponse = difyApiClient.getDocumentStatus(
knowledge.getDifyDatasetId(), knowledge.getDifyDatasetId(),
file.getDifyBatchId(), file.getDifyBatchId());
difyConfig.getApiKey());
// 4. 更新本地状态 // 4. 更新本地状态
TbAiUploadFile update = new TbAiUploadFile(); TbAiUploadFile update = new TbAiUploadFile();

View File

@@ -6,6 +6,7 @@
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.ai.TbAiKnowledge"> <resultMap id="BaseResultMap" type="org.xyzh.common.dto.ai.TbAiKnowledge">
<id column="id" property="id" jdbcType="VARCHAR"/> <id column="id" property="id" jdbcType="VARCHAR"/>
<result column="title" property="title" 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="description" property="description" jdbcType="VARCHAR"/>
<result column="content" property="content" jdbcType="LONGVARCHAR"/> <result column="content" property="content" jdbcType="LONGVARCHAR"/>
<result column="source_type" property="sourceType" jdbcType="INTEGER"/> <result column="source_type" property="sourceType" jdbcType="INTEGER"/>
@@ -17,6 +18,11 @@
<result column="dify_dataset_id" property="difyDatasetId" jdbcType="VARCHAR"/> <result column="dify_dataset_id" property="difyDatasetId" jdbcType="VARCHAR"/>
<result column="dify_indexing_technique" property="difyIndexingTechnique" jdbcType="VARCHAR"/> <result column="dify_indexing_technique" property="difyIndexingTechnique" jdbcType="VARCHAR"/>
<result column="embedding_model" property="embeddingModel" 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="vector_id" property="vectorID" jdbcType="VARCHAR"/>
<result column="document_count" property="documentCount" jdbcType="INTEGER"/> <result column="document_count" property="documentCount" jdbcType="INTEGER"/>
<result column="total_chunks" property="totalChunks" jdbcType="INTEGER"/> <result column="total_chunks" property="totalChunks" jdbcType="INTEGER"/>
@@ -32,8 +38,9 @@
<!-- 基础字段 --> <!-- 基础字段 -->
<sql id="Base_Column_List"> <sql id="Base_Column_List">
id, title, description, content, source_type, source_id, file_name, file_path, id, title, avatar, description, content, source_type, source_id, file_name, file_path,
category, tags, dify_dataset_id, dify_indexing_technique, embedding_model, 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, vector_id, document_count, total_chunks, status, creator, creator_dept,
updater, create_time, update_time, delete_time, deleted updater, create_time, update_time, delete_time, deleted
</sql> </sql>
@@ -161,13 +168,15 @@
<!-- insertKnowledge插入知识库 --> <!-- insertKnowledge插入知识库 -->
<insert id="insertKnowledge" parameterType="org.xyzh.common.dto.ai.TbAiKnowledge"> <insert id="insertKnowledge" parameterType="org.xyzh.common.dto.ai.TbAiKnowledge">
INSERT INTO tb_ai_knowledge ( INSERT INTO tb_ai_knowledge (
id, title, description, content, source_type, source_id, file_name, file_path, id, title, avatar, description, content, source_type, source_id, file_name, file_path,
category, tags, dify_dataset_id, dify_indexing_technique, embedding_model, 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, vector_id, document_count, total_chunks, status, creator, creator_dept,
updater, create_time, update_time, deleted updater, create_time, update_time, deleted
) VALUES ( ) VALUES (
#{ID}, #{title}, #{description}, #{content}, #{sourceType}, #{sourceID}, #{fileName}, #{filePath}, #{ID}, #{title}, #{avatar}, #{description}, #{content}, #{sourceType}, #{sourceID}, #{fileName}, #{filePath},
#{category}, #{tags}, #{difyDatasetId}, #{difyIndexingTechnique}, #{embeddingModel}, #{category}, #{tags}, #{difyDatasetId}, #{difyIndexingTechnique}, #{embeddingModel}, #{embeddingModelProvider},
#{rerankModel}, #{rerankModelProvider}, #{retrievalTopK}, #{retrievalScoreThreshold},
#{vectorID}, #{documentCount}, #{totalChunks}, #{status}, #{creator}, #{creatorDept}, #{vectorID}, #{documentCount}, #{totalChunks}, #{status}, #{creator}, #{creatorDept},
#{updater}, #{createTime}, #{updateTime}, #{deleted} #{updater}, #{createTime}, #{updateTime}, #{deleted}
) )
@@ -178,6 +187,7 @@
UPDATE tb_ai_knowledge UPDATE tb_ai_knowledge
<set> <set>
<if test="title != null and title != ''">title = #{title},</if> <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="description != null">description = #{description},</if>
<if test="content != null">content = #{content},</if> <if test="content != null">content = #{content},</if>
<if test="sourceType != null">source_type = #{sourceType},</if> <if test="sourceType != null">source_type = #{sourceType},</if>
@@ -189,6 +199,11 @@
<if test="difyDatasetId != null">dify_dataset_id = #{difyDatasetId},</if> <if test="difyDatasetId != null">dify_dataset_id = #{difyDatasetId},</if>
<if test="difyIndexingTechnique != null">dify_indexing_technique = #{difyIndexingTechnique},</if> <if test="difyIndexingTechnique != null">dify_indexing_technique = #{difyIndexingTechnique},</if>
<if test="embeddingModel != null">embedding_model = #{embeddingModel},</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="vectorID != null">vector_id = #{vectorID},</if>
<if test="documentCount != null">document_count = #{documentCount},</if> <if test="documentCount != null">document_count = #{documentCount},</if>
<if test="totalChunks != null">total_chunks = #{totalChunks},</if> <if test="totalChunks != null">total_chunks = #{totalChunks},</if>

View File

@@ -6,6 +6,7 @@ import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.ai.TbAiKnowledge; import org.xyzh.common.dto.ai.TbAiKnowledge;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @description AI知识库管理服务接口 * @description AI知识库管理服务接口
@@ -65,7 +66,7 @@ public interface AiKnowledgeService {
* @param pageParam 分页参数 * @param pageParam 分页参数
* @return 分页结果 * @return 分页结果
*/ */
PageDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam); ResultDomain<TbAiKnowledge> pageKnowledges(TbAiKnowledge filter, PageParam pageParam);
/** /**
* 同步Dify知识库信息到本地 * 同步Dify知识库信息到本地
@@ -103,4 +104,16 @@ public interface AiKnowledgeService {
* @return 统计信息 * @return 统计信息
*/ */
ResultDomain<TbAiKnowledge> getKnowledgeStats(String knowledgeId); ResultDomain<TbAiKnowledge> getKnowledgeStats(String knowledgeId);
/**
* 获取可用的嵌入模型列表
* @return 嵌入模型列表
*/
ResultDomain<Map<String, Object>> getAvailableEmbeddingModels();
/**
* 获取可用的Rerank模型列表
* @return Rerank模型列表
*/
ResultDomain<Map<String, Object>> getAvailableRerankModels();
} }

View File

@@ -18,6 +18,11 @@ public class TbAiKnowledge extends BaseDTO {
*/ */
private String title; private String title;
/**
* @description 知识库头像
*/
private String avatar;
/** /**
* @description 知识库描述 * @description 知识库描述
*/ */
@@ -73,6 +78,31 @@ public class TbAiKnowledge extends BaseDTO {
*/ */
private String embeddingModel; 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用于向量检索 * @description 向量ID用于向量检索
*/ */
@@ -116,6 +146,15 @@ public class TbAiKnowledge extends BaseDTO {
this.title = title; this.title = title;
} }
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public String getDescription() { public String getDescription() {
return description; return description;
} }
@@ -260,6 +299,45 @@ public class TbAiKnowledge extends BaseDTO {
this.updater = updater; 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 @Override
public String toString() { public String toString() {
return "TbAiKnowledge{" + return "TbAiKnowledge{" +

View File

@@ -94,16 +94,6 @@ export const knowledgeApi = {
return response.data; 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 权限参数 * @param params 权限参数
@@ -143,6 +133,15 @@ export const knowledgeApi = {
async getStats(knowledgeId: string): Promise<ResultDomain<any>> { async getStats(knowledgeId: string): Promise<ResultDomain<any>> {
const response = await api.get<any>(`/ai/knowledge/${knowledgeId}/stats`); const response = await api.get<any>(`/ai/knowledge/${knowledgeId}/stats`);
return response.data; 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;
} }
}; };

View File

@@ -47,26 +47,48 @@ export interface AiAgentConfig extends BaseDTO {
* AI知识库实体 * AI知识库实体
*/ */
export interface AiKnowledge extends BaseDTO { export interface AiKnowledge extends BaseDTO {
/** 知识库名称 */ /** 知识库标题 */
name?: string; title?: string;
/** 知识库头像 */
avatar?: string;
/** 知识库描述 */ /** 知识库描述 */
description?: string; description?: string;
/** 索引方式high_quality=高质量, economy=经济) */ /** 内容 */
indexingTechnique?: string; content?: string;
/** Embedding模型 */ /** 来源类型 */
embeddingModel?: string; sourceType?: number;
/** 来源ID */
sourceID?: string;
/** 文件名 */
fileName?: string;
/** 文件路径 */
filePath?: string;
/** 分类 */
category?: string;
/** 标签 */
tags?: string;
/** Dify数据集ID */ /** Dify数据集ID */
difyDatasetId?: string; difyDatasetId?: string;
/** 同步状态0未同步 1已同步 2同步失败 */ /** Dify索引方式high_quality=高质量, economy=经济 */
syncStatus?: number; difyIndexingTechnique?: string;
/** Embedding模型 */
embeddingModel?: string;
/** Embedding模型提供商 */
embeddingModelProvider?: string;
/** 向量ID */
vectorID?: string;
/** 文档数量 */ /** 文档数量 */
documentCount?: number; documentCount?: number;
/** 字符数 */ /** 总分段数 */
characterCount?: number; totalChunks?: number;
/** 创建者部门 */
creatorDept?: string;
/** 状态0禁用 1启用 */ /** 状态0禁用 1启用 */
status?: number; status?: number;
/** 创建者 */
creator?: string;
/** 创建者部门 */
creatorDept?: string;
/** 更新者 */
updater?: string;
} }
/** /**

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';

View File

@@ -679,6 +679,19 @@ async function sendMessage() {
// 调用API // 调用API
isGenerating.value = true; 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 { try {
let aiMessageContent = ''; let aiMessageContent = '';
@@ -710,6 +723,12 @@ async function sendMessage() {
// 保存AI消息的数据库IDtask_id用于停止生成 // 保存AI消息的数据库IDtask_id用于停止生成
currentMessageId.value = initData.messageId; currentMessageId.value = initData.messageId;
console.log('[保存MessageID(TaskID)]', 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) => { onMessage: (chunk: string) => {
// 确保AI消息已创建即使内容为空 // 确保AI消息已创建即使内容为空
@@ -900,6 +919,11 @@ async function regenerateMessage(messageId: string) {
// 保存AI消息的数据库IDtask_id用于停止生成 // 保存AI消息的数据库IDtask_id用于停止生成
currentMessageId.value = initData.messageId; currentMessageId.value = initData.messageId;
console.log('[保存MessageID(TaskID)-重新生成]', initData.messageId); console.log('[保存MessageID(TaskID)-重新生成]', initData.messageId);
// 如果后端返回了新的messageId更新消息对象的ID
if (initData.messageId !== messageId) {
messages.value[messageIndex].id = initData.messageId;
}
}, },
onMessage: (chunk: string) => { onMessage: (chunk: string) => {
// 累加内容包括空chunk因为后端可能分块发送 // 累加内容包括空chunk因为后端可能分块发送
@@ -907,10 +931,9 @@ async function regenerateMessage(messageId: string) {
aiMessageContent += chunk; aiMessageContent += chunk;
} }
// 找到对应消息并更新 // 直接使用messageIndex更新消息内容
const msgIndex = messages.value.findIndex(m => m.id === messageId); if (messageIndex !== -1) {
if (msgIndex !== -1) { messages.value[messageIndex].content = aiMessageContent;
messages.value[msgIndex].content = aiMessageContent;
} }
nextTick(() => scrollToBottom()); nextTick(() => scrollToBottom());