Quellcode durchsuchen

qdrant向量知识库

yh vor 2 Tagen
Ursprung
Commit
9ddaa80663
34 geänderte Dateien mit 639 neuen und 1230 gelöschten Zeilen
  1. 6 2
      fs-ai-api/pom.xml
  2. 1 3
      fs-ai-api/src/main/java/com/fs/FsAiApiApplication.java
  3. 0 4
      fs-ai-api/src/main/java/com/fs/ai/rag/AiRagProperties.java
  4. 0 40
      fs-ai-api/src/main/java/com/fs/ai/rag/KnowledgeVectorService.java
  5. 0 154
      fs-ai-api/src/main/java/com/fs/ai/rag/controller/AiRagController.java
  6. 31 0
      fs-ai-api/src/main/java/com/fs/ai/rag/controller/EmbeddingController.java
  7. 0 22
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/CollectionCreateReq.java
  8. 0 18
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/CollectionDeleteReq.java
  9. 0 18
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/CollectionListReq.java
  10. 0 14
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/CreateDatabaseReq.java
  11. 0 12
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/CreateTenantReq.java
  12. 0 16
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/DeleteReq.java
  13. 0 20
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/IndexReq.java
  14. 0 18
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/QueryReq.java
  15. 0 26
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/RecordDeleteReq.java
  16. 0 30
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/RecordQueryReq.java
  17. 0 30
      fs-ai-api/src/main/java/com/fs/ai/rag/dto/RecordUpsertReq.java
  18. 9 1
      fs-ai-api/src/main/java/com/fs/ai/rag/service/VolcanoEmbeddingService.java
  19. 0 732
      fs-ai-api/src/main/java/com/fs/ai/rag/service/impl/KnowledgeVectorServiceImpl.java
  20. 84 0
      fs-ai-api/src/main/java/com/fs/ai/rag/service/impl/VolcanoEmbeddingServiceImpl.java
  21. 12 8
      fs-ai-api/src/main/resources/application.yml
  22. 85 0
      fs-company/src/main/java/com/fs/company/controller/knowledge/AiKnowledgeBaseController.java
  23. 6 16
      fs-company/src/main/java/com/fs/company/controller/knowledge/CompanyKnowledgeBaseController.java
  24. 78 0
      fs-service/src/main/java/com/fs/company/domain/AiKnowledgeBase.java
  25. 2 0
      fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeBase.java
  26. 12 0
      fs-service/src/main/java/com/fs/company/dto/CompanyKnowledgeBaseDto.java
  27. 18 0
      fs-service/src/main/java/com/fs/company/mapper/AiKnowledgeBaseMapper.java
  28. 6 2
      fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeBaseMapper.java
  29. 19 0
      fs-service/src/main/java/com/fs/company/service/AiKnowledgeBaseService.java
  30. 3 7
      fs-service/src/main/java/com/fs/company/service/ICompanyKnowledgeBaseService.java
  31. 48 0
      fs-service/src/main/java/com/fs/company/service/impl/AiKnowledgeBaseServiceImpl.java
  32. 171 20
      fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java
  33. 18 0
      fs-service/src/main/resources/mapper/company/AiKnowledgeBaseMapper.xml
  34. 30 17
      fs-service/src/main/resources/mapper/company/CompanyKnowledgeBaseMapper.xml

+ 6 - 2
fs-ai-api/pom.xml

@@ -10,7 +10,7 @@
     <modelVersion>4.0.0</modelVersion>
 
     <artifactId>fs-ai-api</artifactId>
-    <description>知识库向量服务:Embedding + Chroma v2</description>
+    <description>知识库向量服务:Embedding + Qdrant</description>
 
     <dependencies>
 
@@ -61,7 +61,11 @@
             <artifactId>fs-service</artifactId>
         </dependency>
 
-
+        <dependency>
+            <groupId>io.qdrant</groupId>
+            <artifactId>client</artifactId>
+            <version>1.17.0</version>
+        </dependency>
 
     </dependencies>
 

+ 1 - 3
fs-ai-api/src/main/java/com/fs/FsAiApiApplication.java

@@ -1,11 +1,9 @@
 package com.fs;
 
