diff --git a/schoolNewsServ/.bin/mysql/sql/add_embedding_model_provider.sql b/schoolNewsServ/.bin/mysql/sql/add_embedding_model_provider.sql new file mode 100644 index 0000000..2f96330 --- /dev/null +++ b/schoolNewsServ/.bin/mysql/sql/add_embedding_model_provider.sql @@ -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; + diff --git a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql index 5b70c78..71e1d9e 100644 --- a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql +++ b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql @@ -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` diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java index 8937877..439ccf2 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java @@ -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); + } + } } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java index b42b89a..a186190 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java @@ -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(团队所有成员) */ diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java index ba15a3e..cd63953 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java @@ -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; } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/EmbeddingModelResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/EmbeddingModelResponse.java new file mode 100644 index 0000000..0b1ebda --- /dev/null +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/EmbeddingModelResponse.java @@ -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 data; + + /** + * 模型提供商 + */ + @Data + public static class ModelProvider { + /** + * 提供商标识 + */ + @JSONField(name = "provider") + private String provider; + + /** + * 提供商标签 + */ + @JSONField(name = "label") + private Map label; + + /** + * 小图标 + */ + @JSONField(name = "icon_small") + private Map iconSmall; + + /** + * 大图标 + */ + @JSONField(name = "icon_large") + private Map iconLarge; + + /** + * 状态 + */ + @JSONField(name = "status") + private String status; + + /** + * 模型列表 + */ + @JSONField(name = "models") + private List models; + } + + /** + * 模型详情 + */ + @Data + public static class Model { + /** + * 模型名称 + */ + @JSONField(name = "model") + private String model; + + /** + * 模型标签 + */ + @JSONField(name = "label") + private Map label; + + /** + * 模型类型 + */ + @JSONField(name = "model_type") + private String modelType; + + /** + * 特性列表 + */ + @JSONField(name = "features") + private List 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; + } +} + diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RerankModelResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RerankModelResponse.java new file mode 100644 index 0000000..d1124bc --- /dev/null +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RerankModelResponse.java @@ -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 data; + + @Data + public static class ModelProvider { + private String provider; + private Map 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 models; + } + + @Data + public static class Model { + private String model; // e.g., "rerank-multilingual-v3.0" + private Map label; + @JSONField(name = "model_type") + private String modelType; // e.g., "rerank" + private List 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; + } +} + diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalModel.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalModel.java new file mode 100644 index 0000000..987912b --- /dev/null +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalModel.java @@ -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; +} + diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java index 9bacf91..b66520e 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java @@ -33,6 +33,8 @@ public class DifyConfig { // private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f"; private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x"; + private String knowledgeApiKey="dataset-HeDK9gHBqPnI4rBZ2q2Hm7rV"; + /** * 请求超时时间(秒) */ diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java index e7aa074..e2f9e27 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java @@ -30,21 +30,19 @@ public class AiKnowledgeController { /** * @description 创建知识库 - * @param requestBody 请求体(knowledge, permissionType, deptIds, roleIds) + * @param knowledge 知识库对象 * @return ResultDomain * @author AI Assistant * @since 2025-11-04 */ @PostMapping - public ResultDomain createKnowledge(@RequestBody Map requestBody) { - TbAiKnowledge knowledge = (TbAiKnowledge) requestBody.get("knowledge"); - String permissionType = (String) requestBody.get("permissionType"); - @SuppressWarnings("unchecked") - List deptIds = (List) requestBody.get("deptIds"); - @SuppressWarnings("unchecked") - List roleIds = (List) requestBody.get("roleIds"); + public ResultDomain createKnowledge(@RequestBody TbAiKnowledge knowledge) { + // 默认权限为PUBLIC(公开) + String permissionType = "PUBLIC"; + List deptIds = null; + List 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 pageKnowledges(@RequestBody PageRequest pageRequest) { + public ResultDomain pageKnowledges(@RequestBody PageRequest 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 + * @author AI Assistant + * @since 2025-11-06 + */ + @GetMapping("/embedding-models") + public ResultDomain> getAvailableEmbeddingModels() { + log.info("获取可用的嵌入模型列表"); + return knowledgeService.getAvailableEmbeddingModels(); + } + + /** + * @description 获取可用的Rerank模型列表 + * @return ResultDomain + * @author AI Assistant + * @since 2025-11-06 + */ + @GetMapping("/rerank-models") + public ResultDomain> getAvailableRerankModels() { + log.info("获取可用的Rerank模型列表"); + return knowledgeService.getAvailableRerankModels(); + } } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java index bfb1861..0a0b474 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java @@ -10,6 +10,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; /** @@ -101,12 +107,50 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { embeddingModel = difyConfig.getDataset().getDefaultEmbeddingModel(); } difyRequest.setEmbeddingModel(embeddingModel); + + // 设置模型提供商(从前端传入或使用配置默认值) + String provider = knowledge.getEmbeddingModelProvider(); - // 调用Dify API创建知识库 - DatasetCreateResponse difyResponse = difyApiClient.createDataset( - difyRequest, - difyConfig.getApiKey() - ); + 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())) { + 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 pageKnowledges(TbAiKnowledge filter, PageParam pageParam) { + public ResultDomain pageKnowledges(TbAiKnowledge filter, PageParam pageParam) { + ResultDomain resultDomain = new ResultDomain<>(); try { List 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> getAvailableEmbeddingModels() { + ResultDomain> resultDomain = new ResultDomain<>(); + + try { + // 调用Dify API获取嵌入模型列表 + EmbeddingModelResponse response = + difyApiClient.getAvailableEmbeddingModels(); + + if (response == null || response.getData() == null) { + resultDomain.fail("获取嵌入模型列表失败"); + return resultDomain; + } + + // 转换为前端需要的格式 + Map result = new HashMap<>(); + result.put("providers", response.getData()); + + // 提取所有可用的模型列表(扁平化) + List> 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 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> getAvailableRerankModels() { + ResultDomain> resultDomain = new ResultDomain<>(); + + try { + // 调用Dify API获取Rerank模型列表 + RerankModelResponse response = + difyApiClient.getAvailableRerankModels(); + + if (response == null || response.getData() == null) { + resultDomain.fail("获取Rerank模型列表失败"); + return resultDomain; + } + + // 转换为前端需要的格式 + Map result = new HashMap<>(); + result.put("providers", response.getData()); + + // 提取所有可用的模型列表(扁平化) + List> 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 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; + } + } + } diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java index ab57b4c..447334a 100644 --- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java +++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java @@ -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(); diff --git a/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml b/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml index 9cf56e4..881a736 100644 --- a/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml +++ b/schoolNewsServ/ai/src/main/resources/mapper/AiKnowledgeMapper.xml @@ -6,6 +6,7 @@ + @@ -17,6 +18,11 @@ + + + + + @@ -32,8 +38,9 @@ - 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 @@ -161,13 +168,15 @@ 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 title = #{title}, + avatar = #{avatar}, description = #{description}, content = #{content}, source_type = #{sourceType}, @@ -189,6 +199,11 @@ dify_dataset_id = #{difyDatasetId}, dify_indexing_technique = #{difyIndexingTechnique}, embedding_model = #{embeddingModel}, + embedding_model_provider = #{embeddingModelProvider}, + rerank_model = #{rerankModel}, + rerank_model_provider = #{rerankModelProvider}, + retrieval_top_k = #{retrievalTopK}, + retrieval_score_threshold = #{retrievalScoreThreshold}, vector_id = #{vectorID}, document_count = #{documentCount}, total_chunks = #{totalChunks}, diff --git a/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java b/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java index 904575b..27cd294 100644 --- a/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java +++ b/schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/knowledge/AiKnowledgeService.java @@ -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 pageKnowledges(TbAiKnowledge filter, PageParam pageParam); + ResultDomain pageKnowledges(TbAiKnowledge filter, PageParam pageParam); /** * 同步Dify知识库信息到本地 @@ -103,4 +104,16 @@ public interface AiKnowledgeService { * @return 统计信息 */ ResultDomain getKnowledgeStats(String knowledgeId); + + /** + * 获取可用的嵌入模型列表 + * @return 嵌入模型列表 + */ + ResultDomain> getAvailableEmbeddingModels(); + + /** + * 获取可用的Rerank模型列表 + * @return Rerank模型列表 + */ + ResultDomain> getAvailableRerankModels(); } diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java index b7047a1..88abb23 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/ai/TbAiKnowledge.java @@ -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{" + diff --git a/schoolNewsWeb/src/apis/ai/knowledge.ts b/schoolNewsWeb/src/apis/ai/knowledge.ts index a7ca175..e648bea 100644 --- a/schoolNewsWeb/src/apis/ai/knowledge.ts +++ b/schoolNewsWeb/src/apis/ai/knowledge.ts @@ -94,16 +94,6 @@ export const knowledgeApi = { return response.data; }, - /** - * 从Dify同步知识库状态 - * @param knowledgeId 知识库ID - * @returns Promise> - */ - async syncFromDify(knowledgeId: string): Promise> { - const response = await api.get(`/ai/knowledge/${knowledgeId}/sync`); - return response.data; - }, - /** * 设置知识库权限 * @param params 权限参数 @@ -143,6 +133,15 @@ export const knowledgeApi = { async getStats(knowledgeId: string): Promise> { const response = await api.get(`/ai/knowledge/${knowledgeId}/stats`); return response.data; + }, + + /** + * 获取可用的嵌入模型列表 + * @returns Promise> + */ + async getAvailableEmbeddingModels(): Promise> { + const response = await api.get('/ai/knowledge/embedding-models'); + return response.data; } }; diff --git a/schoolNewsWeb/src/types/ai/index.ts b/schoolNewsWeb/src/types/ai/index.ts index cd221ce..5d096ef 100644 --- a/schoolNewsWeb/src/types/ai/index.ts +++ b/schoolNewsWeb/src/types/ai/index.ts @@ -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; } /** diff --git a/schoolNewsWeb/src/views/admin/manage/ai/KnowledgeManagementView.vue b/schoolNewsWeb/src/views/admin/manage/ai/KnowledgeManagementView.vue index 03951aa..5777a19 100644 --- a/schoolNewsWeb/src/views/admin/manage/ai/KnowledgeManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/ai/KnowledgeManagementView.vue @@ -1,1003 +1,322 @@ - + \ No newline at end of file diff --git a/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue new file mode 100644 index 0000000..155c242 --- /dev/null +++ b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeBasic.vue @@ -0,0 +1,556 @@ + + + + + diff --git a/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeCard.vue b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeCard.vue new file mode 100644 index 0000000..fe342a7 --- /dev/null +++ b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeCard.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeInfo.vue b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeInfo.vue new file mode 100644 index 0000000..8afe61d --- /dev/null +++ b/schoolNewsWeb/src/views/admin/manage/ai/components/KnowledgeInfo.vue @@ -0,0 +1,639 @@ + + + + + diff --git a/schoolNewsWeb/src/views/admin/manage/ai/components/index.ts b/schoolNewsWeb/src/views/admin/manage/ai/components/index.ts new file mode 100644 index 0000000..7380c45 --- /dev/null +++ b/schoolNewsWeb/src/views/admin/manage/ai/components/index.ts @@ -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'; \ No newline at end of file diff --git a/schoolNewsWeb/src/views/public/ai/AIAgent.vue b/schoolNewsWeb/src/views/public/ai/AIAgent.vue index ad547f5..6ed1694 100644 --- a/schoolNewsWeb/src/views/public/ai/AIAgent.vue +++ b/schoolNewsWeb/src/views/public/ai/AIAgent.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());