From 3ca2c89288db1d6cba514ced02c91624ef5fe497 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 09 六月 2026 16:15:56 +0800
Subject: [PATCH] feat(collaborativeApproval): 添加知识库RAG向量检索问答功能
---
.gitignore | 2
doc/知识库RAG功能实现文档.md | 1034 ++++++++++++++++++++++++++++++++++++++
src/api/collaborativeApproval/knowledgeBase.js | 52 +
src/views/collaborativeApproval/knowledgeBase/index.vue | 525 +++++++++++++++++++
4 files changed, 1,609 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
index 78a752d..f76fc8f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
yarn-error.log*
**/*.log
+.claude/
+
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
diff --git "a/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md" "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..4a94cc5
--- /dev/null
+++ "b/doc/\347\237\245\350\257\206\345\272\223RAG\345\212\237\350\203\275\345\256\236\347\216\260\346\226\207\346\241\243.md"
@@ -0,0 +1,1034 @@
+# 鐭ヨ瘑搴揜AG鍚戦噺妫�绱㈠姛鑳藉疄鐜版枃妗�
+
+## 涓�銆佸姛鑳芥杩�
+
+鍩轰簬 RAG锛圧etrieval-Augmented Generation锛夋妧鏈疄鐜扮煡璇嗗簱闂瓟鍔熻兘锛屾敮鎸侊細
+- 鐭ヨ瘑搴撶鐞嗭紙CRUD锛�
+- 鏂囦欢涓婁紶涓庡悜閲忓寲澶勭悊
+- 鍩轰簬鍚戦噺妫�绱㈢殑鏅鸿兘闂瓟
+- 澶氱鏂囦欢鏍煎紡鏀寔锛坱xt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df锛�
+
+## 浜屻�佹妧鏈灦鏋�
+
+### 2.1 鎶�鏈爤
+| 缁勪欢 | 鎶�鏈� |
+|------|------|
+| 鍚戦噺鏁版嵁搴� | Pinecone |
+| Embedding妯″瀷 | 闃块噷浜� DashScope text-embedding-v3 |
+| LLM | 闃块噷浜戦�氫箟鍗冮棶 qwen-max |
+| 妗嗘灦 | langchain4j |
+| ORM | MyBatis-Plus |
+
+### 2.2 鏋舵瀯鍥�
+
+```
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� 鍓嶇搴旂敤 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Controller Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeBaseCtrl 鈹� 鈹� KnowledgeChatController 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴撶鐞�) 鈹� 鈹� (鐭ヨ瘑搴撻棶绛�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� Service Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹侹nowledgeBaseService 鈹� 鈹� KnowledgeRagService 鈹� 鈹�
+鈹� 鈹� (鐭ヨ瘑搴揅RUD) 鈹� 鈹� (鍚戦噺鍖�/妫�绱�) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+ 鈹�
+ 鈻�
+鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+鈹� AI Layer 鈹�
+鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹屸攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹� 鈹� KnowledgeChatAgent 鈹� 鈹� EmbeddingStore (Pinecone) 鈹� 鈹�
+鈹� 鈹� (闂瓟Agent) 鈹� 鈹� (鍚戦噺瀛樺偍) 鈹� 鈹�
+鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹� 鈹�
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+```
+
+---
+
+## 涓夈�佸悗绔疄鐜�
+
+### 3.1 鏁版嵁搴撹璁�
+
+#### 3.1.1 鐭ヨ瘑搴撹〃锛坘nowledge_base锛�
+
+```sql
+CREATE TABLE knowledge_base (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(255) COMMENT '鐭ヨ瘑鏍囬',
+ type VARCHAR(50) COMMENT '鐭ヨ瘑绫诲瀷',
+ scenario VARCHAR(255) COMMENT '閫傜敤鍦烘櫙',
+ efficiency VARCHAR(20) COMMENT '瑙e喅鏁堢巼',
+ problem TEXT COMMENT '闂鎻忚堪',
+ solution TEXT COMMENT '瑙e喅鏂规',
+ key_points TEXT COMMENT '鍏抽敭瑕佺偣',
+ creator VARCHAR(100) COMMENT '鍒涘缓浜�',
+ usage_count INT DEFAULT 0 COMMENT '浣跨敤娆℃暟',
+ file_count INT DEFAULT 0 COMMENT '鏂囦欢鏁伴噺',
+ total_chunk_count INT DEFAULT 0 COMMENT '鎬诲垏鐗囨暟閲�',
+ description VARCHAR(500) COMMENT '鐭ヨ瘑搴撴弿杩�',
+ create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
+ update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ create_user INT,
+ update_user INT,
+ tenant_id BIGINT,
+ dept_id BIGINT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鐭ヨ瘑搴撹〃';
+```
+
+#### 3.1.2 鐭ヨ瘑搴撳悜閲忚褰曡〃锛坘nowledge_base_vector锛�
+
+```sql
+CREATE TABLE knowledge_base_vector (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '涓婚敭ID',
+ knowledge_base_id BIGINT NOT NULL COMMENT '鍏宠仈鐭ヨ瘑搴揑D',
+ storage_blob_id BIGINT NOT NULL COMMENT '鍏宠仈鏂囦欢blob ID',
+ file_name VARCHAR(255) NOT NULL COMMENT '鏂囦欢鍚嶇О',
+ file_type VARCHAR(50) NOT NULL COMMENT '鏂囦欢绫诲瀷',
+ vector_status TINYINT DEFAULT 0 COMMENT '鍚戦噺鍖栫姸鎬�: 0-寰呭鐞�, 1-澶勭悊涓�, 2-宸插畬鎴�, 3-澶辫触',
+ vector_error VARCHAR(500) COMMENT '鍚戦噺鍖栧け璐ュ師鍥�',
+ chunk_count INT DEFAULT 0 COMMENT '鍒囩墖鏁伴噺',
+ namespace VARCHAR(100) COMMENT '鍚戦噺鍛藉悕绌洪棿',
+ create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
+ create_user INT,
+ update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ update_user INT,
+ tenant_id BIGINT,
+ dept_id BIGINT,
+ INDEX idx_knowledge_base_id (knowledge_base_id),
+ INDEX idx_storage_blob_id (storage_blob_id),
+ INDEX idx_vector_status (vector_status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鐭ヨ瘑搴撴枃浠跺悜閲忚褰曡〃';
+```
+
+### 3.2 Maven渚濊禆
+
+```xml
+<!-- langchain4j BOM -->
+<dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-bom</artifactId>
+ <version>1.0.0-beta3</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+</dependencyManagement>
+
+<dependencies>
+ <!-- langchain4j 鏍稿績 -->
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-spring-boot-starter</artifactId>
+ </dependency>
+
+ <!-- Pinecone 鍚戦噺鏁版嵁搴� -->
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-pinecone</artifactId>
+ </dependency>
+
+ <!-- 闃块噷浜� DashScope -->
+ <dependency>
+ <groupId>dev.langchain4j</groupId>
+ <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
+ </dependency>
+</dependencies>
+```
+
+### 3.3 閰嶇疆鏂囦欢锛坅pplication.yml锛�
+
+```yaml
+# Pinecone 鍚戦噺鏁版嵁搴撻厤缃�
+pinecone:
+ api-key: your-pinecone-api-key
+ index: your-index-name
+ namespace: knowledge-base
+
+# langchain4j 閰嶇疆
+langchain4j:
+ community:
+ dashscope:
+ streaming-chat-model:
+ api-key: your-dashscope-api-key
+ model-name: "qwen-max"
+ embedding-model:
+ api-key: your-dashscope-api-key
+ model-name: "text-embedding-v3"
+```
+
+### 3.4 鏍稿績浠g爜瀹炵幇
+
+#### 3.4.1 瀹炰綋绫�
+
+**KnowledgeBase.java**
+```java
+@Data
+@TableName("knowledge_base")
+public class KnowledgeBase implements Serializable {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String title;
+ private String type;
+ private String scenario;
+ private String efficiency;
+ private String problem;
+ private String solution;
+ private String keyPoints;
+ private String creator;
+ private Integer usageCount;
+ private Integer fileCount;
+ private Integer totalChunkCount;
+ private String description;
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+ @TableField(fill = FieldFill.INSERT)
+ private Integer createUser;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Integer updateUser;
+ @TableField(fill = FieldFill.INSERT)
+ private Long tenantId;
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+}
+```
+
+**KnowledgeBaseVector.java**
+```java
+@Data
+@TableName("knowledge_base_vector")
+public class KnowledgeBaseVector implements Serializable {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long knowledgeBaseId;
+ private Long storageBlobId;
+ private String fileName;
+ private String fileType;
+ private Integer vectorStatus;
+ private String vectorError;
+ private Integer chunkCount;
+ private String namespace;
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+ @TableField(fill = FieldFill.INSERT)
+ private Integer createUser;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private Integer updateUser;
+ @TableField(fill = FieldFill.INSERT)
+ private Long tenantId;
+ @TableField(fill = FieldFill.INSERT)
+ private Long deptId;
+
+ // 鍚戦噺鍖栫姸鎬佸父閲�
+ public static final int STATUS_PENDING = 0;
+ public static final int STATUS_PROCESSING = 1;
+ public static final int STATUS_COMPLETED = 2;
+ public static final int STATUS_FAILED = 3;
+}
+```
+
+#### 3.4.2 EmbeddingStore閰嶇疆
+
+**EmbeddingStoreConfig.java**
+```java
+@Configuration
+public class EmbeddingStoreConfig {
+
+ @Value("${pinecone.api-key}")
+ private String pineconeApiKey;
+
+ @Value("${pinecone.index}")
+ private String indexName;
+
+ @Value("${pinecone.namespace}")
+ private String namespace;
+
+ @Bean
+ public Pinecone pinecone() {
+ return new Pinecone.Builder(pineconeApiKey).build();
+ }
+
+ @Bean
+ public Index pineconeIndex(Pinecone pinecone) {
+ return pinecone.getIndexConnection(indexName);
+ }
+
+ @Bean
+ public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
+ return PineconeEmbeddingStore.builder()
+ .apiKey(pineconeApiKey)
+ .index(indexName)
+ .nameSpace(namespace)
+ .createIndex(PineconeServerlessIndexConfig.builder()
+ .cloud("AWS")
+ .region("us-east-1")
+ .dimension(embeddingModel.dimension())
+ .build())
+ .build();
+ }
+}
+```
+
+#### 3.4.3 RAG鏈嶅姟瀹炵幇
+
+**KnowledgeRagService.java**
+```java
+public interface KnowledgeRagService {
+ void processVectorAsync(Long vectorId);
+ void processVector(Long vectorId);
+ List<String> searchRelevantContent(String namespace, String query, int maxResults);
+ void deleteEmbeddings(String namespace, Long storageBlobId);
+}
+```
+
+**KnowledgeRagServiceImpl.java**锛堟牳蹇冨疄鐜帮級
+```java
+@Slf4j
+@Service
+public class KnowledgeRagServiceImpl implements KnowledgeRagService {
+
+ private final KnowledgeBaseVectorService knowledgeBaseVectorService;
+ private final StorageBlobService storageBlobService;
+ private final EmbeddingModel embeddingModel;
+ private final EmbeddingStore<TextSegment> embeddingStore;
+ private final FileProperties fileProperties;
+ private final Index pineconeIndex;
+
+ @Value("${pinecone.namespace}")
+ private String namespace;
+
+ private static final int CHUNK_SIZE = 500;
+ private static final int CHUNK_OVERLAP = 100;
+ private static final long CHUNK_THRESHOLD_BYTES = 80L * 1024 * 1024;
+ private static final int EMBEDDING_MAX_LENGTH = 8000;
+
+ @Override
+ @Async("threadPoolTaskExecutor")
+ public void processVectorAsync(Long vectorId) {
+ processVector(vectorId);
+ }
+
+ @Override
+ public void processVector(Long vectorId) {
+ KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId);
+ if (vector == null) return;
+
+ try {
+ // 鏇存柊鐘舵�佷负澶勭悊涓�
+ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_PROCESSING, null, null);
+
+ // 鑾峰彇鏂囦欢鍐呭
+ StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId());
+ File file = getFile(blob);
+ String content = extractFileContent(file, vector.getFileName());
+
+ if (content == null || content.trim().isEmpty()) {
+ throw new RuntimeException("鏂囦欢鍐呭涓虹┖");
+ }
+
+ // 鏂囨湰鍒囩墖
+ List<TextSegment> chunks;
+ boolean needChunk = file.length() > CHUNK_THRESHOLD_BYTES || content.length() > EMBEDDING_MAX_LENGTH;
+ if (needChunk) {
+ chunks = splitText(content, vector);
+ } else {
+ Map<String, Object> metadata = buildMetadata(vector);
+ chunks = List.of(TextSegment.from(content, new Metadata(metadata)));
+ }
+
+ // 鐢熸垚宓屽叆鍚戦噺骞跺瓨鍌�
+ int chunkCount = 0;
+ for (TextSegment chunk : chunks) {
+ Embedding embedding = embeddingModel.embed(chunk).content();
+ embeddingStore.add(embedding, chunk);
+ chunkCount++;
+ }
+
+ // 鏇存柊鐘舵�佷负瀹屾垚
+ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_COMPLETED, chunkCount, null);
+
+ } catch (Exception e) {
+ log.error("鍚戦噺鍖栧鐞嗗け璐�", e);
+ knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_FAILED, null, e.getMessage());
+ }
+ }
+
+ @Override
+ public List<String> searchRelevantContent(String namespace, String query, int maxResults) {
+ Embedding queryEmbedding = embeddingModel.embed(query).content();
+ EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
+ .queryEmbedding(queryEmbedding)
+ .maxResults(maxResults)
+ .minScore(0.7)
+ .build();
+ EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);
+ return searchResult.matches().stream()
+ .map(match -> match.embedded().text())
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void deleteEmbeddings(String namespace, Long storageBlobId) {
+ Struct filter = Struct.newBuilder()
+ .putFields("storageBlobId", Value.newBuilder()
+ .setStructValue(Struct.newBuilder()
+ .putFields("$eq", Value.newBuilder()
+ .setNumberValue(storageBlobId.doubleValue())
+ .build()))
+ .build())
+ .build();
+ pineconeIndex.delete(new ArrayList<>(), false, this.namespace, filter);
+ }
+
+ private String extractFileContent(File file, String fileName) throws Exception {
+ String ext = getFileExtension(fileName);
+ if (isPlainText(ext)) {
+ return readFileWithEncoding(file);
+ }
+ if ("docx".equals(ext)) {
+ return extractDocx(file);
+ }
+ if ("xlsx".equals(ext) || "xls".equals(ext)) {
+ return extractExcel(file);
+ }
+ return readFileWithEncoding(file);
+ }
+
+ // ... 鍏朵粬杈呭姪鏂规硶
+}
+```
+
+#### 3.4.4 鐭ヨ瘑搴撻棶绛擜gent
+
+**KnowledgeChatAgent.java**
+```java
+@AiService(
+ wiringMode = EXPLICIT,
+ streamingChatModel = "qwenStreamingChatModel",
+ chatMemoryProvider = "chatMemoryProvider"
+)
+public interface KnowledgeChatAgent {
+
+ @SystemMessage("""
+ 浣犳槸浼佷笟鐭ヨ瘑搴撻棶绛斿姪鎵嬨��
+ 浣犻渶瑕佸熀浜庢彁渚涚殑鐭ヨ瘑搴撳唴瀹瑰洖绛旂敤鎴烽棶棰樸��
+ 閬靛惊浠ヤ笅瑙勫垯锛�
+ 1. 涓ユ牸鍩轰簬鐭ヨ瘑搴撳唴瀹瑰洖绛旓紝涓嶈缂栭�犱俊鎭�
+ 2. 濡傛灉鐭ヨ瘑搴撲腑娌℃湁鐩稿叧淇℃伅锛屾槑纭憡鐭ョ敤鎴�
+ 3. 鍥炵瓟瑕佸噯纭�佺畝娲併�佹湁鏉$悊
+ 4. 寮曠敤鏉ユ簮鏃舵敞鏄�"鏍规嵁鐭ヨ瘑搴撳唴瀹�"
+ """)
+ Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
+}
+```
+
+#### 3.4.5 Controller灞�
+
+**KnowledgeBaseController.java**
+```java
+@RestController
+@RequestMapping("/knowledgeBase")
+@Tag(name = "鐭ヨ瘑搴撶鐞�")
+public class KnowledgeBaseController {
+
+ @GetMapping("/getList")
+ public AjaxResult getList(@RequestParam(defaultValue = "1") long current,
+ @RequestParam(defaultValue = "10") long size,
+ KnowledgeBase knowledgeBase) {
+ Page page = new Page(current, size);
+ return AjaxResult.success(knowledgeBaseService.listpage(page, knowledgeBase));
+ }
+
+ @PostMapping("/add")
+ public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase) {
+ return AjaxResult.success(knowledgeBaseService.save(knowledgeBase));
+ }
+
+ @PostMapping("/update")
+ public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase) {
+ return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase));
+ }
+
+ @DeleteMapping("/delete")
+ public AjaxResult delete(@RequestBody List<Long> ids) {
+ return AjaxResult.success(knowledgeBaseService.removeByIds(ids));
+ }
+
+ @GetMapping("/vector/status/{knowledgeBaseId}")
+ @Operation(summary = "鏌ヨ鐭ヨ瘑搴撴枃浠跺悜閲忓寲鐘舵��")
+ public AjaxResult getVectorStatus(@PathVariable Long knowledgeBaseId) {
+ return AjaxResult.success(knowledgeBaseVectorService.getVectorStatusByKnowledgeBaseId(knowledgeBaseId));
+ }
+
+ @PostMapping("/vector/reprocess/{vectorId}")
+ @Operation(summary = "閲嶆柊鍚戦噺鍖栨枃浠�")
+ public AjaxResult reprocessVector(@PathVariable Long vectorId) {
+ knowledgeBaseVectorService.reprocessVector(vectorId);
+ return AjaxResult.success("宸查噸鏂版彁浜ゅ悜閲忓寲浠诲姟");
+ }
+
+ @PostMapping("/file/save")
+ @Operation(summary = "淇濆瓨鐭ヨ瘑搴撴枃浠跺叧鑱�")
+ public AjaxResult saveKnowledgeBaseFiles(@RequestBody KnowledgeBaseFileDTO dto) {
+ // 淇濆瓨闄勪欢鍏宠仈骞惰Е鍙戝悜閲忓寲
+ // ...
+ }
+
+ @DeleteMapping("/file/delete")
+ @Operation(summary = "鍒犻櫎鐭ヨ瘑搴撴枃浠�")
+ public AjaxResult deleteKnowledgeBaseFiles(@RequestBody List<Long> vectorIds) {
+ knowledgeBaseVectorService.deleteVectors(vectorIds);
+ return AjaxResult.success();
+ }
+}
+```
+
+**KnowledgeChatController.java**
+```java
+@RestController
+@RequestMapping("/ai/knowledge")
+@Tag(name = "鐭ヨ瘑搴撻棶绛�")
+public class KnowledgeChatController {
+
+ private final KnowledgeChatAgent knowledgeChatAgent;
+ private final KnowledgeRagService knowledgeRagService;
+ private final KnowledgeBaseService knowledgeBaseService;
+
+ @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
+ @Operation(summary = "鐭ヨ瘑搴撻棶绛�")
+ public Flux<String> chat(@RequestBody KnowledgeChatRequest request) {
+ // 妫�绱㈢浉鍏冲唴瀹�
+ String namespace = "kb-" + request.getKnowledgeBaseId();
+ List<String> relevantContents = knowledgeRagService.searchRelevantContent(
+ namespace, request.getQuestion(), 5);
+
+ if (relevantContents.isEmpty()) {
+ return Flux.just("鐭ヨ瘑搴撲腑鏈壘鍒扮浉鍏冲唴瀹�");
+ }
+
+ // 鏋勫缓涓婁笅鏂�
+ StringBuilder context = new StringBuilder();
+ context.append("浠ヤ笅鏄粠鐭ヨ瘑搴撲腑妫�绱㈠埌鐨勭浉鍏冲唴瀹癸細\n\n");
+ for (int i = 0; i < relevantContents.size(); i++) {
+ context.append("銆愬唴瀹�").append(i + 1).append("銆慭n");
+ context.append(relevantContents.get(i)).append("\n\n");
+ }
+ context.append("---\n璇峰熀浜庝互涓婄煡璇嗗簱鍐呭鍥炵瓟锛歕n").append(request.getQuestion());
+
+ return knowledgeChatAgent.chat(request.getMemoryId(), context.toString());
+ }
+
+ @GetMapping("/list")
+ @Operation(summary = "鐭ヨ瘑搴撳垪琛�")
+ public AjaxResult listKnowledgeBases() {
+ return AjaxResult.success(knowledgeBaseService.list());
+ }
+}
+```
+
+---
+
+## 鍥涖�丄PI鎺ュ彛鏂囨。
+
+### 4.1 鐭ヨ瘑搴撶鐞嗘帴鍙�
+
+| 鎺ュ彛 | 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|------|
+| 鑾峰彇鍒楄〃 | GET | /knowledgeBase/getList | 鍒嗛〉鏌ヨ鐭ヨ瘑搴撳垪琛� |
+| 鏂板鐭ヨ瘑搴� | POST | /knowledgeBase/add | 鍒涘缓鐭ヨ瘑搴� |
+| 鏇存柊鐭ヨ瘑搴� | POST | /knowledgeBase/update | 鏇存柊鐭ヨ瘑搴撲俊鎭� |
+| 鍒犻櫎鐭ヨ瘑搴� | DELETE | /knowledgeBase/delete | 鎵归噺鍒犻櫎鐭ヨ瘑搴� |
+| 鏌ヨ鍚戦噺鍖栫姸鎬� | GET | /knowledgeBase/vector/status/{id} | 鏌ヨ鏂囦欢鍚戦噺鍖栫姸鎬� |
+| 閲嶆柊鍚戦噺鍖� | POST | /knowledgeBase/vector/reprocess/{id} | 閲嶆柊澶勭悊澶辫触鐨勬枃浠� |
+| 淇濆瓨鏂囦欢鍏宠仈 | POST | /knowledgeBase/file/save | 涓婁紶鏂囦欢鍚庡叧鑱斿埌鐭ヨ瘑搴� |
+| 鍒犻櫎鏂囦欢 | DELETE | /knowledgeBase/file/delete | 鍒犻櫎鐭ヨ瘑搴撴枃浠� |
+
+### 4.2 鐭ヨ瘑搴撻棶绛旀帴鍙�
+
+| 鎺ュ彛 | 鏂规硶 | 璺緞 | 璇存槑 |
+|------|------|------|------|
+| 鐭ヨ瘑搴撻棶绛� | POST | /ai/knowledge/chat | 娴佸紡杩斿洖闂瓟缁撴灉 |
+| 鐭ヨ瘑搴撳垪琛� | GET | /ai/knowledge/list | 鑾峰彇鍙�夌煡璇嗗簱鍒楄〃 |
+
+### 4.3 鎺ュ彛璇︾粏璇存槑
+
+#### 4.3.1 淇濆瓨鐭ヨ瘑搴撴枃浠跺叧鑱�
+
+**璇锋眰**
+```json
+POST /knowledgeBase/file/save
+{
+ "knowledgeBaseId": 1,
+ "storageBlobIds": [100, 101, 102]
+}
+```
+
+**鍝嶅簲**
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛"
+}
+```
+
+#### 4.3.2 鐭ヨ瘑搴撻棶绛�
+
+**璇锋眰**
+```json
+POST /ai/knowledge/chat
+Content-Type: application/json
+
+{
+ "knowledgeBaseId": 1,
+ "memoryId": "session-uuid",
+ "question": "濡備綍澶勭悊搴撳瓨鐩樼偣宸紓锛�"
+}
+```
+
+**鍝嶅簲**锛圫SE娴佸紡锛�
+```
+鏍规嵁鐭ヨ瘑搴撳唴瀹癸紝搴撳瓨鐩樼偣宸紓鐨勫鐞嗘祦绋嬪涓嬶細
+
+1. 鍙戠幇宸紓鍚庯紝棣栧厛鏍稿鐩樼偣璁板綍...
+2. 妫�鏌ユ槸鍚︽湁婕忕洏鎴栭敊鐩�...
+3. ...
+```
+
+---
+
+## 浜斻�佸墠绔疄鐜�
+
+### 5.1 鐭ヨ瘑搴撶鐞嗛〉闈�
+
+```vue
+<template>
+ <div class="knowledge-base">
+ <!-- 鍒楄〃 -->
+ <el-table :data="tableData" border>
+ <el-table-column prop="title" label="鐭ヨ瘑鏍囬" />
+ <el-table-column prop="type" label="鐭ヨ瘑绫诲瀷" />
+ <el-table-column prop="fileCount" label="鏂囦欢鏁伴噺" />
+ <el-table-column prop="totalChunkCount" label="鍒囩墖鏁伴噺" />
+ <el-table-column label="鎿嶄綔">
+ <template #default="{ row }">
+ <el-button @click="handleEdit(row)">缂栬緫</el-button>
+ <el-button @click="handleFiles(row)">鏂囦欢绠$悊</el-button>
+ <el-button @click="handleChat(row)">闂瓟</el-button>
+ <el-button type="danger" @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getKnowledgeBaseList, deleteKnowledgeBase } from '@/api/knowledge'
+
+const tableData = ref([])
+
+const loadData = async () => {
+ const res = await getKnowledgeBaseList({ current: 1, size: 10 })
+ tableData.value = res.data.records
+}
+
+onMounted(loadData)
+</script>
+```
+
+### 5.2 鏂囦欢涓婁紶涓庡悜閲忓寲鐘舵��
+
+```vue
+<template>
+ <div class="file-manager">
+ <!-- 鏂囦欢涓婁紶 -->
+ <el-upload
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ multiple
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+
+ <!-- 鏂囦欢鍒楄〃涓庡悜閲忓寲鐘舵�� -->
+ <el-table :data="fileList">
+ <el-table-column prop="fileName" label="鏂囦欢鍚�" />
+ <el-table-column label="鍚戦噺鍖栫姸鎬�">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.vectorStatus)">
+ {{ getStatusText(row.vectorStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="chunkCount" label="鍒囩墖鏁�" />
+ <el-table-column label="鎿嶄綔">
+ <template #default="{ row }">
+ <el-button v-if="row.vectorStatus === 3" @click="reprocess(row)">
+ 閲嶆柊澶勭悊
+ </el-button>
+ <el-button type="danger" @click="deleteFile(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+</template>
+
+<script setup>
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload'
+
+// 涓婁紶鎴愬姛鍚庝繚瀛樺叧鑱�
+const uploadedBlobIds = ref([])
+
+const handleUploadSuccess = (response, file) => {
+ if (response.code === 200) {
+ uploadedBlobIds.value.push(response.data.id)
+ }
+}
+
+// 淇濆瓨鏂囦欢鍏宠仈
+const saveFiles = async () => {
+ await saveKnowledgeBaseFiles({
+ knowledgeBaseId: props.knowledgeBaseId,
+ storageBlobIds: uploadedBlobIds.value
+ })
+ // 鍒锋柊鏂囦欢鍒楄〃
+ loadFileList()
+}
+
+// 鐘舵�佹枃鏈槧灏�
+const getStatusText = (status) => {
+ const map = {
+ 0: '寰呭鐞�',
+ 1: '澶勭悊涓�',
+ 2: '宸插畬鎴�',
+ 3: '澶辫触'
+ }
+ return map[status] || '鏈煡'
+}
+
+const getStatusType = (status) => {
+ const map = {
+ 0: 'info',
+ 1: 'warning',
+ 2: 'success',
+ 3: 'danger'
+ }
+ return map[status] || 'info'
+}
+</script>
+```
+
+### 5.3 鐭ヨ瘑搴撻棶绛旂晫闈�
+
+```vue
+<template>
+ <div class="knowledge-chat">
+ <!-- 鐭ヨ瘑搴撻�夋嫨 -->
+ <el-select v-model="selectedKbId" placeholder="閫夋嫨鐭ヨ瘑搴�">
+ <el-option
+ v-for="kb in knowledgeBases"
+ :key="kb.id"
+ :label="kb.title"
+ :value="kb.id"
+ />
+ </el-select>
+
+ <!-- 瀵硅瘽鍖哄煙 -->
+ <div class="chat-messages">
+ <div
+ v-for="(msg, index) in messages"
+ :key="index"
+ :class="['message', msg.role]"
+ >
+ <div class="content">{{ msg.content }}</div>
+ </div>
+ </div>
+
+ <!-- 杈撳叆妗� -->
+ <el-input
+ v-model="inputQuestion"
+ placeholder="璇疯緭鍏ラ棶棰�"
+ @keyup.enter="sendMessage"
+ >
+ <template #append>
+ <el-button @click="sendMessage" :loading="loading">鍙戦��</el-button>
+ </template>
+ </el-input>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getKnowledgeBaseList, knowledgeChat } from '@/api/knowledge'
+
+const knowledgeBases = ref([])
+const selectedKbId = ref(null)
+const messages = ref([])
+const inputQuestion = ref('')
+const loading = ref(false)
+const memoryId = ref(crypto.randomUUID())
+
+const sendMessage = async () => {
+ if (!inputQuestion.value.trim()) return
+ if (!selectedKbId.value) {
+ ElMessage.warning('璇烽�夋嫨鐭ヨ瘑搴�')
+ return
+ }
+
+ // 娣诲姞鐢ㄦ埛娑堟伅
+ messages.value.push({
+ role: 'user',
+ content: inputQuestion.value
+ })
+
+ loading.value = true
+
+ try {
+ // 娴佸紡璇锋眰
+ const response = await fetch('/api/ai/knowledge/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ knowledgeBaseId: selectedKbId.value,
+ memoryId: memoryId.value,
+ question: inputQuestion.value
+ })
+ })
+
+ // 澶勭悊SSE娴佸紡鍝嶅簲
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+ let aiContent = ''
+
+ messages.value.push({ role: 'assistant', content: '' })
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ const text = decoder.decode(value)
+ aiContent += text
+ messages.value[messages.value.length - 1].content = aiContent
+ }
+ } finally {
+ loading.value = false
+ inputQuestion.value = ''
+ }
+}
+
+onMounted(async () => {
+ const res = await getKnowledgeBaseList()
+ knowledgeBases.value = res.data
+})
+</script>
+```
+
+### 5.4 API灏佽
+
+```javascript
+// api/knowledge.js
+import request from '@/utils/request'
+
+// 鑾峰彇鐭ヨ瘑搴撳垪琛�
+export function getKnowledgeBaseList(params) {
+ return request({
+ url: '/knowledgeBase/getList',
+ method: 'get',
+ params
+ })
+}
+
+// 鏂板鐭ヨ瘑搴�
+export function addKnowledgeBase(data) {
+ return request({
+ url: '/knowledgeBase/add',
+ method: 'post',
+ data
+ })
+}
+
+// 鏇存柊鐭ヨ瘑搴�
+export function updateKnowledgeBase(data) {
+ return request({
+ url: '/knowledgeBase/update',
+ method: 'post',
+ data
+ })
+}
+
+// 鍒犻櫎鐭ヨ瘑搴�
+export function deleteKnowledgeBase(ids) {
+ return request({
+ url: '/knowledgeBase/delete',
+ method: 'delete',
+ data: ids
+ })
+}
+
+// 鑾峰彇鏂囦欢鍚戦噺鍖栫姸鎬�
+export function getVectorStatus(knowledgeBaseId) {
+ return request({
+ url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
+ method: 'get'
+ })
+}
+
+// 閲嶆柊鍚戦噺鍖�
+export function reprocessVector(vectorId) {
+ return request({
+ url: `/knowledgeBase/vector/reprocess/${vectorId}`,
+ method: 'post'
+ })
+}
+
+// 淇濆瓨鏂囦欢鍏宠仈
+export function saveKnowledgeBaseFiles(data) {
+ return request({
+ url: '/knowledgeBase/file/save',
+ method: 'post',
+ data
+ })
+}
+
+// 鍒犻櫎鏂囦欢
+export function deleteKnowledgeBaseFiles(vectorIds) {
+ return request({
+ url: '/knowledgeBase/file/delete',
+ method: 'delete',
+ data: vectorIds
+ })
+}
+
+// 鐭ヨ瘑搴撻棶绛旓紙娴佸紡锛�
+export async function knowledgeChat(data) {
+ const response = await fetch('/api/ai/knowledge/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ })
+ return response.body
+}
+
+// 鑾峰彇鐭ヨ瘑搴撳垪琛紙闂瓟鐢級
+export function getKnowledgeBaseListForChat() {
+ return request({
+ url: '/ai/knowledge/list',
+ method: 'get'
+ })
+}
+```
+
+---
+
+## 鍏�佹牳蹇冩祦绋�
+
+### 6.1 鏂囦欢涓婁紶涓庡悜閲忓寲娴佺▼
+
+```
+1. 鍓嶇璋冪敤 /common/upload 涓婁紶鏂囦欢 鈫� 杩斿洖 storageBlobId
+2. 鍓嶇璋冪敤 /knowledgeBase/file/save 鍏宠仈鏂囦欢鍒扮煡璇嗗簱
+3. 鍚庣鍒涘缓 KnowledgeBaseVector 璁板綍锛堢姸鎬侊細寰呭鐞嗭級
+4. 鍚庣寮傛璋冪敤 KnowledgeRagService.processVectorAsync()
+ 鈹溾攢鈹� 鏇存柊鐘舵�佷负"澶勭悊涓�"
+ 鈹溾攢鈹� 鎻愬彇鏂囦欢鍐呭锛堟敮鎸佸绉嶆牸寮忥級
+ 鈹溾攢鈹� 鑷姩妫�娴嬫枃浠剁紪鐮侊紙UTF-8/GBK锛�
+ 鈹溾攢鈹� 鏂囨湰鍒囩墖锛堝ぇ鏂囦欢鎴栭暱鍐呭鎵嶅垏鐗囷級
+ 鈹溾攢鈹� 鐢熸垚 Embedding 鍚戦噺
+ 鈹溾攢鈹� 瀛樺偍鍒� Pinecone
+ 鈹斺攢鈹� 鏇存柊鐘舵�佷负"瀹屾垚"鎴�"澶辫触"
+```
+
+### 6.2 鐭ヨ瘑搴撻棶绛旀祦绋�
+
+```
+1. 鐢ㄦ埛閫夋嫨鐭ヨ瘑搴擄紝杈撳叆闂
+2. 鍓嶇璋冪敤 /ai/knowledge/chat锛堟祦寮忔帴鍙o級
+3. 鍚庣澶勭悊锛�
+ 鈹溾攢鈹� 鏋勫缓鍛藉悕绌洪棿锛歬b-{knowledgeBaseId}
+ 鈹溾攢鈹� 璋冪敤 Embedding 妯″瀷鐢熸垚闂鍚戦噺
+ 鈹溾攢鈹� 浠� Pinecone 妫�绱㈢浉鍏冲唴瀹癸紙minScore=0.7, maxResults=5锛�
+ 鈹溾攢鈹� 鏋勫缓涓婁笅鏂� Prompt
+ 鈹溾攢鈹� 璋冪敤 LLM 鐢熸垚鍥炵瓟
+ 鈹斺攢鈹� 娴佸紡杩斿洖缁撴灉
+```
+
+---
+
+## 涓冦�佹敞鎰忎簨椤�
+
+1. **Pinecone 鍛藉悕绌洪棿**锛氫笉鑳戒娇鐢� `__default__`锛屽繀椤讳娇鐢ㄨ嚜瀹氫箟鍛藉悕绌洪棿
+2. **鏂囦欢缂栫爜**锛氳嚜鍔ㄦ娴� UTF-8/GBK锛岄伩鍏嶄贡鐮�
+3. **鍒囩墖绛栫暐**锛�
+ - 鏂囦欢 > 80MB 鎴栧唴瀹� > 8000 瀛楃鏃舵墠鍒囩墖
+ - 鍒囩墖澶у皬 500 瀛楃锛岄噸鍙� 100 瀛楃
+ - 浼樺厛鍦ㄥ彞瀛愯竟鐣屽垏鍒�
+4. **Embedding 闄愬埗**锛氶樋閲屼簯 DashScope 闄愬埗鍗曟杈撳叆鏈�澶� 8192 瀛楃
+5. **鍚戦噺鍒犻櫎**锛氫娇鐢� Pinecone 鍘熺敓瀹㈡埛绔紝閫氳繃 metadata filter 鍒犻櫎
+6. **寮傛澶勭悊**锛氬悜閲忓寲浣跨敤 `@Async` 寮傛鎵ц锛岄伩鍏嶉樆濉炴帴鍙�
+
+---
+
+## 鍏�佹枃浠舵竻鍗�
+
+### 鍚庣鏂囦欢
+```
+src/main/java/com/ruoyi/
+鈹溾攢鈹� approve/
+鈹� 鈹溾攢鈹� controller/
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseController.java
+鈹� 鈹溾攢鈹� pojo/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBase.java
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseVector.java
+鈹� 鈹溾攢鈹� service/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseService.java
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseVectorService.java
+鈹� 鈹� 鈹斺攢鈹� impl/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseServiceImpl.java
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseVectorServiceImpl.java
+鈹� 鈹溾攢鈹� mapper/
+鈹� 鈹� 鈹溾攢鈹� KnowledgeBaseMapper.java
+鈹� 鈹� 鈹斺攢鈹� KnowledgeBaseVectorMapper.java
+鈹� 鈹斺攢鈹� dto/
+鈹� 鈹斺攢鈹� KnowledgeBaseVectorVO.java
+鈹斺攢鈹� ai/
+ 鈹溾攢鈹� config/
+ 鈹� 鈹溾攢鈹� EmbeddingStoreConfig.java
+ 鈹� 鈹斺攢鈹� XiaozhiAgentConfig.java
+ 鈹溾攢鈹� controller/
+ 鈹� 鈹斺攢鈹� KnowledgeChatController.java
+ 鈹溾攢鈹� assistant/
+ 鈹� 鈹斺攢鈹� KnowledgeChatAgent.java
+ 鈹溾攢鈹� service/
+ 鈹� 鈹溾攢鈹� KnowledgeRagService.java
+ 鈹� 鈹斺攢鈹� impl/
+ 鈹� 鈹斺攢鈹� KnowledgeRagServiceImpl.java
+ 鈹斺攢鈹� dto/
+ 鈹斺攢鈹� KnowledgeChatRequest.java
+```
+
+### 鍓嶇鏂囦欢
+```
+src/views/knowledge/
+鈹溾攢鈹� index.vue # 鐭ヨ瘑搴撳垪琛�
+鈹溾攢鈹� form.vue # 鏂板/缂栬緫琛ㄥ崟
+鈹溾攢鈹� files.vue # 鏂囦欢绠$悊
+鈹斺攢鈹� chat.vue # 鐭ヨ瘑搴撻棶绛�
+
+src/api/knowledge.js # API灏佽
+```
\ No newline at end of file
diff --git a/src/api/collaborativeApproval/knowledgeBase.js b/src/api/collaborativeApproval/knowledgeBase.js
index b195525..8ad24dd 100644
--- a/src/api/collaborativeApproval/knowledgeBase.js
+++ b/src/api/collaborativeApproval/knowledgeBase.js
@@ -53,3 +53,55 @@
});
}
+// 鑾峰彇鐭ヨ瘑搴撴枃浠跺悜閲忓寲鐘舵��
+export function getVectorStatus(knowledgeBaseId) {
+ return request({
+ url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
+ method: "get",
+ });
+}
+
+// 閲嶆柊鍚戦噺鍖栨枃浠�
+export function reprocessVector(vectorId) {
+ return request({
+ url: `/knowledgeBase/vector/reprocess/${vectorId}`,
+ method: "post",
+ });
+}
+
+// 淇濆瓨鐭ヨ瘑搴撴枃浠跺叧鑱�
+export function saveKnowledgeBaseFiles(data) {
+ return request({
+ url: "/knowledgeBase/file/save",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鐭ヨ瘑搴撴枃浠�
+export function deleteKnowledgeBaseFiles(vectorIds) {
+ return request({
+ url: "/knowledgeBase/file/delete",
+ method: "delete",
+ data: vectorIds,
+ });
+}
+
+// 鑾峰彇鐭ヨ瘑搴撳垪琛�(闂瓟鐢�)
+export function getKnowledgeBaseListForChat() {
+ return request({
+ url: "/ai/knowledge/list",
+ method: "get",
+ });
+}
+
+// 鐭ヨ瘑搴撻棶绛�(娴佸紡)
+export async function knowledgeChat(data) {
+ const response = await fetch("/api/ai/knowledge/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ });
+ return response.body;
+}
+
diff --git a/src/views/collaborativeApproval/knowledgeBase/index.vue b/src/views/collaborativeApproval/knowledgeBase/index.vue
index 43fee33..8d625cf 100644
--- a/src/views/collaborativeApproval/knowledgeBase/index.vue
+++ b/src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -225,18 +225,141 @@
</div>
</div>
</FormDialog>
+
+ <!-- 鏂囦欢绠$悊寮圭獥 -->
+ <FormDialog
+ v-model="filesDialogVisible"
+ title="鏂囦欢绠$悊"
+ :width="'900px'"
+ @close="closeFilesDialog"
+ @confirm="closeFilesDialog"
+ @cancel="closeFilesDialog"
+ >
+ <div class="file-manager">
+ <!-- 鏂囦欢涓婁紶 -->
+ <div class="upload-section">
+ <el-upload
+ :action="uploadUrl"
+ :headers="uploadHeaders"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :before-upload="beforeUpload"
+ multiple
+ :show-file-list="false"
+ accept=".txt,.md,.docx,.xlsx,.xls,.pdf"
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ <el-button
+ type="success"
+ @click="saveFiles"
+ :disabled="uploadedBlobIds.length === 0"
+ :loading="savingFiles"
+ style="margin-left: 10px"
+ >
+ 淇濆瓨鏂囦欢鍏宠仈
+ </el-button>
+ </div>
+
+ <!-- 鏂囦欢鍒楄〃涓庡悜閲忓寲鐘舵�� -->
+ <el-table :data="fileList" style="margin-top: 20px" border>
+ <el-table-column prop="fileName" label="鏂囦欢鍚�" show-overflow-tooltip />
+ <el-table-column prop="fileType" label="鏂囦欢绫诲瀷" width="100" />
+ <el-table-column label="鍚戦噺鍖栫姸鎬�" width="120">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.vectorStatus)">
+ {{ getStatusText(row.vectorStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="chunkCount" label="鍒囩墖鏁�" width="100" align="center" />
+ <el-table-column prop="createTime" label="涓婁紶鏃堕棿" width="180" />
+ <el-table-column label="鎿嶄綔" width="150" align="center">
+ <template #default="{ row }">
+ <el-button
+ v-if="row.vectorStatus === 3"
+ type="text"
+ @click="reprocessFile(row)"
+ >
+ 閲嶆柊澶勭悊
+ </el-button>
+ <el-button type="text" @click="deleteFile(row)" style="color: #f56c6c">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </FormDialog>
+
+ <!-- 鐭ヨ瘑搴撻棶绛斿脊绐� -->
+ <FormDialog
+ v-model="chatDialogVisible"
+ title="鐭ヨ瘑搴撻棶绛�"
+ :width="'800px'"
+ @close="closeChatDialog"
+ @confirm="closeChatDialog"
+ @cancel="closeChatDialog"
+ >
+ <div class="knowledge-chat">
+ <div class="chat-header">
+ <el-tag type="success">褰撳墠鐭ヨ瘑搴�: {{ currentKnowledgeBase?.title }}</el-tag>
+ </div>
+
+ <!-- 瀵硅瘽鍖哄煙 -->
+ <div class="chat-messages" ref="chatMessagesRef">
+ <div
+ v-for="(msg, index) in messages"
+ :key="index"
+ :class="['message', msg.role]"
+ >
+ <div class="message-role">{{ msg.role === 'user' ? '鎴�' : 'AI鍔╂墜' }}</div>
+ <div class="message-content">{{ msg.content }}</div>
+ </div>
+ <div v-if="chatLoading" class="message assistant">
+ <div class="message-role">AI鍔╂墜</div>
+ <div class="message-content typing">姝e湪鎬濊�冧腑...</div>
+ </div>
+ </div>
+
+ <!-- 杈撳叆妗� -->
+ <div class="chat-input">
+ <el-input
+ v-model="inputQuestion"
+ placeholder="璇疯緭鍏ラ棶棰橈紝鎸夊洖杞﹀彂閫�"
+ @keyup.enter="sendMessage"
+ :disabled="chatLoading"
+ >
+ <template #append>
+ <el-button @click="sendMessage" :loading="chatLoading">鍙戦��</el-button>
+ </template>
+ </el-input>
+ </div>
+ </div>
+ </FormDialog>
</div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
-import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch } from "vue";
+import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDialog from '@/components/Dialog/FormDialog.vue';
-import { listKnowledgeBase, delKnowledgeBase,addKnowledgeBase,updateKnowledgeBase } from "@/api/collaborativeApproval/knowledgeBase.js";
+import {
+ listKnowledgeBase,
+ delKnowledgeBase,
+ addKnowledgeBase,
+ updateKnowledgeBase,
+ getVectorStatus,
+ reprocessVector,
+ saveKnowledgeBaseFiles,
+ deleteKnowledgeBaseFiles,
+ knowledgeChat
+} from "@/api/collaborativeApproval/knowledgeBase.js";
import useUserStore from '@/store/modules/user';
import { userListNoPageByTenantId } from '@/api/system/user.js';
+import { getToken } from "@/utils/auth";
// 琛ㄥ崟楠岃瘉瑙勫垯
const rules = {
@@ -283,7 +406,17 @@
dialogTitle: "",
dialogType: "add",
viewDialogVisible: false,
- currentKnowledge: {}
+ currentKnowledge: {},
+ filesDialogVisible: false,
+ currentKnowledgeBase: null,
+ fileList: [],
+ uploadedBlobIds: [],
+ savingFiles: false,
+ chatDialogVisible: false,
+ messages: [],
+ inputQuestion: "",
+ chatLoading: false,
+ memoryId: ""
});
const {
@@ -297,7 +430,17 @@
dialogTitle,
dialogType,
viewDialogVisible,
- currentKnowledge
+ currentKnowledge,
+ filesDialogVisible,
+ currentKnowledgeBase,
+ fileList,
+ uploadedBlobIds,
+ savingFiles,
+ chatDialogVisible,
+ messages,
+ inputQuestion,
+ chatLoading,
+ memoryId
} = toRefs(data);
// 琛ㄥ崟寮曠敤
@@ -305,6 +448,12 @@
// 鐢ㄦ埛鐩稿叧
const userStore = useUserStore();
const userList = ref([]);
+// 鑱婂ぉ娑堟伅瀹瑰櫒寮曠敤
+const chatMessagesRef = ref();
+
+// 鏂囦欢涓婁紶鐩稿叧
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
+const uploadHeaders = { Authorization: "Bearer " + getToken() };
// 琛ㄦ牸鍒楅厤缃�
const tableColumn = ref([
@@ -352,6 +501,18 @@
}
},
{
+ label: "鏂囦欢鏁伴噺",
+ prop: "fileCount",
+ width: 100,
+ align: "center"
+ },
+ {
+ label: "鍒囩墖鏁伴噺",
+ prop: "totalChunkCount",
+ width: 100,
+ align: "center"
+ },
+ {
label: "浣跨敤娆℃暟",
prop: "usageCount",
width: 100,
@@ -379,6 +540,20 @@
type: "text",
clickFun: (row) => {
openForm("edit", row);
+ }
+ },
+ {
+ name: "鏂囦欢",
+ type: "text",
+ clickFun: (row) => {
+ openFilesDialog(row);
+ }
+ },
+ {
+ name: "闂瓟",
+ type: "text",
+ clickFun: (row) => {
+ openChatDialog(row);
}
},
{
@@ -680,6 +855,260 @@
const handleExport = () => {
proxy.download('/knowledgeBase/export', { ...searchForm.value }, '鐭ヨ瘑搴�.xlsx')
}
+
+// ============ 鏂囦欢绠$悊鐩稿叧 ============
+
+// 鎵撳紑鏂囦欢绠$悊寮圭獥
+const openFilesDialog = (row) => {
+ currentKnowledgeBase.value = row;
+ filesDialogVisible.value = true;
+ loadFileList();
+};
+
+// 鍔犺浇鏂囦欢鍒楄〃
+const loadFileList = async () => {
+ if (!currentKnowledgeBase.value?.id) return;
+
+ try {
+ const res = await getVectorStatus(currentKnowledgeBase.value.id);
+ fileList.value = res.data || [];
+ } catch (error) {
+ console.error("鍔犺浇鏂囦欢鍒楄〃澶辫触:", error);
+ ElMessage.error("鍔犺浇鏂囦欢鍒楄〃澶辫触");
+ }
+};
+
+// 涓婁紶鍓嶆牎楠�
+const beforeUpload = (file) => {
+ const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf'];
+ const fileName = file.name.toLowerCase();
+ const isAllowed = allowedTypes.some(type => fileName.endsWith(type));
+
+ if (!isAllowed) {
+ ElMessage.error('鍙敮鎸� txt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+ return false;
+ }
+
+ const isLt50M = file.size / 1024 / 1024 < 50;
+ if (!isLt50M) {
+ ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 50MB');
+ return false;
+ }
+
+ return true;
+};
+
+// 涓婁紶鎴愬姛
+const handleUploadSuccess = (response, file) => {
+ if (response.code === 200) {
+ uploadedBlobIds.value.push(response.data.id);
+ ElMessage.success(`鏂囦欢 ${file.name} 涓婁紶鎴愬姛`);
+ } else {
+ ElMessage.error(response.msg || "涓婁紶澶辫触");
+ }
+};
+
+// 涓婁紶澶辫触
+const handleUploadError = (error, file) => {
+ ElMessage.error(`鏂囦欢 ${file.name} 涓婁紶澶辫触`);
+};
+
+// 淇濆瓨鏂囦欢鍏宠仈
+const saveFiles = async () => {
+ if (uploadedBlobIds.value.length === 0) {
+ ElMessage.warning("璇峰厛涓婁紶鏂囦欢");
+ return;
+ }
+
+ savingFiles.value = true;
+ try {
+ await saveKnowledgeBaseFiles({
+ knowledgeBaseId: currentKnowledgeBase.value.id,
+ storageBlobIds: uploadedBlobIds.value
+ });
+
+ ElMessage.success("鏂囦欢鍏宠仈淇濆瓨鎴愬姛锛屾鍦ㄥ悗鍙板鐞嗗悜閲忓寲");
+ uploadedBlobIds.value = [];
+
+ // 寤惰繜鍒锋柊鏂囦欢鍒楄〃锛岀粰鍚庡彴澶勭悊鏃堕棿
+ setTimeout(() => {
+ loadFileList();
+ }, 1000);
+ } catch (error) {
+ console.error("淇濆瓨鏂囦欢鍏宠仈澶辫触:", error);
+ ElMessage.error("淇濆瓨鏂囦欢鍏宠仈澶辫触");
+ } finally {
+ savingFiles.value = false;
+ }
+};
+
+// 閲嶆柊澶勭悊鍚戦噺鍖栫殑鏂囦欢
+const reprocessFile = async (row) => {
+ try {
+ await reprocessVector(row.id);
+ ElMessage.success("宸查噸鏂版彁浜ゅ悜閲忓寲浠诲姟");
+ // 寤惰繜鍒锋柊
+ setTimeout(() => {
+ loadFileList();
+ }, 1000);
+ } catch (error) {
+ console.error("閲嶆柊澶勭悊澶辫触:", error);
+ ElMessage.error("閲嶆柊澶勭悊澶辫触");
+ }
+};
+
+// 鍒犻櫎鏂囦欢
+const deleteFile = async (row) => {
+ try {
+ await ElMessageBox.confirm(
+ "纭畾瑕佸垹闄よ鏂囦欢鍚楋紵鍒犻櫎鍚庡皢鏃犳硶鎭㈠鍚戦噺鏁版嵁",
+ "鍒犻櫎纭",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }
+ );
+
+ await deleteKnowledgeBaseFiles([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ loadFileList();
+ } catch (error) {
+ if (error !== 'cancel') {
+ console.error("鍒犻櫎鏂囦欢澶辫触:", error);
+ ElMessage.error("鍒犻櫎鏂囦欢澶辫触");
+ }
+ }
+};
+
+// 鐘舵�佹枃鏈槧灏�
+const getStatusText = (status) => {
+ const map = {
+ 0: '寰呭鐞�',
+ 1: '澶勭悊涓�',
+ 2: '宸插畬鎴�',
+ 3: '澶辫触'
+ };
+ return map[status] || '鏈煡';
+};
+
+// 鐘舵�佹爣绛剧被鍨嬫槧灏�
+const getStatusType = (status) => {
+ const map = {
+ 0: 'info',
+ 1: 'warning',
+ 2: 'success',
+ 3: 'danger'
+ };
+ return map[status] || 'info';
+};
+
+// 鍏抽棴鏂囦欢绠$悊寮圭獥
+const closeFilesDialog = () => {
+ filesDialogVisible.value = false;
+ currentKnowledgeBase.value = null;
+ fileList.value = [];
+ uploadedBlobIds.value = [];
+ getList(); // 鍒锋柊涓诲垪琛紝鏇存柊鏂囦欢鏁伴噺
+};
+
+// ============ 鐭ヨ瘑搴撻棶绛旂浉鍏� ============
+
+// 鎵撳紑闂瓟寮圭獥
+const openChatDialog = (row) => {
+ currentKnowledgeBase.value = row;
+ chatDialogVisible.value = true;
+ memoryId.value = crypto.randomUUID();
+ messages.value = [];
+ inputQuestion.value = "";
+};
+
+// 鍙戦�佹秷鎭�
+const sendMessage = async () => {
+ if (!inputQuestion.value.trim()) {
+ ElMessage.warning("璇疯緭鍏ラ棶棰�");
+ return;
+ }
+
+ const question = inputQuestion.value.trim();
+
+ // 娣诲姞鐢ㄦ埛娑堟伅
+ messages.value.push({
+ role: 'user',
+ content: question
+ });
+
+ inputQuestion.value = "";
+ chatLoading.value = true;
+
+ // 婊氬姩鍒板簳閮�
+ await nextTick();
+ scrollToBottom();
+
+ try {
+ // 娴佸紡璇锋眰
+ const response = await fetch('/api/ai/knowledge/chat', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer ' + getToken()
+ },
+ body: JSON.stringify({
+ knowledgeBaseId: currentKnowledgeBase.value.id,
+ memoryId: memoryId.value,
+ question: question
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('璇锋眰澶辫触');
+ }
+
+ // 澶勭悊SSE娴佸紡鍝嶅簲
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let aiContent = '';
+
+ messages.value.push({ role: 'assistant', content: '' });
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const text = decoder.decode(value);
+ aiContent += text;
+ messages.value[messages.value.length - 1].content = aiContent;
+
+ // 婊氬姩鍒板簳閮�
+ await nextTick();
+ scrollToBottom();
+ }
+ } catch (error) {
+ console.error("闂瓟璇锋眰澶辫触:", error);
+ ElMessage.error("闂瓟璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯");
+ messages.value.push({
+ role: 'assistant',
+ content: '鎶辨瓑锛屽彂鐢熶簡閿欒锛岃绋嶅悗閲嶈瘯'
+ });
+ } finally {
+ chatLoading.value = false;
+ }
+};
+
+// 婊氬姩鍒板簳閮�
+const scrollToBottom = () => {
+ if (chatMessagesRef.value) {
+ chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight;
+ }
+};
+
+// 鍏抽棴闂瓟寮圭獥
+const closeChatDialog = () => {
+ chatDialogVisible.value = false;
+ currentKnowledgeBase.value = null;
+ messages.value = [];
+ inputQuestion.value = "";
+};
</script>
<style scoped>
@@ -755,4 +1184,92 @@
font-size: 14px;
color: #909399;
}
+
+/* 鏂囦欢绠$悊鏍峰紡 */
+.file-manager {
+ padding: 20px 0;
+}
+
+.upload-section {
+ display: flex;
+ align-items: center;
+}
+
+/* 鐭ヨ瘑搴撻棶绛旀牱寮� */
+.knowledge-chat {
+ display: flex;
+ flex-direction: column;
+ height: 500px;
+}
+
+.chat-header {
+ margin-bottom: 16px;
+}
+
+.chat-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ margin-bottom: 16px;
+}
+
+.message {
+ margin-bottom: 16px;
+ max-width: 80%;
+}
+
+.message.user {
+ margin-left: auto;
+ text-align: right;
+}
+
+.message.assistant {
+ margin-right: auto;
+}
+
+.message-role {
+ font-size: 12px;
+ color: #909399;
+ margin-bottom: 4px;
+}
+
+.message-content {
+ display: inline-block;
+ padding: 10px 14px;
+ border-radius: 8px;
+ line-height: 1.6;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+}
+
+.message.user .message-content {
+ background: #409eff;
+ color: white;
+}
+
+.message.assistant .message-content {
+ background: white;
+ color: #303133;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.typing {
+ animation: typing 1.5s infinite;
+}
+
+@keyframes typing {
+ 0%, 50%, 100% {
+ opacity: 1;
+ }
+ 25%, 75% {
+ opacity: 0.5;
+ }
+}
+
+.chat-input {
+ margin-top: auto;
+}
+
</style>
--
Gitblit v1.9.3