-import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.scheduling.annotation.EnableAsync;
-import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
 /**
@@ -15,7 +13,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableAsync
-@EnableScheduling
+//@EnableScheduling
 public class FsAiApiApplication {
 
     public static void main(String[] args) {

+ 0 - 4
fs-ai-api/src/main/java/com/fs/ai/rag/AiRagProperties.java

@@ -12,10 +12,6 @@ import java.util.Map;
 @ConfigurationProperties(prefix = "ai.rag")
 public class AiRagProperties {
     private boolean enabled = true;
-    private String chromaBaseUrl = "https://saaschroma.ylrzcloud.com/api/v2";
-    private String chromaToken = "";
-    private String chromaTenantId = "";
-    private String chromaDatabase = "default_database";
     private String embeddingUrl = "";
     private String embeddingApiKey = "";
     private String embeddingModel = "text-embedding-3-small";

+ 0 - 40
fs-ai-api/src/main/java/com/fs/ai/rag/KnowledgeVectorService.java

@@ -1,40 +0,0 @@
-package com.fs.ai.rag;
-
-import com.fs.ai.rag.dto.DeleteReq;
-import com.fs.ai.rag.dto.CreateDatabaseReq;
-import com.fs.ai.rag.dto.CreateTenantReq;
-import com.fs.ai.rag.dto.CollectionCreateReq;
-import com.fs.ai.rag.dto.CollectionDeleteReq;
-import com.fs.ai.rag.dto.CollectionListReq;
-import com.fs.ai.rag.dto.IndexReq;
-import com.fs.ai.rag.dto.QueryReq;
-import com.fs.ai.rag.dto.RecordDeleteReq;
-import com.fs.ai.rag.dto.RecordQueryReq;
-import com.fs.ai.rag.dto.RecordUpsertReq;
-
-import java.util.List;
-import java.util.Map;
-
-public interface KnowledgeVectorService {
-    void index(IndexReq req);
-
-    List<String> search(QueryReq req);
-
-    void deleteByDocId(DeleteReq req);
-
-    Map<String, Object> createTenant(CreateTenantReq req);
-
-    Map<String, Object> createDatabase(CreateDatabaseReq req);
-
-    Map<String, Object> createCollection(CollectionCreateReq req);
-
-    Object listCollections(CollectionListReq req);
-
-    void deleteCollection(CollectionDeleteReq req);
-
-    Map<String, Object> upsertRecords(RecordUpsertReq req);
-
-    Map<String, Object> queryRecords(RecordQueryReq req);
-
-    Map<String, Object> deleteRecords(RecordDeleteReq req);
-}

+ 0 - 154
fs-ai-api/src/main/java/com/fs/ai/rag/controller/AiRagController.java

@@ -1,154 +0,0 @@
-package com.fs.ai.rag.controller;
-
-import com.fs.ai.rag.KnowledgeVectorService;
-import com.fs.ai.rag.dto.*;
-import com.fs.common.core.domain.R;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-@Api(tags = "知识库向量(Chroma v2)")
-@RestController
-@RequestMapping("/kb")
-@RequiredArgsConstructor
-public class AiRagController {
-
-    private final KnowledgeVectorService knowledgeVectorService;
-
-    @ApiOperation("入库(分片 + Embedding + upsert)")
-    @PostMapping("/index")
-    public R index(@RequestBody Object req) {
-        for (IndexReq one : toIndexReqList(req)) {
-            knowledgeVectorService.index(one);
-        }
-        return R.ok();
-    }
-
-    @ApiOperation("检索")
-    @PostMapping("/query")
-    public R query(@RequestBody QueryReq req) {
-        return R.ok().put("data", knowledgeVectorService.search(req));
-    }
-
-    @ApiOperation("按 doc_id 删除")
-    @PostMapping("/delete")
-    public R delete(@RequestBody DeleteReq req) {
-        knowledgeVectorService.deleteByDocId(req);
-        return R.ok();
-    }
-
-    @ApiOperation("创建 Chroma Tenant")
-    @PostMapping("/tenant/create")
-    public R createTenant(@RequestBody CreateTenantReq req) {
-        return R.ok().put("data", knowledgeVectorService.createTenant(req));
-    }
-
-    @ApiOperation("创建 Chroma Database")
-    @PostMapping("/database/create")
-    public R createDatabase(@RequestBody CreateDatabaseReq req) {
-        return R.ok().put("data", knowledgeVectorService.createDatabase(req));
-    }
-
-    @ApiOperation("创建集合(可指定 tenant_id / database)")
-    @PostMapping("/collection/create")
-    public R createCollection(@RequestBody CollectionCreateReq req) {
-        return R.ok().put("data", knowledgeVectorService.createCollection(req));
-    }
-
-    @ApiOperation("集合列表(可指定 tenant_id / database)")
-    @PostMapping("/collection/list")
-    public R listCollections(@RequestBody(required = false) CollectionListReq req) {
-        return R.ok().put("data", knowledgeVectorService.listCollections(req == null ? new CollectionListReq() : req));
-    }
-
-    @ApiOperation("删除集合(collection_id 或 collection_name)")
-    @PostMapping("/collection/delete")
-    public R deleteCollection(@RequestBody CollectionDeleteReq req) {
-        knowledgeVectorService.deleteCollection(req);
-        return R.ok();
-    }
-
-    @ApiOperation("记录 upsert(collection_id 或 collection_name)")
-    @PostMapping("/record/upsert")
-    public R upsertRecords(@RequestBody RecordUpsertReq req) {
-        return R.ok().put("data", knowledgeVectorService.upsertRecords(req));
-    }
-
-    @ApiOperation("记录 query(collection_id 或 collection_name)")
-    @PostMapping("/record/query")
-    public R queryRecords(@RequestBody RecordQueryReq req) {
-        return R.ok().put("data", knowledgeVectorService.queryRecords(req));
-    }
-
-    @ApiOperation("记录 delete(collection_id 或 collection_name)")
-    @PostMapping("/record/delete")
-    public R deleteRecords(@RequestBody RecordDeleteReq req) {
-        return R.ok().put("data", knowledgeVectorService.deleteRecords(req));
-    }
-
-    @SuppressWarnings("unchecked")
-    private List<IndexReq> toIndexReqList(Object req) {
-        if (req instanceof Map) {
-            Object documents = ((Map<String, Object>) req).get("documents");
-            if (documents instanceof List) {
-                List<IndexReq> out = new ArrayList<>();
-                for (Object item : (List<?>) documents) {
-                    if (!(item instanceof Map)) {
-                        continue;
-                    }
-                    out.add(convertDocument((Map<String, Object>) item));
-                }
-                return out;
-            }
-        }
-        // 兼容旧格式:直接 IndexReq
-        if (req instanceof IndexReq) {
-            return java.util.Collections.singletonList((IndexReq) req);
-        }
-        // 兜底:Spring 默认把 JSON object 反序列化成 LinkedHashMap
-        return java.util.Collections.singletonList(convertDocument((Map<String, Object>) req));
-    }
-
-    @SuppressWarnings("unchecked")
-    private IndexReq convertDocument(Map<String, Object> doc) {
-        IndexReq req = new IndexReq();
-        req.setDocId(toStr(doc.get("doc_id")));
-        req.setTenantCode(toStr(doc.get("tenantCode")));
-        if (req.getTenantCode() == null) {
-            req.setTenantCode(toStr(doc.get("tenant_code")));
-        }
-        req.setTenantId(toStr(doc.get("tenant_id")));
-        req.setText(toStr(doc.get("content")));
-
-        String collectionName = toStr(doc.get("collection_name"));
-        if (collectionName == null) {
-            Map<String, Object> metadata = doc.get("metadata") instanceof Map ? (Map<String, Object>) doc.get("metadata") : null;
-            String companyId = metadata == null ? null : toStr(metadata.get("company_id"));
-            String bizType = metadata == null ? null : toStr(metadata.get("biz_type"));
-            if (companyId != null && bizType != null) {
-                collectionName = "company_" + companyId + "_" + bizType;
-            } else {
-                collectionName = "default_collection";
-            }
-        }
-        req.setCollectionName(collectionName);
-        return req;
-    }
-
-    private String toStr(Object value) {
-        if (value == null) {
-            return null;
-        }
-        String s = Objects.toString(value, null);
-        return (s == null || s.trim().isEmpty()) ? null : s.trim();
-    }
-}

+ 31 - 0
fs-ai-api/src/main/java/com/fs/ai/rag/controller/EmbeddingController.java

@@ -0,0 +1,31 @@
+package com.fs.ai.rag.controller;
+
+import com.fs.ai.rag.service.VolcanoEmbeddingService;
+import com.fs.common.core.domain.R;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/ai/embedding")
+public class EmbeddingController {
+
+    private final VolcanoEmbeddingService volcanoEmbeddingService;
+
+    public EmbeddingController(VolcanoEmbeddingService volcanoEmbeddingService) {
+        this.volcanoEmbeddingService = volcanoEmbeddingService;
+    }
+
+    // 对外提供 HTTP 接口
+    @PostMapping("/create")
+    public R createEmbedding(@RequestBody Map<String, Object> request) {
+        String text = request.get("text").toString();
+        List<Float> embedding = volcanoEmbeddingService.createEmbedding(text);
+
+        return R.ok().put("data", embedding);
+    }
+}

+ 0 - 22
fs-ai-api/src/main/java/com/fs/ai/rag/dto/CollectionCreateReq.java

@@ -1,22 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.util.Map;
-
-@Data
-@ApiModel("创建集合请求")
-public class CollectionCreateReq {
-    @ApiModelProperty("Tenant UUID,可空(为空使用默认配置)")
-    private String tenantId;
-    @ApiModelProperty("数据库名称,可空(为空使用默认配置)")
-    private String database;
-    @ApiModelProperty("集合名")
-    private String name;
-    @ApiModelProperty("是否存在即返回")
-    private Boolean getOrCreate = true;
-    @ApiModelProperty("集合元数据")
-    private Map<String, Object> metadata;
-}

+ 0 - 18
fs-ai-api/src/main/java/com/fs/ai/rag/dto/CollectionDeleteReq.java

@@ -1,18 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-@Data
-@ApiModel("删除集合请求")
-public class CollectionDeleteReq {
-    @ApiModelProperty("Tenant UUID,可空(为空使用默认配置)")
-    private String tenantId;
-    @ApiModelProperty("数据库名称,可空(为空使用默认配置)")
-    private String database;
-    @ApiModelProperty("集合ID(优先)")
-    private String collectionId;
-    @ApiModelProperty("集合名称(当未传 collectionId 时使用)")
-    private String collectionName;
-}

+ 0 - 18
fs-ai-api/src/main/java/com/fs/ai/rag/dto/CollectionListReq.java

@@ -1,18 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-@Data
-@ApiModel("集合列表请求")
-public class CollectionListReq {
-    @ApiModelProperty("Tenant UUID,可空(为空使用默认配置)")
-    private String tenantId;
-    @ApiModelProperty("数据库名称,可空(为空使用默认配置)")
-    private String database;
-    @ApiModelProperty("分页大小")
-    private Integer limit = 100;
-    @ApiModelProperty("分页偏移")
-    private Integer offset = 0;
-}

+ 0 - 14
fs-ai-api/src/main/java/com/fs/ai/rag/dto/CreateDatabaseReq.java

@@ -1,14 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-@Data
-@ApiModel("创建 Chroma Database")
-public class CreateDatabaseReq {
-    @ApiModelProperty("Tenant UUID(必填)")
-    private String tenantId;
-    @ApiModelProperty("数据库名称,如 default_database / ylrz_db")
-    private String name;
-}

+ 0 - 12
fs-ai-api/src/main/java/com/fs/ai/rag/dto/CreateTenantReq.java

@@ -1,12 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-@Data
-@ApiModel("创建 Chroma Tenant")
-public class CreateTenantReq {
-    @ApiModelProperty("租户名,如 ylrz_tenant")
-    private String name;
-}

+ 0 - 16
fs-ai-api/src/main/java/com/fs/ai/rag/dto/DeleteReq.java

@@ -1,16 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-@Data
-@ApiModel("按业务主键删除")
-public class DeleteReq {
-    @ApiModelProperty("租户")
-    private String tenantId;
-    @ApiModelProperty("业务主键 doc_id")
-    private String docId;
-    @ApiModelProperty("集合名")
-    private String collectionName;
-}

+ 0 - 20
fs-ai-api/src/main/java/com/fs/ai/rag/dto/IndexReq.java

@@ -1,20 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-@Data
-@ApiModel("知识入库")
-public class IndexReq {
-    @ApiModelProperty("租户编码(将作为 Chroma tenant)")
-    private String tenantCode;
-    @ApiModelProperty("公司/租户维度,用作 Chroma metadata.tenant_id")
-    private String tenantId;
-    @ApiModelProperty("业务主键,全文更新时先按此删旧向量")
-    private String docId;
-    @ApiModelProperty("集合名(公司+业务域),如 company_1_workflow_ai")
-    private String collectionName;
-    @ApiModelProperty("入库全文")
-    private String text;
-}

+ 0 - 18
fs-ai-api/src/main/java/com/fs/ai/rag/dto/QueryReq.java

@@ -1,18 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-@Data
-@ApiModel("向量检索")
-public class QueryReq {
-    @ApiModelProperty("租户过滤,与入库 metadata.tenant_id 一致")
-    private String tenantId;
-    @ApiModelProperty("集合名")
-    private String collectionName;
-    @ApiModelProperty("检索问题")
-    private String question;
-    @ApiModelProperty("条数")
-    private Integer topK = 5;
-}

+ 0 - 26
fs-ai-api/src/main/java/com/fs/ai/rag/dto/RecordDeleteReq.java

@@ -1,26 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.util.List;
-import java.util.Map;
-
-@Data
-@ApiModel("记录 delete 请求")
-public class RecordDeleteReq {
-    @ApiModelProperty("Tenant UUID,可空(为空使用默认配置)")
-    private String tenantId;
-    @ApiModelProperty("数据库名称,可空(为空使用默认配置)")
-    private String database;
-    @ApiModelProperty("集合ID(优先)")
-    private String collectionId;
-    @ApiModelProperty("集合名称(当未传 collectionId 时使用)")
-    private String collectionName;
-
-    @ApiModelProperty("按 IDs 删除(可选)")
-    private List<String> ids;
-    @ApiModelProperty("按 where 条件删除(可选)")
-    private Map<String, Object> where;
-}

+ 0 - 30
fs-ai-api/src/main/java/com/fs/ai/rag/dto/RecordQueryReq.java

@@ -1,30 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.util.List;
-import java.util.Map;
-
-@Data
-@ApiModel("记录 query 请求")
-public class RecordQueryReq {
-    @ApiModelProperty("Tenant UUID,可空(为空使用默认配置)")
-    private String tenantId;
-    @ApiModelProperty("数据库名称,可空(为空使用默认配置)")
-    private String database;
-    @ApiModelProperty("集合ID(优先)")
-    private String collectionId;
-    @ApiModelProperty("集合名称(当未传 collectionId 时使用)")
-    private String collectionName;
-
-    @ApiModelProperty("查询向量")
-    private List<List<Double>> queryEmbeddings;
-    @ApiModelProperty("返回条数")
-    private Integer nResults = 5;
-    @ApiModelProperty("过滤条件")
-    private Map<String, Object> where;
-    @ApiModelProperty("返回字段")
-    private List<String> include;
-}

+ 0 - 30
fs-ai-api/src/main/java/com/fs/ai/rag/dto/RecordUpsertReq.java

@@ -1,30 +0,0 @@
-package com.fs.ai.rag.dto;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.util.List;
-import java.util.Map;
-
-@Data
-@ApiModel("记录 upsert 请求")
-public class RecordUpsertReq {
-    @ApiModelProperty("Tenant UUID,可空(为空使用默认配置)")
-    private String tenantId;
-    @ApiModelProperty("数据库名称,可空(为空使用默认配置)")
-    private String database;
-    @ApiModelProperty("集合ID(优先)")
-    private String collectionId;
-    @ApiModelProperty("集合名称(当未传 collectionId 时使用)")
-    private String collectionName;
-
-    @ApiModelProperty("向量记录 IDs")
-    private List<String> ids;
-    @ApiModelProperty("documents")
-    private List<String> documents;
-    @ApiModelProperty("embeddings")
-    private List<List<Double>> embeddings;
-    @ApiModelProperty("metadatas")
-    private List<Map<String, Object>> metadatas;
-}

+ 9 - 1
fs-ai-api/src/main/java/com/fs/ai/rag/service/VolcanoEmbeddingService.java

@@ -1,4 +1,12 @@
 package com.fs.ai.rag.service;
 
-public class VolcanoEmbeddingService {
+import java.util.List;
+
+
+public interface VolcanoEmbeddingService {
+
+    /**
+     * 传入文本 → 返回火山向量
+     */
+    List<Float> createEmbedding(String text);
 }

