知识库创建
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
-- ========================================
|
||||||
|
-- 添加 embedding_model_provider 字段
|
||||||
|
-- ========================================
|
||||||
|
-- 用途:在 tb_ai_knowledge 表中添加向量模型提供商字段
|
||||||
|
-- 执行时间:2025-11-06
|
||||||
|
-- 注意:如果该字段已存在,请忽略此脚本
|
||||||
|
-- ========================================
|
||||||
|
|
||||||
|
USE `school_news`;
|
||||||
|
|
||||||
|
-- 检查并添加 embedding_model_provider 字段
|
||||||
|
ALTER TABLE `tb_ai_knowledge`
|
||||||
|
ADD COLUMN `embedding_model_provider` VARCHAR(100) DEFAULT NULL COMMENT '向量模型提供商'
|
||||||
|
AFTER `embedding_model`;
|
||||||
|
|
||||||
|
-- 完成
|
||||||
|
SELECT 'embedding_model_provider 字段添加成功!' AS message;
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ DROP TABLE IF EXISTS `tb_ai_knowledge`;
|
|||||||
CREATE TABLE `tb_ai_knowledge` (
|
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知识库ID(Dataset ID)',
|
`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)',
|
`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`
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(团队所有成员)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package org.xyzh.ai.client.dto;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.annotation.JSONField;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Dify嵌入模型响应
|
||||||
|
* @filename EmbeddingModelResponse.java
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2025-11-06
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class EmbeddingModelResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型提供商列表
|
||||||
|
*/
|
||||||
|
@JSONField(name = "data")
|
||||||
|
private List<ModelProvider> data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型提供商
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class ModelProvider {
|
||||||
|
/**
|
||||||
|
* 提供商标识
|
||||||
|
*/
|
||||||
|
@JSONField(name = "provider")
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供商标签
|
||||||
|
*/
|
||||||
|
@JSONField(name = "label")
|
||||||
|
private Map<String, String> label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小图标
|
||||||
|
*/
|
||||||
|
@JSONField(name = "icon_small")
|
||||||
|
private Map<String, String> iconSmall;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 大图标
|
||||||
|
*/
|
||||||
|
@JSONField(name = "icon_large")
|
||||||
|
private Map<String, String> iconLarge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@JSONField(name = "status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型列表
|
||||||
|
*/
|
||||||
|
@JSONField(name = "models")
|
||||||
|
private List<Model> models;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型详情
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class Model {
|
||||||
|
/**
|
||||||
|
* 模型名称
|
||||||
|
*/
|
||||||
|
@JSONField(name = "model")
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型标签
|
||||||
|
*/
|
||||||
|
@JSONField(name = "label")
|
||||||
|
private Map<String, String> label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型类型
|
||||||
|
*/
|
||||||
|
@JSONField(name = "model_type")
|
||||||
|
private String modelType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 特性列表
|
||||||
|
*/
|
||||||
|
@JSONField(name = "features")
|
||||||
|
private List<Object> features;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取来源
|
||||||
|
*/
|
||||||
|
@JSONField(name = "fetch_from")
|
||||||
|
private String fetchFrom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型属性
|
||||||
|
*/
|
||||||
|
@JSONField(name = "model_properties")
|
||||||
|
private ModelProperties modelProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已弃用
|
||||||
|
*/
|
||||||
|
@JSONField(name = "deprecated")
|
||||||
|
private Boolean deprecated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@JSONField(name = "status")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用负载均衡
|
||||||
|
*/
|
||||||
|
@JSONField(name = "load_balancing_enabled")
|
||||||
|
private Boolean loadBalancingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型属性
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class ModelProperties {
|
||||||
|
/**
|
||||||
|
* 上下文大小
|
||||||
|
*/
|
||||||
|
@JSONField(name = "context_size")
|
||||||
|
private Integer contextSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大分块数
|
||||||
|
*/
|
||||||
|
@JSONField(name = "max_chunks")
|
||||||
|
private Integer maxChunks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.xyzh.ai.client.dto;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.annotation.JSONField;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Dify Rerank模型响应
|
||||||
|
* @filename RerankModelResponse.java
|
||||||
|
* @author AI Assistant
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-06
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RerankModelResponse {
|
||||||
|
private List<ModelProvider> data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ModelProvider {
|
||||||
|
private String provider;
|
||||||
|
private Map<String, String> label; // e.g., {"en_US": "Cohere", "zh_Hans": "Cohere"}
|
||||||
|
@JSONField(name = "icon_small")
|
||||||
|
private String iconSmall;
|
||||||
|
@JSONField(name = "icon_large")
|
||||||
|
private String iconLarge;
|
||||||
|
private String status; // e.g., "active"
|
||||||
|
private List<Model> models;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Model {
|
||||||
|
private String model; // e.g., "rerank-multilingual-v3.0"
|
||||||
|
private Map<String, String> label;
|
||||||
|
@JSONField(name = "model_type")
|
||||||
|
private String modelType; // e.g., "rerank"
|
||||||
|
private List<String> features;
|
||||||
|
@JSONField(name = "fetch_from")
|
||||||
|
private String fetchFrom;
|
||||||
|
@JSONField(name = "model_properties")
|
||||||
|
private ModelProperties modelProperties;
|
||||||
|
private Boolean deprecated;
|
||||||
|
private String status; // e.g., "active"
|
||||||
|
private String provider; // 模型提供商(可能在 model 数据中)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ModelProperties {
|
||||||
|
@JSONField(name = "context_size")
|
||||||
|
private Integer contextSize;
|
||||||
|
@JSONField(name = "max_chunks")
|
||||||
|
private Integer maxChunks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package org.xyzh.ai.client.dto;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.annotation.JSONField;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Dify检索模型配置(Retrieval Model)
|
||||||
|
* @filename RetrievalModel.java
|
||||||
|
* @author AI Assistant
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-06
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RetrievalModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索方法:vector_search(向量搜索)、full_text_search(全文搜索)、hybrid_search(混合搜索)
|
||||||
|
*/
|
||||||
|
@JSONField(name = "search_method")
|
||||||
|
private String searchMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rerank模型提供商
|
||||||
|
*/
|
||||||
|
@JSONField(name = "reranking_provider_name")
|
||||||
|
private String rerankingProviderName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rerank模型名称
|
||||||
|
*/
|
||||||
|
@JSONField(name = "reranking_model")
|
||||||
|
private String rerankingModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rerank是否启用
|
||||||
|
*/
|
||||||
|
@JSONField(name = "reranking_enable")
|
||||||
|
private Boolean rerankingEnable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top K(返回前K个结果)
|
||||||
|
*/
|
||||||
|
@JSONField(name = "top_k")
|
||||||
|
private Integer topK;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分数阈值(0.00-1.00)
|
||||||
|
*/
|
||||||
|
@JSONField(name = "score_threshold")
|
||||||
|
private Double scoreThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用分数阈值
|
||||||
|
*/
|
||||||
|
@JSONField(name = "score_threshold_enabled")
|
||||||
|
private Boolean scoreThresholdEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -33,6 +33,8 @@ public class DifyConfig {
|
|||||||
// private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f";
|
// private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f";
|
||||||
private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x";
|
private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x";
|
||||||
|
|
||||||
|
private String knowledgeApiKey="dataset-HeDK9gHBqPnI4rBZ2q2Hm7rV";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求超时时间(秒)
|
* 请求超时时间(秒)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{" +
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,556 @@
|
|||||||
|
<template>
|
||||||
|
<div class="knowledge-basic-container">
|
||||||
|
<!-- 查看模式 -->
|
||||||
|
<div v-if="type === 'view'" class="view-mode">
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="avatar-section">
|
||||||
|
<img v-if="knowledge?.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
|
||||||
|
<div v-else class="default-avatar">📚</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-details">
|
||||||
|
<h2 class="knowledge-name">{{ knowledge?.title || '未命名' }}</h2>
|
||||||
|
<p class="knowledge-description">{{ knowledge?.description || '暂无描述' }}</p>
|
||||||
|
<div class="meta-info">
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">状态:</span>
|
||||||
|
<el-tag :type="knowledge?.status === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ knowledge?.status === 1 ? '已启用' : '已禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item" v-if="knowledge?.difyDatasetId">
|
||||||
|
<span class="label">Dify数据集ID:</span>
|
||||||
|
<span class="value">{{ knowledge.difyDatasetId }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">索引方式:</span>
|
||||||
|
<span class="value">{{ getIndexingText(knowledge?.difyIndexingTechnique) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item" v-if="knowledge?.difyIndexingTechnique == 'high_quality'">
|
||||||
|
<span class="label">Embedding模型:</span>
|
||||||
|
<span class="value">{{ knowledge?.embeddingModel || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">文档数量:</span>
|
||||||
|
<span class="value">{{ knowledge?.documentCount || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<span class="label">创建时间:</span>
|
||||||
|
<span class="value">{{ formatDate(knowledge?.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑模式 -->
|
||||||
|
<div v-else class="edit-mode">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
class="knowledge-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="知识库标题" prop="title" required>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.title"
|
||||||
|
placeholder="请输入知识库标题"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
:disabled="props.type === 'view'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="知识库头像" prop="avatar">
|
||||||
|
<FileUpload
|
||||||
|
:cover-url="formData.avatar"
|
||||||
|
@update:cover-url="handleAvatarUpdate"
|
||||||
|
:as-dialog="false"
|
||||||
|
list-type="cover"
|
||||||
|
accept="image/*"
|
||||||
|
:max-size="2"
|
||||||
|
module="ai-knowledge"
|
||||||
|
tip="点击上传知识库头像"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="知识库描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入知识库描述,介绍知识库的内容、用途等..."
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="索引方式" prop="difyIndexingTechnique" required>
|
||||||
|
<el-radio-group v-model="formData.difyIndexingTechnique">
|
||||||
|
<el-radio value="high_quality">
|
||||||
|
<span class="radio-label">高质量</span>
|
||||||
|
<span class="radio-desc">更精确的检索结果,消耗更多tokens</span>
|
||||||
|
</el-radio>
|
||||||
|
<el-radio
|
||||||
|
value="economy"
|
||||||
|
:disabled="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'"
|
||||||
|
>
|
||||||
|
<span class="radio-label">经济</span>
|
||||||
|
<span class="radio-desc">快速检索,消耗较少tokens</span>
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
<div v-if="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'" class="form-tip">
|
||||||
|
高质量模式不能降级为经济模式
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="formData.difyIndexingTechnique == 'high_quality'" label="Embedding模型" prop="embeddingModel">
|
||||||
|
<el-select
|
||||||
|
v-model="formData.embeddingModel"
|
||||||
|
placeholder="请选择Embedding模型(可选)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
:loading="modelsLoading"
|
||||||
|
@change="handleModelChange"
|
||||||
|
>
|
||||||
|
<el-option-group
|
||||||
|
v-for="provider in embeddingModels"
|
||||||
|
:key="provider.provider"
|
||||||
|
:label="provider.label || provider.provider"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="model in provider.models"
|
||||||
|
:key="model.model"
|
||||||
|
:label="getModelLabel(model)"
|
||||||
|
:value="model.model"
|
||||||
|
:data-provider="model.provider"
|
||||||
|
>
|
||||||
|
<span>{{ getModelLabel(model) }}</span>
|
||||||
|
<span v-if="model.contextSize" style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
|
||||||
|
上下文: {{ model.contextSize }}
|
||||||
|
</span>
|
||||||
|
</el-option>
|
||||||
|
</el-option-group>
|
||||||
|
</el-select>
|
||||||
|
<div v-if="type === 'edit'" class="form-tip form-tip-success">
|
||||||
|
可以切换Embedding模型以优化检索效果
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="Dify数据集ID" prop="difyDatasetId">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.difyDatasetId"
|
||||||
|
placeholder="可选,如需关联现有Dify数据集请填写"
|
||||||
|
:disabled="type === 'edit'"
|
||||||
|
/>
|
||||||
|
<div v-if="type === 'edit'" class="form-tip">
|
||||||
|
Dify数据集ID创建后不可修改
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-switch
|
||||||
|
v-model="formData.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="禁用"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||||
|
{{ type === 'add' ? '创建知识库' : '保存修改' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleCancel">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue';
|
||||||
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||||
|
import { FileUpload } from '@/components/file';
|
||||||
|
import type { AiKnowledge } from '@/types/ai';
|
||||||
|
import { knowledgeApi } from '@/apis/ai';
|
||||||
|
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'KnowledgeBasic'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface KnowledgeBasicProps {
|
||||||
|
type: 'view' | 'add' | 'edit';
|
||||||
|
knowledge?: AiKnowledge | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<KnowledgeBasicProps>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [knowledge: AiKnowledge];
|
||||||
|
cancel: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 表单引用
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
const submitting = ref(false);
|
||||||
|
const modelsLoading = ref(false);
|
||||||
|
const embeddingModels = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive<Partial<AiKnowledge>>({
|
||||||
|
title: '',
|
||||||
|
avatar: '',
|
||||||
|
description: '',
|
||||||
|
difyIndexingTechnique: 'high_quality',
|
||||||
|
embeddingModel: '',
|
||||||
|
embeddingModelProvider: '',
|
||||||
|
difyDatasetId: '',
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const rules: FormRules = {
|
||||||
|
title: [
|
||||||
|
{ required: true, message: '请输入知识库标题', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 100, message: '标题长度在2-100个字符之间', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
difyIndexingTechnique: [
|
||||||
|
{ required: true, message: '请选择索引方式', trigger: 'change' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听知识库数据变化
|
||||||
|
watch(() => props.knowledge, (newVal) => {
|
||||||
|
if (newVal && (props.type === 'view' || props.type === 'edit')) {
|
||||||
|
Object.assign(formData, {
|
||||||
|
id: newVal.id,
|
||||||
|
title: newVal.title,
|
||||||
|
avatar: newVal.avatar,
|
||||||
|
description: newVal.description,
|
||||||
|
difyIndexingTechnique: newVal.difyIndexingTechnique || 'high_quality',
|
||||||
|
embeddingModel: newVal.embeddingModel,
|
||||||
|
embeddingModelProvider: newVal.embeddingModelProvider,
|
||||||
|
difyDatasetId: newVal.difyDatasetId,
|
||||||
|
status: newVal.status ?? 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 处理头像更新
|
||||||
|
function handleAvatarUpdate(val: string) {
|
||||||
|
formData.avatar = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载嵌入模型列表
|
||||||
|
async function loadEmbeddingModels() {
|
||||||
|
try {
|
||||||
|
modelsLoading.value = true;
|
||||||
|
const result = await knowledgeApi.getAvailableEmbeddingModels();
|
||||||
|
if (result.success && result.data) {
|
||||||
|
// 按提供商分组
|
||||||
|
const providers = result.data.providers || [];
|
||||||
|
embeddingModels.value = providers.map((provider: any) => ({
|
||||||
|
provider: provider.provider,
|
||||||
|
label: provider.label?.zh_Hans || provider.label?.en_US || provider.provider,
|
||||||
|
models: (provider.models || []).map((model: any) => ({
|
||||||
|
model: model.model,
|
||||||
|
provider: model.provider || provider.provider, // 添加 provider 到 model 对象
|
||||||
|
label: model.label?.zh_Hans || model.label?.en_US || model.model,
|
||||||
|
contextSize: model.model_properties?.context_size,
|
||||||
|
status: model.status
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('获取嵌入模型列表失败,使用默认选项');
|
||||||
|
// 设置默认模型列表
|
||||||
|
embeddingModels.value = [{
|
||||||
|
provider: 'openai',
|
||||||
|
label: 'OpenAI',
|
||||||
|
models: [
|
||||||
|
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
|
||||||
|
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
|
||||||
|
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
|
||||||
|
]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载嵌入模型列表失败:', error);
|
||||||
|
// 设置默认模型列表
|
||||||
|
embeddingModels.value = [{
|
||||||
|
provider: 'openai',
|
||||||
|
label: 'OpenAI',
|
||||||
|
models: [
|
||||||
|
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
|
||||||
|
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
|
||||||
|
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
|
||||||
|
]
|
||||||
|
}];
|
||||||
|
} finally {
|
||||||
|
modelsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模型显示标签
|
||||||
|
function getModelLabel(model: any): string {
|
||||||
|
return model.label || model.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理模型变化
|
||||||
|
function handleModelChange(modelName: string) {
|
||||||
|
if (!modelName) {
|
||||||
|
formData.embeddingModelProvider = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找选中模型的提供商
|
||||||
|
for (const providerGroup of embeddingModels.value) {
|
||||||
|
const foundModel = providerGroup.models.find((m: any) => m.model === modelName);
|
||||||
|
if (foundModel) {
|
||||||
|
formData.embeddingModelProvider = foundModel.provider;
|
||||||
|
console.log('选择模型:', modelName, '提供商:', foundModel.provider);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载模型列表
|
||||||
|
loadEmbeddingModels();
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const valid = await formRef.value.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
|
||||||
|
if (props.type === 'add') {
|
||||||
|
// 创建知识库
|
||||||
|
const result = await knowledgeApi.createKnowledge(formData as AiKnowledge);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
ElMessage.success('创建成功');
|
||||||
|
emit('success', result.data);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '创建失败');
|
||||||
|
}
|
||||||
|
} else if (props.type === 'edit') {
|
||||||
|
// 更新知识库
|
||||||
|
const result = await knowledgeApi.updateKnowledge(formData as AiKnowledge);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
emit('success', result.data);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '保存失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
ElMessage.error(props.type === 'add' ? '创建失败' : '保存失败');
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消操作
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取索引方式文本
|
||||||
|
function getIndexingText(technique: string | undefined): string {
|
||||||
|
switch (technique) {
|
||||||
|
case 'high_quality':
|
||||||
|
return '高质量';
|
||||||
|
case 'economy':
|
||||||
|
return '经济';
|
||||||
|
default:
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
function formatDate(date: string | Date | undefined): string {
|
||||||
|
if (!date) return '-';
|
||||||
|
const d = new Date(date);
|
||||||
|
if (isNaN(d.getTime())) return '-';
|
||||||
|
return d.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.knowledge-basic-container {
|
||||||
|
padding: 24px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 14px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看模式样式
|
||||||
|
.view-mode {
|
||||||
|
.info-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #F8F9FA;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-avatar {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-details {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.knowledge-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #101828;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #667085;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #667085;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑模式样式
|
||||||
|
.edit-mode {
|
||||||
|
.knowledge-form {
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #101828;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-radio) {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
.el-radio__label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #101828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #667085;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #F59E0B;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip-success {
|
||||||
|
color: #67C23A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #F3F3F5;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px 24px;
|
||||||
|
|
||||||
|
&.el-button--primary {
|
||||||
|
background: #E7000B;
|
||||||
|
border-color: #E7000B;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #C90009;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-switch) {
|
||||||
|
.el-switch__label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #667085;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: #E7000B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
<template>
|
||||||
|
<div class="knowledge-card" @click="handleClick">
|
||||||
|
<div class="card-header">
|
||||||
|
<!-- 头像 -->
|
||||||
|
<div class="knowledge-avatar">
|
||||||
|
<img v-if="knowledge.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
|
||||||
|
<div v-else class="default-avatar">📚</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标题和状态 -->
|
||||||
|
<div class="knowledge-title-section">
|
||||||
|
<h3 class="knowledge-title">{{ knowledge.title }}</h3>
|
||||||
|
<span
|
||||||
|
class="knowledge-status"
|
||||||
|
:class="knowledge.status === 1 ? 'status-active' : 'status-inactive'"
|
||||||
|
>
|
||||||
|
{{ knowledge.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述 -->
|
||||||
|
<div class="knowledge-description">
|
||||||
|
{{ knowledge.description || '暂无描述' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="knowledge-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">文档数量</span>
|
||||||
|
<span class="stat-value">{{ knowledge.documentCount || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">状态</span>
|
||||||
|
<span class="stat-value">
|
||||||
|
{{ getStatusText(knowledge.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="knowledge-actions" @click.stop>
|
||||||
|
<el-button size="small" @click="handleEdit">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="handleDelete">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AiKnowledge } from '@/types/ai';
|
||||||
|
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'KnowledgeCard'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
knowledge: AiKnowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(['click', 'edit', 'delete', 'sync']);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
emit('click', props.knowledge);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
emit('edit', props.knowledge);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
emit('delete', props.knowledge);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status?: number) {
|
||||||
|
switch (status) {
|
||||||
|
case 0: return '禁用';
|
||||||
|
case 1: return '启用';
|
||||||
|
case 2: return '处理中';
|
||||||
|
default: return '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.knowledge-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #E7000B;
|
||||||
|
box-shadow: 0 4px 12px rgba(231, 0, 11, 0.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #F3F3F5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-title-section {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #101828;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-status {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.status-active {
|
||||||
|
background: #DCFCE7;
|
||||||
|
color: #008236;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-inactive {
|
||||||
|
background: #FEF2F2;
|
||||||
|
color: #DC2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-top: 1px solid #F3F3F5;
|
||||||
|
border-bottom: 1px solid #F3F3F5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9CA3AF;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #101828;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
:deep(.el-button) {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
|
||||||
|
&.el-button--small {
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.el-button--default {
|
||||||
|
background: #F3F3F5;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #0A0A0A;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #E5E5E7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.el-button--danger {
|
||||||
|
background: #FEF2F2;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #DC2626;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #FEE2E2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
<template>
|
||||||
|
<div class="knowledge-info-panel" v-if="knowledge">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<div class="panel-header">
|
||||||
|
<el-button text @click="$emit('close')">
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
返回列表
|
||||||
|
</el-button>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button @click="handleEdit">
|
||||||
|
<el-icon><Edit /></el-icon>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="handleUpload">
|
||||||
|
<el-icon><Upload /></el-icon>
|
||||||
|
上传文档
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 知识库信息卡片 -->
|
||||||
|
<div class="knowledge-header">
|
||||||
|
<div class="knowledge-avatar">
|
||||||
|
<el-image
|
||||||
|
v-if="knowledge.avatar"
|
||||||
|
:src="knowledge.avatar"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<el-icon v-else :size="48"><Document /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="knowledge-details">
|
||||||
|
<h2 class="knowledge-name">{{ knowledge.title }}</h2>
|
||||||
|
<p class="knowledge-description">{{ knowledge.description || '暂无描述' }}</p>
|
||||||
|
<div class="knowledge-meta">
|
||||||
|
<el-tag :type="knowledge.status === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ knowledge.status === 1 ? '已启用' : '已禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="meta-item">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
{{ formatDate(knowledge.createTime) }}
|
||||||
|
</span>
|
||||||
|
<span class="meta-item" v-if="knowledge.difyDatasetId">
|
||||||
|
<el-icon><Link /></el-icon>
|
||||||
|
Dify: {{ knowledge.difyDatasetId }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<el-icon color="#409EFF"><Document /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ knowledge.documentCount || 0 }}</div>
|
||||||
|
<div class="stat-label">文档数量</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<el-icon color="#67C23A"><Files /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<el-icon color="#E6A23C"><Paperclip /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-value">{{ documents.length }}</div>
|
||||||
|
<div class="stat-label">上传文件</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文档列表 -->
|
||||||
|
<div class="documents-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3 class="section-title">文档列表</h3>
|
||||||
|
<el-input
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="搜索文档..."
|
||||||
|
clearable
|
||||||
|
style="width: 300px;"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="filteredDocuments"
|
||||||
|
v-loading="loading"
|
||||||
|
style="width: 100%"
|
||||||
|
:empty-text="loading ? '加载中...' : '暂无文档'"
|
||||||
|
>
|
||||||
|
<el-table-column prop="fileName" label="文件名" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="file-name-cell">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ row.fileName }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="fileType" label="类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small">{{ row.fileType || 'unknown' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="fileSize" label="大小" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatFileSize(row.fileSize) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
:type="row.status === 2 ? 'success' : row.status === 1 ? 'warning' : 'danger'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getStatusText(row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="chunkCount" label="分段数" width="100" />
|
||||||
|
<el-table-column prop="createTime" label="上传时间" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="handleViewSegments(row)">
|
||||||
|
查看分段
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="primary" @click="handleDownload(row)">
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
<el-button link type="danger" @click="handleDeleteDocument(row)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="uploadDialogVisible"
|
||||||
|
title="上传文档到知识库"
|
||||||
|
width="600px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:file-list="fileList"
|
||||||
|
drag
|
||||||
|
multiple
|
||||||
|
accept=".txt,.md,.pdf,.doc,.docx"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持 txt/md/pdf/doc/docx 格式文件,单个文件不超过50MB
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirmUpload" :loading="uploading">
|
||||||
|
确认上传
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 文档分段对话框 -->
|
||||||
|
<DocumentSegmentDialog
|
||||||
|
v-if="selectedDocument && selectedDocument.difyDocumentId && props.knowledge?.difyDatasetId"
|
||||||
|
:model-value="segmentDialogVisible"
|
||||||
|
:dataset-id="props.knowledge.difyDatasetId"
|
||||||
|
:document-id="selectedDocument.difyDocumentId"
|
||||||
|
@update:model-value="segmentDialogVisible = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import type { UploadUserFile, UploadInstance } from 'element-plus';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Upload,
|
||||||
|
Refresh,
|
||||||
|
Edit,
|
||||||
|
Document,
|
||||||
|
Clock,
|
||||||
|
Link,
|
||||||
|
Files,
|
||||||
|
Paperclip,
|
||||||
|
Search,
|
||||||
|
UploadFilled
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import type { AiKnowledge, AiUploadFile } from '@/types/ai';
|
||||||
|
import { fileUploadApi, knowledgeApi } from '@/apis/ai';
|
||||||
|
import { DocumentSegmentDialog } from './';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'KnowledgeInfo'
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
knowledge: AiKnowledge | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
refresh: [];
|
||||||
|
edit: [knowledge: AiKnowledge];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 数据状态
|
||||||
|
const documents = ref<AiUploadFile[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const searchQuery = ref('');
|
||||||
|
const uploadDialogVisible = ref(false);
|
||||||
|
const segmentDialogVisible = ref(false);
|
||||||
|
const selectedDocument = ref<AiUploadFile | null>(null);
|
||||||
|
const uploading = ref(false);
|
||||||
|
|
||||||
|
// 上传相关
|
||||||
|
const uploadRef = ref<UploadInstance>();
|
||||||
|
const fileList = ref<UploadUserFile[]>([]);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const filteredDocuments = computed(() => {
|
||||||
|
if (!searchQuery.value) return documents.value;
|
||||||
|
const query = searchQuery.value.toLowerCase();
|
||||||
|
return documents.value.filter(doc =>
|
||||||
|
doc.fileName?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听知识库变化
|
||||||
|
watch(() => props.knowledge, (newVal) => {
|
||||||
|
if (newVal?.id) {
|
||||||
|
loadDocuments();
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 加载文档列表
|
||||||
|
async function loadDocuments() {
|
||||||
|
if (!props.knowledge?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const result = await fileUploadApi.listFilesByKnowledge(props.knowledge.id);
|
||||||
|
if (result.success) {
|
||||||
|
documents.value = (result.dataList || []) as AiUploadFile[];
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载文档列表失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载文档列表失败:', error);
|
||||||
|
ElMessage.error('加载文档列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑知识库
|
||||||
|
function handleEdit() {
|
||||||
|
if (props.knowledge) {
|
||||||
|
emit('edit', props.knowledge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理上传
|
||||||
|
function handleUpload() {
|
||||||
|
uploadDialogVisible.value = true;
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件变化
|
||||||
|
function handleFileChange(file: any, fileListParam: any) {
|
||||||
|
fileList.value = fileListParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认上传
|
||||||
|
async function handleConfirmUpload() {
|
||||||
|
if (fileList.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择要上传的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.knowledge?.id) {
|
||||||
|
ElMessage.error('知识库ID不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uploading.value = true;
|
||||||
|
const files = fileList.value
|
||||||
|
.map(f => f.raw)
|
||||||
|
.filter(f => f !== null && f !== undefined) as File[];
|
||||||
|
|
||||||
|
// 逐个上传文件
|
||||||
|
for (const file of files) {
|
||||||
|
const result = await fileUploadApi.uploadFile(props.knowledge.id, file);
|
||||||
|
if (!result.success) {
|
||||||
|
ElMessage.error(`文件 ${file.name} 上传失败: ${result.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('文件上传成功');
|
||||||
|
uploadDialogVisible.value = false;
|
||||||
|
fileList.value = [];
|
||||||
|
await loadDocuments();
|
||||||
|
emit('refresh');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('上传文件失败:', error);
|
||||||
|
ElMessage.error('上传文件失败');
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看分段
|
||||||
|
function handleViewSegments(document: AiUploadFile) {
|
||||||
|
selectedDocument.value = document;
|
||||||
|
segmentDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
function handleDownload(document: AiUploadFile) {
|
||||||
|
if (document.sysFileId) {
|
||||||
|
window.open(`/api/file/download/${document.sysFileId}`, '_blank');
|
||||||
|
} else if (document.filePath) {
|
||||||
|
window.open(`/api/file/download/${document.filePath}`, '_blank');
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('文件路径不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文档
|
||||||
|
async function handleDeleteDocument(document: AiUploadFile) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除文档"${document.fileName}"吗?此操作不可恢复。`,
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (document.id) {
|
||||||
|
const result = await fileUploadApi.deleteFile(document.id);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
await loadDocuments();
|
||||||
|
emit('refresh');
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除文档失败:', error);
|
||||||
|
ElMessage.error('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
function formatDate(date: string | Date | undefined): string {
|
||||||
|
if (!date) return '-';
|
||||||
|
const d = new Date(date);
|
||||||
|
if (isNaN(d.getTime())) return '-';
|
||||||
|
return d.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number | undefined): string {
|
||||||
|
if (!bytes || bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusText(status: number | undefined): string {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return '处理中';
|
||||||
|
case 2:
|
||||||
|
return '已完成';
|
||||||
|
case 3:
|
||||||
|
return '失败';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.knowledge-info-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid #F3F3F5;
|
||||||
|
background: #FFFFFF;
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button) {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.el-button--primary {
|
||||||
|
background: #E7000B;
|
||||||
|
border-color: #E7000B;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #C90009;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
background: linear-gradient(135deg, #F8F9FA 0%, #FFFFFF 100%);
|
||||||
|
border-bottom: 1px solid #F3F3F5;
|
||||||
|
|
||||||
|
.knowledge-avatar {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
:deep(.el-image) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
color: #E7000B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-details {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.knowledge-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #101828;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #667085;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #667085;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid #F3F3F5;
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #F8F9FA;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #101828;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents-section {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #101828;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
color: #E7000B;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.el-table__header th {
|
||||||
|
background: #F8F9FA;
|
||||||
|
color: #667085;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
border-bottom: 1px solid #F3F3F5;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload-dragger {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as KnowledgeCard } from './KnowledgeCard.vue';
|
||||||
|
export { default as KnowledgeInfo } from './KnowledgeInfo.vue';
|
||||||
|
export { default as KnowledgeBasic } from './KnowledgeBasic.vue';
|
||||||
|
export { default as DocumentSegmentDialog } from './DocumentSegmentDialog.vue';
|
||||||
@@ -679,6 +679,19 @@ async function sendMessage() {
|
|||||||
// 调用API
|
// 调用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消息的数据库ID(task_id),用于停止生成
|
// 保存AI消息的数据库ID(task_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消息的数据库ID(task_id),用于停止生成
|
// 保存AI消息的数据库ID(task_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());
|
||||||
|
|||||||
Reference in New Issue
Block a user