+ 0 - 732
fs-ai-api/src/main/java/com/fs/ai/rag/service/impl/KnowledgeVectorServiceImpl.java

@@ -1,732 +0,0 @@
-package com.fs.ai.rag.service.impl;
-
-import com.fs.ai.rag.AiRagProperties;
-import com.fs.ai.rag.KnowledgeVectorService;
-import com.fs.ai.rag.dto.CollectionCreateReq;
-import com.fs.ai.rag.dto.CollectionDeleteReq;
-import com.fs.ai.rag.dto.CollectionListReq;
-import com.fs.ai.rag.dto.CreateDatabaseReq;
-import com.fs.ai.rag.dto.CreateTenantReq;
-import com.fs.ai.rag.dto.DeleteReq;
-import com.fs.ai.rag.dto.IndexReq;
-import com.fs.ai.rag.dto.QueryReq;
-import com.fs.ai.rag.dto.RecordDeleteReq;
-import com.fs.ai.rag.dto.RecordQueryReq;
-import com.fs.ai.rag.dto.RecordUpsertReq;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-
-import java.util.*;
-
-@Service
-@Slf4j
-@RequiredArgsConstructor
-public class KnowledgeVectorServiceImpl implements KnowledgeVectorService {
-    private static final String WORKFLOW_DATABASE = "ai_workflow";
-    private static final String WORKFLOW_COLLECTION = "workflow_knowledge_base";
-
-    private final AiRagProperties props;
-    private final RestTemplate restTemplate;
-
-    private volatile ChromaScope resolvedScope;
-    private static final class ChromaScope {
-        final String tenantId;
-        final String databaseName;
-
-        ChromaScope(String tenantId, String databaseName) {
-            this.tenantId = tenantId;
-            this.databaseName = databaseName;
-        }
-    }
-
-    private static final class EmbeddingProfile {
-        final String url;
-        final String apiKey;
-        final String model;
-
-        EmbeddingProfile(String url, String apiKey, String model) {
-            this.url = url;
-            this.apiKey = apiKey;
-            this.model = model;
-        }
-    }
-
-    @Override
-    public void index(IndexReq req) {
-        if (!props.isEnabled()) {
-            return;
-        }
-        validateIndex(req);
-        String tenantCode = StringUtils.trimToNull(req.getTenantCode());
-        String targetCollection = WORKFLOW_COLLECTION;
-        ChromaScope scope = StringUtils.isNotBlank(tenantCode)
-                ? new ChromaScope(tenantCode, WORKFLOW_DATABASE)
-                : resolveScope();
-        if (StringUtils.isBlank(tenantCode) && StringUtils.isNotBlank(req.getCollectionName())) {
-            targetCollection = req.getCollectionName();
-        }
-
-        EmbeddingProfile embeddingProfile = resolveEmbeddingProfile(targetCollection);
-        String collId = StringUtils.isNotBlank(tenantCode)
-                ? getOrCreateWorkflowCollectionId(scope)
-                : getOrCreateCollectionId(scope, targetCollection);
-
-        deleteByDocIdInternal(scope, collId, req.getDocId());
-
-        List<String> chunks = chunk(req.getText());
-        if (chunks.isEmpty()) {
-            return;
-        }
-        List<List<Double>> embeddings = embedBatch(chunks, embeddingProfile);
-        if (embeddings.size() != chunks.size()) {
-            throw new IllegalStateException("Embedding 条数与分片不一致");
-        }
-
-        List<String> ids = new ArrayList<>();
-        List<Map<String, Object>> metadatas = new ArrayList<>();
-        int n = chunks.size();
-        for (int i = 0; i < n; i++) {
-            ids.add(req.getDocId() + "_" + i);
-            Map<String, Object> meta = new LinkedHashMap<>();
-            meta.put("doc_id", req.getDocId());
-            meta.put("tenant_id", StringUtils.defaultIfBlank(req.getTenantCode(), req.getTenantId()));
-            meta.put("chunk_index", i);
-            meta.put("total_chunks", n);
-            metadatas.add(meta);
-        }
-
-        String url = collectionsPath(scope, collId) + "/upsert";
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("ids", ids);
-        body.put("embeddings", embeddings);
-        body.put("documents", chunks);
-        body.put("metadatas", metadatas);
-        postJson(url, body);
-    }
-
-    @Override
-    public List<String> search(QueryReq req) {
-        if (!props.isEnabled()) {
-            return Collections.emptyList();
-        }
-        if (StringUtils.isAnyBlank(req.getTenantId(), req.getCollectionName(), req.getQuestion())) {
-            throw new IllegalArgumentException("tenantId/collectionName/question 不能为空");
-        }
-        EmbeddingProfile embeddingProfile = resolveEmbeddingProfile(req.getCollectionName());
-        ChromaScope scope = resolveScope();
-        String collId = findCollectionIdByName(scope, req.getCollectionName());
-        if (collId == null) {
-            return Collections.emptyList();
-        }
-
-        List<Double> vec = embedSingle(req.getQuestion(), embeddingProfile);
-        String url = collectionsPath(scope, collId) + "/query";
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("query_embeddings", Collections.singletonList(vec));
-        body.put("n_results", Optional.ofNullable(req.getTopK()).orElse(5));
-        Map<String, String> where = new LinkedHashMap<>();
-        where.put("tenant_id", req.getTenantId());
-        body.put("where", where);
-        body.put("include", Arrays.asList("documents", "metadatas", "distances"));
-
-        Map<String, Object> resp = postJsonReturnMap(url, body);
-        return extractDocuments(resp);
-    }
-
-    @Override
-    public void deleteByDocId(DeleteReq req) {
-        if (!props.isEnabled()) {
-            return;
-        }
-        if (StringUtils.isAnyBlank(req.getTenantId(), req.getDocId(), req.getCollectionName())) {
-            throw new IllegalArgumentException("tenantId/docId/collectionName 不能为空");
-        }
-        ChromaScope scope = resolveScope();
-        String collId = findCollectionIdByName(scope, req.getCollectionName());
-        if (collId == null) {
-            return;
-        }
-        deleteByDocIdInternal(scope, collId, req.getDocId());
-    }
-
-    @Override
-    public Map<String, Object> createTenant(CreateTenantReq req) {
-        if (req == null || StringUtils.isBlank(req.getName())) {
-            throw new IllegalArgumentException("name 不能为空");
-        }
-        String url = baseNormalized() + "/tenants";
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("name", req.getName().trim());
-        return postJsonReturnMap(url, body);
-    }
-
-    @Override
-    public Map<String, Object> createDatabase(CreateDatabaseReq req) {
-        if (req == null || StringUtils.isAnyBlank(req.getTenantId(), req.getName())) {
-            throw new IllegalArgumentException("tenantId/name 不能为空");
-        }
-        String url = baseNormalized() + "/tenants/" + req.getTenantId().trim() + "/databases";
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("name", req.getName().trim());
-        return postJsonReturnMap(url, body);
-    }
-
-    @Override
-    public Map<String, Object> createCollection(CollectionCreateReq req) {
-        ChromaScope scope = resolveScopeOverride(req);
-        String name = trim(req.getName());
-        if (StringUtils.isBlank(name)) {
-            throw new IllegalArgumentException("name 不能为空");
-        }
-        String url = tenantsDatabasesPath(scope) + "/collections";
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("name", name);
-        body.put("get_or_create", req.getGetOrCreate() == null ? Boolean.TRUE : req.getGetOrCreate());
-        if (req.getMetadata() != null) {
-            body.put("metadata", req.getMetadata());
-        }
-        return postJsonReturnMap(url, body);
-    }
-
-    @Override
-    public Object listCollections(CollectionListReq req) {
-        ChromaScope scope = resolveScopeOverride(req);
-        int limit = req.getLimit() == null ? 100 : req.getLimit();
-        int offset = req.getOffset() == null ? 0 : req.getOffset();
-        String url = tenantsDatabasesPath(scope) + "/collections?limit=" + limit + "&offset=" + offset;
-        return getJsonObject(url);
-    }
-
-    @Override
-    public void deleteCollection(CollectionDeleteReq req) {
-        ChromaScope scope = resolveScopeOverride(req);
-        String collectionId = trim(req.getCollectionId());
-        if (StringUtils.isBlank(collectionId)) {
-            String collectionName = trim(req.getCollectionName());
-            if (StringUtils.isBlank(collectionName)) {
-                throw new IllegalArgumentException("collection_id 或 collection_name 必填其一");
-            }
-            collectionId = findCollectionIdByName(scope, collectionName);
-            if (StringUtils.isBlank(collectionId)) {
-                throw new IllegalArgumentException("未找到集合: " + collectionName);
-            }
-        }
-        String url = collectionsPath(scope, collectionId);
-        HttpHeaders h = chromaHeaders();
-        restTemplate.exchange(url, HttpMethod.DELETE, new HttpEntity<>(h), String.class);
-    }
-
-    @Override
-    public Map<String, Object> upsertRecords(RecordUpsertReq req) {
-        ChromaScope scope = resolveScopeOverride(req);
-        String collectionId = resolveCollectionId(scope, req);
-        String url = collectionsPath(scope, collectionId) + "/upsert";
-        return postJsonReturnMap(url, stripControlFields(req));
-    }
-
-    @Override
-    public Map<String, Object> queryRecords(RecordQueryReq req) {
-        ChromaScope scope = resolveScopeOverride(req);
-        String collectionId = resolveCollectionId(scope, req);
-        String url = collectionsPath(scope, collectionId) + "/query";
-        return postJsonReturnMap(url, stripControlFields(req));
-    }
-
-    @Override
-    public Map<String, Object> deleteRecords(RecordDeleteReq req) {
-        ChromaScope scope = resolveScopeOverride(req);
-        String collectionId = resolveCollectionId(scope, req);
-        String url = collectionsPath(scope, collectionId) + "/delete";
-        return postJsonReturnMap(url, stripControlFields(req));
-    }
-
-    private void deleteByDocIdInternal(ChromaScope scope, String collectionId, String docId) {
-        String url = collectionsPath(scope, collectionId) + "/delete";
-        Map<String, Object> where = new LinkedHashMap<>();
-        where.put("doc_id", docId);
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("where", where);
-        postJson(url, body);
-    }
-
-    private String baseNormalized() {
-        String b = props.getChromaBaseUrl().trim();
-        while (b.endsWith("/")) {
-            b = b.substring(0, b.length() - 1);
-        }
-        return b;
-    }
-
-    private String tenantsDatabasesPath(ChromaScope s) {
-        return baseNormalized() + "/tenants/" + s.tenantId + "/databases/" + s.databaseName;
-    }
-
-    private String collectionsPath(ChromaScope s, String collectionId) {
-        return tenantsDatabasesPath(s) + "/collections/" + collectionId;
-    }
-
-    private ChromaScope resolveScope() {
-        ChromaScope c = resolvedScope;
-        if (c != null) {
-            return c;
-        }
-        synchronized (this) {
-            if (resolvedScope != null) {
-                return resolvedScope;
-            }
-            String tid = StringUtils.trimToEmpty(props.getChromaTenantId());
-            String db = StringUtils.trimToEmpty(props.getChromaDatabase());
-            if (StringUtils.isNotBlank(tid) && StringUtils.isNotBlank(db)) {
-                resolvedScope = new ChromaScope(tid, db);
-                return resolvedScope;
-            }
-            Map<String, Object> idMap = getJsonMap(baseNormalized() + "/auth/identity");
-            String tenant = Objects.toString(idMap.get("tenant"), "");
-            if (StringUtils.isBlank(tenant)) {
-                throw new IllegalStateException("Chroma /auth/identity 未返回 tenant,请配置 ai.rag.chroma-tenant-id 与 ai.rag.chroma-database");
-            }
-            if (StringUtils.isBlank(db)) {
-                @SuppressWarnings("unchecked")
-                List<Map<String, Object>> databases = (List<Map<String, Object>>) idMap.get("databases");
-                if (databases != null && !databases.isEmpty()) {
-                    db = Objects.toString(databases.get(0).get("name"), "default_database");
-                } else {
-                    db = "default_database";
-                }
-            }
-            resolvedScope = new ChromaScope(tenant, db);
-            log.info("Chroma 作用域:tenant={}, database={}", tenant, db);
-            return resolvedScope;
-        }
-    }
-
-    private ChromaScope resolveScopeOverride(CollectionCreateReq req) {
-        String tid = req == null ? null : trim(req.getTenantId());
-        String db = req == null ? null : trim(req.getDatabase());
-        if (StringUtils.isNotBlank(tid) && StringUtils.isNotBlank(db)) {
-            return new ChromaScope(tid, db);
-        }
-        ChromaScope fallback = resolveScope();
-        return new ChromaScope(StringUtils.defaultIfBlank(tid, fallback.tenantId),
-                StringUtils.defaultIfBlank(db, fallback.databaseName));
-    }
-
-    private ChromaScope resolveScopeOverride(CollectionListReq req) {
-        String tid = req == null ? null : trim(req.getTenantId());
-        String db = req == null ? null : trim(req.getDatabase());
-        if (StringUtils.isNotBlank(tid) && StringUtils.isNotBlank(db)) {
-            return new ChromaScope(tid, db);
-        }
-        ChromaScope fallback = resolveScope();
-        return new ChromaScope(StringUtils.defaultIfBlank(tid, fallback.tenantId),
-                StringUtils.defaultIfBlank(db, fallback.databaseName));
-    }
-
-    private ChromaScope resolveScopeOverride(CollectionDeleteReq req) {
-        String tid = req == null ? null : trim(req.getTenantId());
-        String db = req == null ? null : trim(req.getDatabase());
-        if (StringUtils.isNotBlank(tid) && StringUtils.isNotBlank(db)) {
-            return new ChromaScope(tid, db);
-        }
-        ChromaScope fallback = resolveScope();
-        return new ChromaScope(StringUtils.defaultIfBlank(tid, fallback.tenantId),
-                StringUtils.defaultIfBlank(db, fallback.databaseName));
-    }
-
-    private ChromaScope resolveScopeOverride(RecordUpsertReq req) {
-        String tid = req == null ? null : trim(req.getTenantId());
-        String db = req == null ? null : trim(req.getDatabase());
-        if (StringUtils.isNotBlank(tid) && StringUtils.isNotBlank(db)) {
-            return new ChromaScope(tid, db);
-        }
-        ChromaScope fallback = resolveScope();
-        return new ChromaScope(StringUtils.defaultIfBlank(tid, fallback.tenantId),
-                StringUtils.defaultIfBlank(db, fallback.databaseName));
-    }
-
-    private ChromaScope resolveScopeOverride(RecordQueryReq req) {
-        String tid = req == null ? null : trim(req.getTenantId());
-        String db = req == null ? null : trim(req.getDatabase());
-        if (StringUtils.isNotBlank(tid) && StringUtils.isNotBlank(db)) {
-            return new ChromaScope(tid, db);
-        }
-        ChromaScope fallback = resolveScope();
-        return new ChromaScope(StringUtils.defaultIfBlank(tid, fallback.tenantId),
-                StringUtils.defaultIfBlank(db, fallback.databaseName));
-    }
-
-    private ChromaScope resolveScopeOverride(RecordDeleteReq req) {
-        String tid = req == null ? null : trim(req.getTenantId());
-        String db = req == null ? null : trim(req.getDatabase());
-        if (StringUtils.isNotBlank(tid) && StringUtils.isNotBlank(db)) {
-            return new ChromaScope(tid, db);
-        }
-        ChromaScope fallback = resolveScope();
-        return new ChromaScope(StringUtils.defaultIfBlank(tid, fallback.tenantId),
-                StringUtils.defaultIfBlank(db, fallback.databaseName));
-    }
-
-    private String resolveCollectionId(ChromaScope scope, RecordUpsertReq req) {
-        String collectionId = trim(req.getCollectionId());
-        if (StringUtils.isNotBlank(collectionId)) {
-            return collectionId;
-        }
-        String collectionName = trim(req.getCollectionName());
-        if (StringUtils.isBlank(collectionName)) {
-            throw new IllegalArgumentException("collection_id 或 collection_name 必填其一");
-        }
-        String resolved = findCollectionIdByName(scope, collectionName);
-        if (StringUtils.isBlank(resolved)) {
-            throw new IllegalArgumentException("未找到集合: " + collectionName);
-        }
-        return resolved;
-    }
-
-    private String resolveCollectionId(ChromaScope scope, RecordQueryReq req) {
-        String collectionId = trim(req.getCollectionId());
-        if (StringUtils.isNotBlank(collectionId)) {
-            return collectionId;
-        }
-        String collectionName = trim(req.getCollectionName());
-        if (StringUtils.isBlank(collectionName)) {
-            throw new IllegalArgumentException("collection_id 或 collection_name 必填其一");
-        }
-        String resolved = findCollectionIdByName(scope, collectionName);
-        if (StringUtils.isBlank(resolved)) {
-            throw new IllegalArgumentException("未找到集合: " + collectionName);
-        }
-        return resolved;
-    }
-
-    private String resolveCollectionId(ChromaScope scope, RecordDeleteReq req) {
-        String collectionId = trim(req.getCollectionId());
-        if (StringUtils.isNotBlank(collectionId)) {
-            return collectionId;
-        }
-        String collectionName = trim(req.getCollectionName());
-        if (StringUtils.isBlank(collectionName)) {
-            throw new IllegalArgumentException("collection_id 或 collection_name 必填其一");
-        }
-        String resolved = findCollectionIdByName(scope, collectionName);
-        if (StringUtils.isBlank(resolved)) {
-            throw new IllegalArgumentException("未找到集合: " + collectionName);
-        }
-        return resolved;
-    }
-
-    private String getOrCreateCollectionId(ChromaScope scope, String name) {
-        String existing = findCollectionIdByName(scope, name);
-        if (existing != null) {
-            return existing;
-        }
-        String url = tenantsDatabasesPath(scope) + "/collections";
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("name", name);
-        body.put("get_or_create", Boolean.TRUE);
-        Map<String, Object> resp = postJsonReturnMap(url, body);
-        Object id = resp.get("id");
-        if (id != null) {
-            return id.toString();
-        }
-        return Objects.requireNonNull(findCollectionIdByName(scope, name), "创建集合失败: " + name);
-    }
-
-    private String getOrCreateWorkflowCollectionId(ChromaScope scope) {
-        String existing = findCollectionIdByName(scope, WORKFLOW_COLLECTION);
-        if (existing != null) {
-            return existing;
-        }
-        String url = tenantsDatabasesPath(scope) + "/collections";
-        Map<String, Object> body = new LinkedHashMap<>();
-        Map<String, Object> configuration = new LinkedHashMap<>();
-        Map<String, Object> hnsw = new LinkedHashMap<>();
-        hnsw.put("space", "cosine");
-        configuration.put("hnsw", hnsw);
-        body.put("configuration", configuration);
-        body.put("get_or_create", Boolean.TRUE);
-        Map<String, Object> metadata = new LinkedHashMap<>();
-        metadata.put("description", "用户创建的工作流知识库");
-        metadata.put("owner", "team-ai");
-        body.put("metadata", metadata);
-        body.put("name", WORKFLOW_COLLECTION);
-        body.put("schema", null);
-        Map<String, Object> resp = postJsonReturnMap(url, body);
-        Object id = resp.get("id");
-        if (id != null) {
-            return id.toString();
-        }
-        return Objects.requireNonNull(findCollectionIdByName(scope, WORKFLOW_COLLECTION),
-                "创建默认工作流集合失败: " + WORKFLOW_COLLECTION);
-    }
-
-    @SuppressWarnings("unchecked")
-    private String findCollectionIdByName(ChromaScope scope, String name) {
-        String url = tenantsDatabasesPath(scope) + "/collections?limit=500&offset=0";
-        Object resp = getJsonObject(url);
-        List<?> collections;
-        // 兼容两类响应:
-        // 1) [](直接数组)
-        // 2) {"collections":[...]}(对象包裹数组)
-        if (resp instanceof List) {
-            collections = (List<?>) resp;
-        } else if (resp instanceof Map) {
-            Object cols = ((Map<String, Object>) resp).get("collections");
-            if (!(cols instanceof List)) {
-                return null;
-            }
-            collections = (List<?>) cols;
-        } else {
-            return null;
-        }
-        for (Object o : collections) {
-            if (!(o instanceof Map)) {
-                continue;
-            }
-            Map<String, Object> m = (Map<String, Object>) o;
-            if (name.equals(Objects.toString(m.get("name"), null))) {
-                return Objects.toString(m.get("id"), null);
-            }
-        }
-        return null;
-    }
-
-    private void validateIndex(IndexReq req) {
-        if (StringUtils.isBlank(req.getTenantCode()) && StringUtils.isBlank(req.getTenantId())) {
-            throw new IllegalArgumentException("tenantCode 或 tenantId 至少填一个");
-        }
-        if (StringUtils.isBlank(req.getDocId())) {
-            throw new IllegalArgumentException("docId 不能为空");
-        }
-        if (StringUtils.isBlank(req.getText())) {
-            throw new IllegalArgumentException("text 不能为空");
-        }
-    }
-
-    private List<String> chunk(String text) {
-        int size = Math.max(100, props.getChunkSize());
-        int overlap = Math.max(0, Math.min(props.getChunkOverlap(), size - 1));
-        int step = Math.max(1, size - overlap);
-        String t = text.trim();
-        if (t.isEmpty()) {
-            return Collections.emptyList();
-        }
-        List<String> out = new ArrayList<>();
-        for (int start = 0; start < t.length(); start += step) {
-            int end = Math.min(start + size, t.length());
-            out.add(t.substring(start, end));
-            if (end >= t.length()) {
-                break;
-            }
-        }
-        return out;
-    }
-
-    private List<Double> embedSingle(String input, EmbeddingProfile profile) {
-        List<List<Double>> batch = embedBatch(Collections.singletonList(input), profile);
-        return batch.isEmpty() ? Collections.emptyList() : batch.get(0);
-    }
-
-    @SuppressWarnings("unchecked")
-    private List<List<Double>> embedBatch(List<String> inputs, EmbeddingProfile profile) {
-        HttpHeaders headers = new HttpHeaders();
-        headers.setContentType(MediaType.APPLICATION_JSON);
-        headers.setBearerAuth(profile.apiKey);
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("model", profile.model);
-        body.put("input", inputs);
-        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
-        Map<String, Object> resp = restTemplate.postForObject(profile.url, entity, Map.class);
-        if (resp == null) {
-            throw new IllegalStateException("Embedding 接口无响应");
-        }
-        Object dataObj = resp.get("data");
-        if (!(dataObj instanceof List)) {
-            throw new IllegalStateException("Embedding 响应格式异常");
-        }
-        List<List<Double>> vecs = new ArrayList<>();
-        for (Object row : (List<?>) dataObj) {
-            if (!(row instanceof Map)) {
-                continue;
-            }
-            Object emb = ((Map<?, ?>) row).get("embedding");
-            if (!(emb instanceof List)) {
-                continue;
-            }
-            List<Double> v = new ArrayList<>();
-            for (Object x : (List<?>) emb) {
-                if (x instanceof Number) {
-                    v.add(((Number) x).doubleValue());
-                }
-            }
-            vecs.add(v);
-        }
-        return vecs;
-    }
-
-    private EmbeddingProfile resolveEmbeddingProfile(String scene) {
-        // 多模型配置优先:routing.scene -> routing.embeddingDefault -> 旧字段回退
-        String alias = null;
-        if (props.getRouting() != null && props.getRouting().getScenes() != null) {
-            alias = props.getRouting().getScenes().get(scene);
-        }
-        if (StringUtils.isBlank(alias) && props.getRouting() != null) {
-            alias = props.getRouting().getEmbeddingDefault();
-        }
-        if (StringUtils.isNotBlank(alias)) {
-            EmbeddingProfile p = buildProfileByAlias(alias);
-            if (p != null) {
-                return p;
-            }
-            log.warn("ai.rag.routing 指定模型别名不存在 alias={},将回退旧配置", alias);
-        }
-        return legacyEmbeddingProfile();
-    }
-
-    private EmbeddingProfile buildProfileByAlias(String alias) {
-        if (props.getModels() == null || props.getProviders() == null) {
-            return null;
-        }
-        AiRagProperties.ModelConfig modelCfg = props.getModels().get(alias);
-        if (modelCfg == null) {
-            return null;
-        }
-        AiRagProperties.ProviderConfig providerCfg = props.getProviders().get(modelCfg.getProvider());
-        if (providerCfg == null) {
-            throw new IllegalStateException("未找到 provider 配置: " + modelCfg.getProvider());
-        }
-        if (!"embedding".equalsIgnoreCase(StringUtils.defaultString(modelCfg.getType(), "embedding"))) {
-            throw new IllegalStateException("模型别名不是 embedding 类型: " + alias);
-        }
-        String baseUrl = StringUtils.trimToEmpty(providerCfg.getBaseUrl());
-        String url = normalizeEmbeddingUrl(baseUrl);
-        String apiKey = StringUtils.trimToEmpty(providerCfg.getApiKey());
-        String model = StringUtils.trimToEmpty(modelCfg.getModel());
-        if (StringUtils.isAnyBlank(url, apiKey, model)) {
-            throw new IllegalStateException("模型别名配置不完整 alias=" + alias + ",请检查 providers/models");
-        }
-        return new EmbeddingProfile(url, apiKey, model);
-    }
-
-    private EmbeddingProfile legacyEmbeddingProfile() {
-        String url = StringUtils.trimToEmpty(props.getEmbeddingUrl());
-        String apiKey = StringUtils.trimToEmpty(props.getEmbeddingApiKey());
-        String model = StringUtils.trimToEmpty(props.getEmbeddingModel());
-        if (StringUtils.isBlank(apiKey)) {
-            throw new IllegalStateException("未配置 ai.rag.embedding-api-key,或未配置可用的 ai.rag.routing/models/providers");
-        }
-        if (StringUtils.isAnyBlank(url, model)) {
-            throw new IllegalStateException("未配置 ai.rag.embedding-url / ai.rag.embedding-model");
-        }
-        return new EmbeddingProfile(normalizeEmbeddingUrl(url), apiKey, model);
-    }
-
-    private String normalizeEmbeddingUrl(String baseOrFull) {
-        String url = StringUtils.trimToEmpty(baseOrFull);
-        if (url.endsWith("/")) {
-            url = url.substring(0, url.length() - 1);
-        }
-        if (url.endsWith("/embeddings")) {
-            return url;
-        }
-        if (url.endsWith("/v1")) {
-            return url + "/embeddings";
-        }
-        return url + "/v1/embeddings";
-    }
-
-    private String trim(Object val) {
-        return val == null ? null : StringUtils.trimToNull(String.valueOf(val));
-    }
-
-    private Map<String, Object> stripControlFields(RecordUpsertReq req) {
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("ids", req.getIds());
-        body.put("documents", req.getDocuments());
-        body.put("embeddings", req.getEmbeddings());
-        body.put("metadatas", req.getMetadatas());
-        return body;
-    }
-
-    private Map<String, Object> stripControlFields(RecordQueryReq req) {
-        Map<String, Object> body = new LinkedHashMap<>();
-        body.put("query_embeddings", req.getQueryEmbeddings());
-        body.put("n_results", req.getNResults());
-        if (req.getWhere() != null) {
-            body.put("where", req.getWhere());
-        }
-        if (req.getInclude() != null) {
-            body.put("include", req.getInclude());
-        }
-        return body;
-    }
-
-    private Map<String, Object> stripControlFields(RecordDeleteReq req) {
-        Map<String, Object> body = new LinkedHashMap<>();
-        if (req.getIds() != null) {
-            body.put("ids", req.getIds());
-        }
-        if (req.getWhere() != null) {
-            body.put("where", req.getWhere());
-        }
-        return body;
-    }
-
-    private void postJson(String url, Map<String, Object> body) {
-        HttpHeaders h = chromaHeaders();
-        h.setContentType(MediaType.APPLICATION_JSON);
-        restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, h), String.class);
-    }
-
-    private Map<String, Object> postJsonReturnMap(String url, Map<String, Object> body) {
-        HttpHeaders h = chromaHeaders();
-        h.setContentType(MediaType.APPLICATION_JSON);
-        return restTemplate.exchange(url, HttpMethod.POST, new HttpEntity<>(body, h),
-                new ParameterizedTypeReference<Map<String, Object>>() {}).getBody();
-    }
-
-    private Map<String, Object> getJsonMap(String url) {
-        HttpHeaders h = chromaHeaders();
-        return restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(h),
-                new ParameterizedTypeReference<Map<String, Object>>() {}).getBody();
-    }
-
-    private Object getJsonObject(String url) {
-        HttpHeaders h = chromaHeaders();
-        return restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(h), Object.class).getBody();
-    }
-
-    private HttpHeaders chromaHeaders() {
-        HttpHeaders h = new HttpHeaders();
-        if (StringUtils.isNotBlank(props.getChromaToken())) {
-            h.add("x-chroma-token", props.getChromaToken());
-        }
-        return h;
-    }
-
-    @SuppressWarnings("unchecked")
-    private List<String> extractDocuments(Map<String, Object> queryResp) {
-        Object docs = queryResp.get("documents");
-        if (!(docs instanceof List) || ((List<?>) docs).isEmpty()) {
-            return Collections.emptyList();
-        }
-        Object first = ((List<?>) docs).get(0);
-        if (!(first instanceof List)) {
-            return Collections.emptyList();
-        }
-        List<String> out = new ArrayList<>();
-        for (Object x : (List<?>) first) {
-            if (x != null) {
-                out.add(x.toString());
-            }
-        }
-        return out;
-    }
-}

+ 84 - 0
fs-ai-api/src/main/java/com/fs/ai/rag/service/impl/VolcanoEmbeddingServiceImpl.java

@@ -0,0 +1,84 @@
+package com.fs.ai.rag.service.impl;
+
+import cn.hutool.http.HttpRequest;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.ai.rag.service.VolcanoEmbeddingService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class VolcanoEmbeddingServiceImpl implements VolcanoEmbeddingService {
+    @Value("${ai.rag.embedding-url}")
+    private String embeddingUrl;
+
+    @Value("${ai.rag.embedding-api-key}")
+    private String apiKey;
+
+    @Value("${ai.rag.embedding-model}")
+    private String model;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    /**
+     * 传入文本 → 返回火山向量
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<Float> createEmbedding(String text) {
+        // 1. 请求头(完全对齐 curl)
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set("Authorization", "Bearer " + apiKey);
+
+        // 2. 构建请求体(严格按照 curl 格式)
+        JSONObject reqJson = new JSONObject();
+        reqJson.put("model", "doubao-embedding-vision-251215");
+        reqJson.put("dimensions", 1024);
+        reqJson.put("encoding_format", "float");
+
+        List<Map<String, Object>> inputList = new ArrayList<>();
+        Map<String, Object> textInput = new HashMap<>();
+        textInput.put("type", "text");
+        textInput.put("text", text);
+        inputList.add(textInput);
+        reqJson.put("input", inputList);
+
+//        // multi_embedding 启用
+//        Map<String, String> multiEmbedding = new HashMap<>();
+//        multiEmbedding.put("type", "enabled");
+//        reqJson.put("multi_embedding", multiEmbedding);
+//
+//        // sparse_embedding 启用
+//        Map<String, String> sparseEmbedding = new HashMap<>();
+//        sparseEmbedding.put("type", "enabled");
+//        reqJson.put("sparse_embedding", sparseEmbedding);
+
+        // 3. 发送请求
+        log.info("向量生成请求:{}", reqJson.toJSONString());
+        String result = HttpRequest.post(embeddingUrl)
+                .header("Authorization", "Bearer " + apiKey)
+                .header("Content-Type", "application/json")
+                .body(reqJson.toJSONString())
+                .execute()
+                .body();
+        log.info("向量生成返回:{}", result);
+
+        JSONObject root = JSON.parseObject(result);
+        JSONArray embeddingArray = root.getJSONObject("data")
+                .getJSONArray("embedding");
+
+        return embeddingArray.toJavaList(Float.class);
+    }
+}

+ 12 - 8
fs-ai-api/src/main/resources/application.yml

@@ -15,17 +15,13 @@ spring:
 swagger:
   enabled: true
 
-# Chroma v2 根路径(含 /api/v2),与心跳一致:https://saaschroma.ylrzcloud.com/api/v2/heartbeat
 ai:
   rag:
     enabled: true
-    chroma-base-url: https://saaschroma.ylrzcloud.com/api/v2
-    # 若 Chroma 开启鉴权,填写 x-chroma-token(OpenAPI 名称 ApiKeyAuth)
-    chroma-token: ""
-    # 可选:手动指定租户 UUID、库名;留空则尝试 GET /auth/identity 自动解析
-    embedding-url: https://api.openai.com/v1/embeddings
-    embedding-api-key: ""
-    embedding-model: text-embedding-3-small
+    embedding-url: https://ark.cn-beijing.volces.com/api/v3/embeddings/multimodal
+    embedding-api-key: ark-5c19ef54-158a-4e9d-ba90-be9c745a8ff6-5cc00
+    embedding-model: doubao-embedding-vision-251215
+
     # 多模型路由(可选)。配置后优先走 routing/models/providers,旧 embedding-* 作为回退。
     routing:
       embedding-default: openai-embed-small
@@ -48,3 +44,11 @@ ai:
     read-timeout-ms: 120000
     chunk-size: 500
     chunk-overlap: 80
+  qdrant:
+    base-url: https://saaschroma.ylrzcloud.com
+    api-key: ""
+    host: saaschroma.ylrzcloud.com
+    port: 443
+    use-tls: true
+    prefer-rest: true
+    vector-size: 1024

+ 85 - 0
fs-company/src/main/java/com/fs/company/controller/knowledge/AiKnowledgeBaseController.java

@@ -0,0 +1,85 @@
+package com.fs.company.controller.knowledge;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.AiKnowledgeBase;
+import com.fs.company.service.AiKnowledgeBaseService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+* <p>
+* ai_knowledge_base 控制器实现
+* </p>
+*
+* @author Administrator
+* @since 2026-04-28 10:17:59
+*/
+@RestController
+@RequestMapping("/ai/knowledge/base")
+public class AiKnowledgeBaseController extends BaseController {
+
+    @Autowired
+    private AiKnowledgeBaseService aiKnowledgeBaseService;
+    @Autowired
+    private TokenService tokenService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String industryType, @RequestParam(required = false) Integer auditStatus, @RequestParam(required = false) String name) {
+        LambdaQueryWrapper<AiKnowledgeBase> lqw = new LambdaQueryWrapper<>();
+        if (StringUtils.isNotEmpty(industryType)) {
+            lqw.eq(AiKnowledgeBase::getIndustryType, industryType);
+        }
+        // 模糊查询(更实用!)
+        if (StringUtils.isNotEmpty(name)) {
+            lqw.like(AiKnowledgeBase::getName, name);
+        }
+        lqw.eq(AiKnowledgeBase::getDelFlag, 0);
+
+        startPage();
+        List<AiKnowledgeBase> list = aiKnowledgeBaseService.list(lqw);
+        return getDataTable(list);
+    }
+
+    @PostMapping("/add")
+    public R create(@RequestBody AiKnowledgeBase aiKnowledgeBase,@RequestHeader("tenant-code") String tenantcode) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiKnowledgeBase.setCreateBy(loginUser.getUser().getUserId());
+        return aiKnowledgeBaseService.create(aiKnowledgeBase,tenantcode);
+    }
+
+    @PutMapping
+    public R updateById(@RequestBody AiKnowledgeBase aiKnowledgeBase) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        aiKnowledgeBase.setUpdateBy(loginUser.getUser().getUserId());
+        boolean result = aiKnowledgeBaseService.updateById(aiKnowledgeBase);
+        return result ? R.ok() : R.error();
+    }
+
+    @DeleteMapping("/{id}")
+    public R removeById(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        AiKnowledgeBase aiKnowledgeBase = new AiKnowledgeBase();
+        aiKnowledgeBase.setId(id);
+        aiKnowledgeBase.setUpdateBy(loginUser.getUser().getUserId());
+        aiKnowledgeBase.setDelFlag(1);
+        boolean result = aiKnowledgeBaseService.updateById(aiKnowledgeBase);
+        return result ? R.ok() : R.error();
+    }
+
+    @GetMapping("/get/{id}")
+    public R getById(@PathVariable Serializable id) {
+        AiKnowledgeBase aiKnowledgeBase = aiKnowledgeBaseService.getById(id);
+        return R.ok().put("data", aiKnowledgeBase);
+    }
+}

+ 6 - 16
fs-company/src/main/java/com/fs/company/controller/knowledge/CompanyKnowledgeBaseController.java

@@ -4,6 +4,7 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.ServletUtils;
 import com.fs.company.domain.CompanyKnowledgeBase;
+import com.fs.company.dto.CompanyKnowledgeBaseDto;
 import com.fs.company.service.ICompanyKnowledgeBaseService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -32,10 +33,11 @@ public class CompanyKnowledgeBaseController extends BaseController {
     @GetMapping("/base/list")
     public AjaxResult list(@RequestParam(required = false) String keyword,
                            @RequestParam(required = false) String industryType,
-                           @RequestParam(required = false) Integer auditStatus) {
+                           @RequestParam(required = false) Integer auditStatus,
+                           @RequestParam(required = false) Long baseId) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         List<CompanyKnowledgeBase> list = knowledgeBaseService.listKnowledge(
-            loginUser.getCompany().getCompanyId(), keyword, industryType, auditStatus);
+            loginUser.getCompany().getCompanyId(), keyword, industryType, auditStatus,baseId);
         return AjaxResult.success(list);
     }
 
@@ -52,9 +54,9 @@ public class CompanyKnowledgeBaseController extends BaseController {
      * 新增知识
      */
     @PostMapping("/base")
-    public AjaxResult add(@RequestBody CompanyKnowledgeBase knowledge) {
+    public AjaxResult add(@RequestBody CompanyKnowledgeBaseDto knowledgeBaseDto, @RequestHeader("tenant-code") String tenantcode) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        return knowledgeBaseService.addKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), knowledge);
+        return knowledgeBaseService.addKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), knowledgeBaseDto,tenantcode);
     }
 
     /**
@@ -117,16 +119,4 @@ public class CompanyKnowledgeBaseController extends BaseController {
         Map<String, Object> result = knowledgeBaseService.dualValidation(loginUser.getCompany().getCompanyId(), query, fastgptResult);
         return AjaxResult.success(result);
     }
-
-    /**
-     * 搜索知识
-     */
-    @GetMapping("/base/search")
-    public AjaxResult search(@RequestParam String keyword,
-                            @RequestParam(required = false) String industryType) {
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        List<CompanyKnowledgeBase> list = knowledgeBaseService.searchKnowledge(
-            loginUser.getCompany().getCompanyId(), keyword, industryType);
-        return AjaxResult.success(list);
-    }
 }

+ 78 - 0
fs-service/src/main/java/com/fs/company/domain/AiKnowledgeBase.java

@@ -0,0 +1,78 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+* <p>
+* ai_knowledge_base 实体类
+* </p>
+*
+* @author Administrator
+* @since 2026-04-28 10:17:24
+*/
+@Getter
+@Setter
+@TableName("ai_knowledge_base")
+public class AiKnowledgeBase implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+    * 主键ID
+    */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+    * 知识库名称
+    */
+    private String name;
+
+    /**
+    * 头像URL
+    */
+    private String avatar;
+
+    /**
+    * 描述
+    */
+    private String description;
+
+    /**
+    * 行业类型
+    */
+    private String industryType;
+
+    /**
+    * 创建人
+    */
+    private Long createBy;
+
+    /**
+     * 更新人
+     */
+    private Long updateBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    private Date updateTime;
+
+    /**
+    * 删除标志(0正常 1删除)
+    */
+    private Integer delFlag;
+
+    private String collectionName;
+
+    private String collectionId;
+
+
+
+}

+ 2 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeBase.java

@@ -20,6 +20,8 @@ public class CompanyKnowledgeBase extends BaseEntity {
 
     private Long companyId;
 
+    private Long baseId;
+
     /** 知识标题 */
     private String title;
 

+ 12 - 0
fs-service/src/main/java/com/fs/company/dto/CompanyKnowledgeBaseDto.java

@@ -0,0 +1,12 @@
+package com.fs.company.dto;
+
+import lombok.Data;
+
+@Data
+public class CompanyKnowledgeBaseDto {
+    private Long baseId;
+    private String collectionId;
+    private String collectionName;
+    private String question;
+    private String answer;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/company/mapper/AiKnowledgeBaseMapper.java

@@ -0,0 +1,18 @@
+package com.fs.company.mapper;
+
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.AiKnowledgeBase;
+
+/**
+* <p>
+* ai_knowledge_base Mapper 接口
+* </p>
+*
+* @author Administrator
+* @since 2026-04-28 10:19:57
+*/
+public interface AiKnowledgeBaseMapper extends BaseMapper<AiKnowledgeBase> {
+
+
+}

+ 6 - 2
fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeBaseMapper.java

@@ -5,13 +5,15 @@ import com.fs.company.domain.CompanyKnowledgeBase;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
 public interface CompanyKnowledgeBaseMapper extends BaseMapper<CompanyKnowledgeBase> {
     
-    List<CompanyKnowledgeBase> selectKnowledgeList(@Param("companyId") Long companyId, 
+    List<CompanyKnowledgeBase> selectKnowledgeList(@Param("companyId") Long companyId,
                                                     @Param("keyword") String keyword,
                                                     @Param("industryType") String industryType,
-                                                    @Param("auditStatus") Integer auditStatus);
+                                                    @Param("auditStatus") Integer auditStatus,
+                                                    @Param("baseId") Long baseId);
     
     int insertKnowledge(CompanyKnowledgeBase entity);
     
@@ -20,6 +22,8 @@ public interface CompanyKnowledgeBaseMapper extends BaseMapper<CompanyKnowledgeB
     int updateKnowledgeById(CompanyKnowledgeBase entity);
     
     int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+
+    Map<String, Object> selectCollectionInfoByBaseId(@Param("baseId") Long baseId);
     
     int incrementUseCount(@Param("id") Long id);
 }

+ 19 - 0
fs-service/src/main/java/com/fs/company/service/AiKnowledgeBaseService.java

@@ -0,0 +1,19 @@
+package com.fs.company.service;
+
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.company.domain.AiKnowledgeBase;
+
+/**
+* <p>
+* ai_knowledge_base Service 接口
+* </p>
+*
+* @author Administrator
+* @since 2026-04-28 10:18:35
+*/
+public interface AiKnowledgeBaseService extends IService<AiKnowledgeBase> {
+
+    R create(AiKnowledgeBase aiKnowledgeBase,String tenantcode);
+}

+ 3 - 7
fs-service/src/main/java/com/fs/company/service/ICompanyKnowledgeBaseService.java

@@ -2,6 +2,7 @@ package com.fs.company.service;
 
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.company.domain.CompanyKnowledgeBase;
+import com.fs.company.dto.CompanyKnowledgeBaseDto;
 
 import java.util.List;
 import java.util.Map;
@@ -11,7 +12,7 @@ public interface ICompanyKnowledgeBaseService {
     /**
      * 查询知识库列表
      */
-    List<CompanyKnowledgeBase> listKnowledge(Long companyId, String keyword, String industryType, Integer auditStatus);
+    List<CompanyKnowledgeBase> listKnowledge(Long companyId, String keyword, String industryType, Integer auditStatus,Long baseId);
     
     /**
      * 根据ID查询知识库
@@ -21,7 +22,7 @@ public interface ICompanyKnowledgeBaseService {
     /**
      * 新增知识
      */
-    AjaxResult addKnowledge(Long companyId, String userName, CompanyKnowledgeBase knowledge);
+    AjaxResult addKnowledge(Long companyId, String userName, CompanyKnowledgeBaseDto knowledgeBaseDto, String tenantcode);
     
     /**
      * 修改知识
@@ -52,9 +53,4 @@ public interface ICompanyKnowledgeBaseService {
      * 双知识库校验
      */
     Map<String, Object> dualValidation(Long companyId, String query, String fastgptResult);
-    
-    /**
-     * 搜索知识
-     */
-    List<CompanyKnowledgeBase> searchKnowledge(Long companyId, String keyword, String industryType);
 }

+ 48 - 0
fs-service/src/main/java/com/fs/company/service/impl/AiKnowledgeBaseServiceImpl.java

@@ -0,0 +1,48 @@
+package com.fs.company.service.impl;
+
+
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.domain.R;
+import com.fs.company.domain.AiKnowledgeBase;
+import com.fs.company.dto.QdrantCollectionReq;
+import com.fs.company.mapper.AiKnowledgeBaseMapper;
+import com.fs.company.service.AiKnowledgeBaseService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+* <p>
+* ai_knowledge_base Service 接口实现
+* </p>
+*
+* @author Administrator
+* @since 2026-04-28 10:19:06
+*/
+@Service
+@Transactional
+@Slf4j
+public class AiKnowledgeBaseServiceImpl extends ServiceImpl<AiKnowledgeBaseMapper, AiKnowledgeBase> implements AiKnowledgeBaseService {
+    private final String BASE_URL = "http://localhost:9009";
+
+    @Autowired
+    private AiKnowledgeBaseMapper aiKnowledgeBaseMapper;
+
+    @Override
+    public R create(AiKnowledgeBase aiKnowledgeBase,String tenantcode) {
+        int insert = baseMapper.insert(aiKnowledgeBase);
+
+        QdrantCollectionReq collectionReq = new QdrantCollectionReq();
+        String cName = tenantcode + "_" + aiKnowledgeBase.getId();
+        collectionReq.setCollectionName(cName);
+        HttpUtil.post(BASE_URL + "/qdrant/collection/create", JSON.toJSONString(collectionReq));
+
+        aiKnowledgeBase.setCollectionName(cName);
+        aiKnowledgeBase.setCollectionId(String.valueOf(aiKnowledgeBase.getId()));
+        int update = baseMapper.updateById(aiKnowledgeBase);
+        return update > 0 ? R.ok() : R.error();
+    }
+}

+ 171 - 20
fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java

@@ -1,17 +1,26 @@
 package com.fs.company.service.impl;
 
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.TypeReference;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.CompanyKnowledgeBase;
+import com.fs.company.dto.CompanyKnowledgeBaseDto;
 import com.fs.company.mapper.CompanyKnowledgeBaseMapper;
 import com.fs.company.service.ICompanyKnowledgeBaseService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -21,12 +30,15 @@ import java.util.Map;
 @Service
 public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseService {
 
+    private static final String AI_API_BASE_URL = "http://localhost:9009";
+
     @Autowired
     private CompanyKnowledgeBaseMapper knowledgeBaseMapper;
 
+
     @Override
-    public List<CompanyKnowledgeBase> listKnowledge(Long companyId, String keyword, String industryType, Integer auditStatus) {
-        return knowledgeBaseMapper.selectKnowledgeList(companyId, keyword, industryType, auditStatus);
+    public List<CompanyKnowledgeBase> listKnowledge(Long companyId, String keyword, String industryType, Integer auditStatus,Long baseId) {
+        return knowledgeBaseMapper.selectKnowledgeList(companyId, keyword, industryType, auditStatus, baseId);
     }
 
     @Override
@@ -36,10 +48,12 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public AjaxResult addKnowledge(Long companyId, String userName, CompanyKnowledgeBase knowledge) {
+    public AjaxResult addKnowledge(Long companyId, String userName, CompanyKnowledgeBaseDto knowledgeBaseDto, String tenantcode) {
+        CompanyKnowledgeBase knowledge = new CompanyKnowledgeBase();
         knowledge.setCompanyId(companyId);
-        knowledge.setAuditStatus(0); // 默认待审核
-        knowledge.setSource("manual"); // 手动录入
+        knowledge.setBaseId(knowledgeBaseDto.getBaseId());
+        knowledge.setAuditStatus(0);
+        knowledge.setSource("manual");
         knowledge.setUseCount(0);
         knowledge.setSyncStatus(0);
         knowledge.setDelFlag(0);
@@ -47,8 +61,23 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
         knowledge.setCreateTime(DateUtils.getNowDate());
         knowledge.setUpdateBy(userName);
         knowledge.setUpdateTime(DateUtils.getNowDate());
+        knowledge.setQuestion(knowledgeBaseDto.getQuestion());
+        knowledge.setAnswer(knowledgeBaseDto.getAnswer());
+
+        int result = knowledgeBaseMapper.insert(knowledge);
+
+        if (result > 0) {
+            try {
+                String text = buildKnowledgeText(knowledge);
+                List<Float> embedding = createEmbedding(text);
+                if (!CollectionUtils.isEmpty(embedding)) {
+                    upsertKnowledgeToQdrant(knowledge, text, embedding, knowledgeBaseDto);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
 
-        int result = knowledgeBaseMapper.insertKnowledge(knowledge);
         return result > 0 ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
     }
 
@@ -60,10 +89,40 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
             return AjaxResult.error("知识不存在");
         }
 
+        if (knowledge.getQuestion() == null) {
+            knowledge.setQuestion(exist.getQuestion());
+        }
+        if (knowledge.getAnswer() == null) {
+            knowledge.setAnswer(exist.getAnswer());
+        }
+        if (knowledge.getBaseId() == null) {
+            knowledge.setBaseId(exist.getBaseId());
+        }
+
         knowledge.setUpdateBy(userName);
         knowledge.setUpdateTime(DateUtils.getNowDate());
 
-        int result = knowledgeBaseMapper.updateKnowledgeById(knowledge);
+        int result = knowledgeBaseMapper.updateById(knowledge);
+        if (result > 0) {
+            try {
+                CompanyKnowledgeBase latest = knowledgeBaseMapper.selectKnowledgeByIdAndCompanyId(knowledge.getId(), companyId);
+                if (latest != null) {
+                    String collectionName = resolveCollectionName(latest.getBaseId());
+                    if (collectionName != null) {
+                        String text = buildKnowledgeText(latest);
+                        List<Float> embedding = createEmbedding(text);
+                        if (!CollectionUtils.isEmpty(embedding)) {
+                            CompanyKnowledgeBaseDto dto = new CompanyKnowledgeBaseDto();
+                            dto.setBaseId(latest.getBaseId());
+                            dto.setCollectionName(collectionName);
+                            upsertKnowledgeToQdrant(latest, text, embedding, dto);
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
         return result > 0 ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
     }
 
@@ -76,6 +135,16 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
         }
 
         int result = knowledgeBaseMapper.logicalDeleteById(id, companyId);
+        if (result > 0) {
+            try {
+                String collectionName = resolveCollectionName(exist.getBaseId());
+                if (collectionName != null) {
+                    deleteKnowledgeFromQdrant(exist.getId(), collectionName);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
         return result > 0 ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
     }
 
@@ -106,10 +175,6 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
             return AjaxResult.error("知识不存在");
         }
 
-        // TODO: 实现同步到FastGPT的逻辑
-        // 1. 调用FastGPT API创建知识库
-        // 2. 更新fastgpt_id和sync_status
-
         knowledge.setSyncStatus(1);
         knowledge.setSyncTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
         knowledge.setUpdateBy(userName);
@@ -143,11 +208,6 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
 
     @Override
     public Map<String, Object> dualValidation(Long companyId, String query, String fastgptResult) {
-        // TODO: 实现双知识库校验逻辑
-        // 1. 在本地知识库中搜索相似问题
-        // 2. 对比FastGPT返回的结果
-        // 3. 返回校验结果
-
         Map<String, Object> result = new HashMap<>();
         result.put("query", query);
         result.put("fastgptResult", fastgptResult);
@@ -157,9 +217,100 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
         return result;
     }
 
-    @Override
-    public List<CompanyKnowledgeBase> searchKnowledge(Long companyId, String keyword, String industryType) {
-        return knowledgeBaseMapper.selectKnowledgeList(companyId, keyword, industryType, null);
+    private String buildKnowledgeText(CompanyKnowledgeBase knowledge) {
+        return "Q:" + knowledge.getQuestion() + "\nA:" + knowledge.getAnswer();
     }
-}
 
+    @SuppressWarnings("unchecked")
+    private List<Float> createEmbedding(String text) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("type", "text");
+        jsonObject.put("text", text);
+
+        String jsonResult = HttpRequest.post(AI_API_BASE_URL + "/ai/embedding/create")
+                .header("Content-Type", "application/json;charset=UTF-8")
+                .body(jsonObject.toJSONString())
+                .execute()
+                .body();
+
+        Map<String, Object> resMap = JSONUtil.toBean(jsonResult, Map.class);
+        Object data = resMap.get("data");
+        if (!(data instanceof List)) {
+            return new ArrayList<>();
+        }
+        return (List<Float>) data;
+    }
+
+    private void upsertKnowledgeToQdrant(CompanyKnowledgeBase knowledge, String text, List<Float> embedding, CompanyKnowledgeBaseDto knowledgeBaseDto) {
+        Map<String, Object> req = new LinkedHashMap<>();
+        req.put("collectionName", knowledgeBaseDto.getCollectionName());
+
+        List<Long> ids = new ArrayList<>();
+        ids.add(knowledge.getId());
+        req.put("ids", ids);
+
+        List<List<Float>> vectors = new ArrayList<>();
+        vectors.add(embedding);
+        req.put("vectors", vectors);
+
+        List<String> documents = new ArrayList<>();
+        documents.add(text);
+        req.put("documents", documents);
+
+        List<Map<String, Object>> payloads = new ArrayList<>();
+        Map<String, Object> payloadMap = JSON.parseObject(JSON.toJSONString(knowledge), new TypeReference<Map<String, Object>>() {});
+        payloads.add(convertToSafePayload(payloadMap));
+        req.put("payloads", payloads);
+
+        HttpRequest.post(AI_API_BASE_URL + "/qdrant/point/upsert")
+                .header("Content-Type", "application/json;charset=UTF-8")
+                .body(JSON.toJSONString(req))
+                .execute()
+                .body();
+    }
+
+    private void deleteKnowledgeFromQdrant(Long knowledgeId, String collectionName) {
+        Map<String, Object> req = new LinkedHashMap<>();
+        req.put("collectionName", collectionName);
+        List<Long> ids = new ArrayList<>();
+        ids.add(knowledgeId);
+        req.put("ids", ids);
+
+        HttpRequest.post(AI_API_BASE_URL + "/qdrant/point/delete")
+                .header("Content-Type", "application/json;charset=UTF-8")
+                .body(JSON.toJSONString(req))
+                .execute()
+                .body();
+    }
+
+    private String resolveCollectionName(Long baseId) {
+        if (baseId == null) {
+            return null;
+        }
+        Map<String, Object> result = knowledgeBaseMapper.selectCollectionInfoByBaseId(baseId);
+        if (result == null) {
+            return null;
+        }
+        Object collectionName = result.get("collection_name");
+        return collectionName == null ? null : String.valueOf(collectionName);
+    }
+
+    private Map<String, Object> convertToSafePayload(Object obj) {
+        Map<String, Object> map = JSON.parseObject(
+                JSON.toJSONString(obj),
+                new TypeReference<Map<String, Object>>() {}
+        );
+
+        Map<String, Object> result = new HashMap<>();
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            Object value = entry.getValue();
+            if (value == null) {
+                continue;
+            }
+            if (value instanceof String || value instanceof Number || value instanceof Boolean) {
+                result.put(entry.getKey(), value);
+            }
+        }
+        return result;
+    }
+}

+ 18 - 0
fs-service/src/main/resources/mapper/company/AiKnowledgeBaseMapper.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.AiKnowledgeBaseMapper">
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="aiKnowledgeBase">
+                <id column="id" property="id" />
+                <result column="name" property="name" />
+                <result column="avatar" property="avatar" />
+                <result column="description" property="description" />
+                <result column="industry_type" property="industryType" />
+                <result column="create_by" property="createBy" />
+                <result column="create_time" property="createTime" />
+                <result column="update_time" property="updateTime" />
+                <result column="del_flag" property="delFlag" />
+    </resultMap>
+
+
+</mapper>

+ 30 - 17
fs-service/src/main/resources/mapper/company/CompanyKnowledgeBaseMapper.xml

@@ -5,6 +5,7 @@
     <resultMap id="CompanyKnowledgeBaseResult" type="CompanyKnowledgeBase">
         <result property="id" column="id"/>
         <result property="companyId" column="company_id"/>
+        <result property="baseId" column="base_id"/>
         <result property="title" column="title"/>
         <result property="question" column="question"/>
         <result property="answer" column="answer"/>
@@ -26,35 +27,38 @@
     </resultMap>
 
     <select id="selectKnowledgeList" resultMap="CompanyKnowledgeBaseResult">
-        select id, company_id, title, question, answer, industry_type, source, audit_status,
-               audit_comment, auditor, audit_time, use_count, fastgpt_id, sync_status, sync_time,
-               del_flag, create_by, create_time, update_by, update_time
+        select id, company_id, title, base_id, question, answer, industry_type, source, audit_status,
+        audit_comment, auditor, audit_time, use_count, fastgpt_id, sync_status, sync_time,
+        del_flag, create_by, create_time, update_by, update_time
         from company_knowledge_base
         where del_flag = 0
-          and company_id = #{companyId}
-          <if test="keyword != null and keyword != ''">
-              and (title like concat('%', #{keyword}, '%') or question like concat('%', #{keyword}, '%'))
-          </if>
-          <if test="industryType != null and industryType != ''">
-              and industry_type = #{industryType}
-          </if>
-          <if test="auditStatus != null">
-              and audit_status = #{auditStatus}
-          </if>
+        and company_id = #{companyId}
+        <if test="keyword != null and keyword != ''">
+            and (title like concat('%', #{keyword}, '%') or question like concat('%', #{keyword}, '%'))
+        </if>
+        <if test="industryType != null and industryType != ''">
+            and industry_type = #{industryType}
+        </if>
+        <if test="auditStatus != null">
+            and audit_status = #{auditStatus}
+        </if>
+        <if test="baseId != null">
+            and base_id = #{baseId}
+        </if>
         order by create_time desc
     </select>
 
     <insert id="insertKnowledge" parameterType="CompanyKnowledgeBase" useGeneratedKeys="true" keyProperty="id">
         insert into company_knowledge_base
-        (company_id, title, question, answer, industry_type, source, audit_status, use_count, 
-         sync_status, del_flag, create_by, create_time, update_by, update_time)
+        (company_id, title, question, answer, industry_type, source, audit_status, use_count,
+         sync_status, del_flag, create_by, create_time, update_by, update_time, base_id)
         values
         (#{companyId}, #{title}, #{question}, #{answer}, #{industryType}, #{source}, #{auditStatus}, #{useCount},
-         #{syncStatus}, #{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime})
+         #{syncStatus}, #{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{baseId})
     </insert>
 
     <select id="selectKnowledgeByIdAndCompanyId" resultMap="CompanyKnowledgeBaseResult">
-        select id, company_id, title, question, answer, industry_type, source, audit_status,
+        select id, company_id, base_id, title, question, answer, industry_type, source, audit_status,
                audit_comment, auditor, audit_time, use_count, fastgpt_id, sync_status, sync_time,
                del_flag, create_by, create_time, update_by, update_time
         from company_knowledge_base
@@ -64,6 +68,14 @@
         limit 1
     </select>
 
+    <select id="selectCollectionInfoByBaseId" resultType="java.util.HashMap">
+        select id, collection_name, collection_id
+        from ai_knowledge_base
+        where id = #{baseId}
+          and del_flag = 0
+        limit 1
+    </select>
+
     <update id="updateKnowledgeById" parameterType="CompanyKnowledgeBase">
         update company_knowledge_base
         <trim prefix="set" suffixOverrides=",">
@@ -79,6 +91,7 @@
             <if test="fastgptId != null">fastgpt_id = #{fastgptId},</if>
             <if test="syncStatus != null">sync_status = #{syncStatus},</if>
             <if test="syncTime != null">sync_time = #{syncTime},</if>
+            <if test="baseId != null">base_id = #{baseId},</if>
             <if test="updateBy != null">update_by = #{updateBy},</if>
             <if test="updateTime != null">update_time = #{updateTime},</if>
         </trim>