Ver Fonte

增加了 data/dk tools/api 的管理

dwp há 6 meses atrás
pai
commit
280cf79a54
25 ficheiros alterados com 2479 adições e 13 exclusões
  1. 98 0
      server/src/main/java/com/giantan/data/dk/Readme.java
  2. 93 0
      server/src/main/java/com/giantan/data/dk/constant/DkConfig.java
  3. 11 0
      server/src/main/java/com/giantan/data/dk/constant/DkConstants.java
  4. 79 0
      server/src/main/java/com/giantan/data/dk/controller/DkCollectionsController.java
  5. 323 0
      server/src/main/java/com/giantan/data/dk/controller/DkDocsController.java
  6. 80 0
      server/src/main/java/com/giantan/data/dk/dto/GTool.java
  7. 15 0
      server/src/main/java/com/giantan/data/dk/dto/GToolExample.java
  8. 4 0
      server/src/main/java/com/giantan/data/dk/dto/KnowledgeChunk.java
  9. 29 0
      server/src/main/java/com/giantan/data/dk/repository/DkExtraRepository.java
  10. 497 0
      server/src/main/java/com/giantan/data/dk/repository/DkIndexer.java
  11. 469 0
      server/src/main/java/com/giantan/data/dk/repository/DkRepository.java
  12. 21 0
      server/src/main/java/com/giantan/data/dk/repository/DkTaskRepository.java
  13. 22 0
      server/src/main/java/com/giantan/data/dk/repository/DkTaxonomyRepository.java
  14. 36 0
      server/src/main/java/com/giantan/data/dk/repository/IDkIndexer.java
  15. 82 0
      server/src/main/java/com/giantan/data/dk/repository/IGDkRepository.java
  16. 365 0
      server/src/main/java/com/giantan/data/dk/service/DkCollectionService.java
  17. 102 0
      server/src/main/java/com/giantan/data/dk/service/DkDocsService.java
  18. 91 0
      server/src/main/java/com/giantan/data/dk/service/IDkDocsService.java
  19. 1 1
      server/src/main/java/com/giantan/data/mds/MdsApplication.java
  20. 3 1
      server/src/main/java/com/giantan/data/mds/service/impl/MdCores.java
  21. 5 5
      server/src/main/java/com/giantan/data/qa/controller/QaDocsController.java
  22. 0 1
      server/src/main/java/com/giantan/data/qa/controller/QaTaxonomyController.java
  23. 0 2
      server/src/main/java/com/giantan/data/qa/service/IQaCollectionService.java
  24. 2 3
      server/src/main/java/com/giantan/data/qa/service/QaCollectionService.java
  25. 51 0
      server/src/test/java/com/giantan/data/mds/DkRepositoryTest.java

+ 98 - 0
server/src/main/java/com/giantan/data/dk/Readme.java

@@ -0,0 +1,98 @@
+package com.giantan.data.dk;
+
+/*
+📌 差异说明:
+Knowledge Graph:国际通用,指基于实体、关系、属性的知识建模。
+Capability Graph:多见于技术、产业、企业 IT 场景,强调“系统或组织的功能/能力”之间的关联。
+Competency Graph:多见于教育、人才、岗位匹配场景,强调“个人/团队的技能能力”建模。
+👉 如果你的上下文是 工具索引库、系统功能能力,推荐用 Capability Graph;
+如果是 人才/技能管理,推荐用 Competency Graph。
+
+ */
+
+/*
+{
+  "id": "uuid",
+  "name": "SearchTool",
+  "description": "根据关键词搜索文档",
+  "input_schema": {
+    "query": {"type": "string", "required": true},
+    "limit": {"type": "integer", "required": false, "default": 10}
+  },
+  "output_schema": {
+    "results": {"type": "array", "items": {"id": "string", "title": "string"}},
+    "count": {"type": "integer"}
+  },
+  "usage_examples": [
+    {
+      "user_query": "帮我找关于乙烯裂解工艺的文档",
+      "tool_input": {"query": "乙烯 裂解 工艺", "limit": 5},
+      "tool_output": {
+        "results": [
+          {"id": "doc123", "title": "乙烯裂解工艺原理"},
+          {"id": "doc456", "title": "乙烯装置操作手册"}
+        ],
+        "count": 2
+      }
+    }
+  ]
+}
+
+ */
+
+/*
+CREATE TABLE gtool (
+    id BIGSERIAL PRIMARY KEY,   -- 内部自增ID
+    gid VARCHAR(64) UNIQUE NOT NULL, -- 全局唯一ID (UUID 或自定义规则)
+    name TEXT NOT NULL,
+    type VARCHAR(50) NOT NULL,       -- "decision_chunk" | "external_api"
+    description TEXT,
+    path TEXT,
+    tags TEXT[],
+    input_schema JSONB,
+    output_schema JSONB,
+    usage_examples JSONB,
+    decision_knowledge JSONB,
+    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
+    metadata JSONB,
+    extra JSONB
+);
+
+-- 索引
+CREATE INDEX idx_tool_type ON tool(type);
+CREATE INDEX idx_tool_path ON tool(path text_pattern_ops);  -- 支持前缀查询
+CREATE INDEX idx_tool_tags ON tool USING GIN(tags);
+CREATE INDEX idx_tool_input_schema ON tool USING GIN(input_schema);
+CREATE INDEX idx_tool_output_schema ON tool USING GIN(output_schema);
+//CREATE INDEX idx_tool_usage_examples ON tool USING GIN(usage_examples);
+CREATE INDEX idx_tool_decision_knowledge ON tool USING GIN(decision_knowledge);
+CREATE INDEX idx_tool_metadata ON tool USING GIN(metadata);
+ */
+
+/*
+-- 1. 基础索引
+-- type 经常用于筛选
+CREATE INDEX idx_gtool_type ON gtool(type);
+-- path 支持前缀匹配和分类查询
+CREATE INDEX idx_gtool_path ON gtool USING btree(path text_pattern_ops);
+
+-- tags 为数组,适合 GIN 索引
+CREATE INDEX idx_gtool_tags ON gtool USING gin(tags);
+
+-- 2. JSONB 字段索引
+-- 一般查询模式是 key/value 过滤,用 jsonb_path_ops 更高效
+CREATE INDEX idx_gtool_input_schema ON gtool USING gin(input_schema jsonb_path_ops);
+
+CREATE INDEX idx_gtool_output_schema ON gtool USING gin(output_schema jsonb_path_ops);
+
+//CREATE INDEX idx_gtool_usage_examples ON gtool USING gin(usage_examples jsonb_path_ops);
+
+CREATE INDEX idx_gtool_decision_knowledge ON gtool USING gin(decision_knowledge jsonb_path_ops);
+
+CREATE INDEX idx_gtool_metadata ON gtool USING gin(metadata jsonb_path_ops);
+
+//CREATE INDEX idx_gtool_extra  ON gtool USING gin(extra jsonb_path_ops);
+
+ */
+public class Readme {
+}

+ 93 - 0
server/src/main/java/com/giantan/data/dk/constant/DkConfig.java

@@ -0,0 +1,93 @@
+package com.giantan.data.dk.constant;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DkConfig {
+
+    public static final String ID = "id";
+    public static final String GID = "gid";
+    public static final String NAME = "name";
+    public static final String TYPE = "type";
+    public static final String DESCRIPTION = "description";
+    public static final String PATH = "path";
+    public static final String TAGS = "tags";
+    public static final String INPUT_SCHEMA = "inputSchema";
+    public static final String OUTPUT_SCHEMA = "outputSchema";
+    public static final String USAGE_EXAPLES = "usageExamples";
+    public static final String DECISION_KNOWLEDGE = "decisionKnowledge";
+
+    public static final String CREATE_AT = "createdAt";
+    public static final String METADATA = "metadata";
+    public static final String EXTRA = "extra";
+
+    public static List<String> createTable(String schema, String tableName) {
+        List<String> ls = new ArrayList<String>();
+        String sql = """
+                    CREATE TABLE IF NOT EXISTS %s.%s (
+                    id BIGSERIAL PRIMARY KEY,  
+                    gid VARCHAR(64) UNIQUE NOT NULL,
+                    name TEXT NOT NULL,
+                    type VARCHAR(50) NOT NULL,
+                    description TEXT,
+                    path TEXT,
+                    tags TEXT[],
+                    input_schema JSONB,
+                    output_schema JSONB,
+                    usage_examples JSONB,
+                    decision_knowledge JSONB,
+                    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
+                    metadata JSONB,
+                    extra JSONB
+                );
+                """;
+        String createTable = String.format(sql, schema, tableName);
+        ls.add(createTable);
+
+        String gidIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_gid ON %s.%S (gid);",
+                tableName, schema, tableName);
+
+        String nameIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_name ON %s.%S (name);",
+                tableName, schema, tableName);
+
+        String typeIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_type ON %s.%S (type);",
+                tableName, schema, tableName);
+
+        // path 支持前缀匹配和分类查询
+        String pathIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_path ON %s.%S  USING btree(path text_pattern_ops);",
+                tableName, schema, tableName);
+
+        // tags 为数组,适合 GIN 索引
+        String tagsIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_tags ON %s.%S USING gin(tags);",
+                tableName, schema, tableName);
+
+        String inputIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_input_schema ON %s.%S USING gin(input_schema jsonb_path_ops);",
+                tableName, schema, tableName);
+
+        String outputIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_output_schema ON %s.%S USING gin(output_schema jsonb_path_ops);",
+                tableName, schema, tableName);
+
+        String metadataIndex = String.format("CREATE INDEX IF NOT EXISTS idx_%s_metadata ON %s.%S USING gin(metadata jsonb_path_ops);",
+                tableName, schema, tableName);
+        ls.add(gidIndex);
+        ls.add(nameIndex);
+        ls.add(typeIndex);
+        ls.add(pathIndex);
+        ls.add(tagsIndex);
+        ls.add(inputIndex);
+        ls.add(outputIndex);
+        ls.add(metadataIndex);
+        return ls;
+    }
+
+    public static List<String> dropTable(String schema, String tableName) {
+        String sql = String.format("DROP TABLE IF EXISTS %s.%s ;", schema, tableName);
+        return List.of(sql);
+    }
+
+    public static List<String> clearAll(String schema, String tableName) {
+        String sql = String.format("DELETE FROM %s.%s", schema, tableName);
+        return List.of(sql);
+    }
+
+}

+ 11 - 0
server/src/main/java/com/giantan/data/dk/constant/DkConstants.java

@@ -0,0 +1,11 @@
+package com.giantan.data.dk.constant;
+
+public class DkConstants {
+
+    public static final String TOOL_TYPE_DECISION ="decision_chunk";
+    public static final String TOOL_TYPE_API ="api";
+
+    public static final String DECISION_TEXT ="content";
+
+    public static final String API_PREFIX = "/v1/dk";
+}

+ 79 - 0
server/src/main/java/com/giantan/data/dk/controller/DkCollectionsController.java

@@ -0,0 +1,79 @@
+package com.giantan.data.dk.controller;
+
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.dk.service.DkCollectionService;
+import com.giantan.data.dk.constant.DkConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+//修改某个字段,推荐 PATCH。
+//整体替换,才用 PUT。
+//新增才用 POST。
+
+@RestController
+@RequestMapping(DkConstants.API_PREFIX+"/collections")
+@CrossOrigin(origins = "*", maxAge = 3600)
+public class DkCollectionsController {
+
+    @Autowired
+    DkCollectionService qaCollectionService;
+
+    @PostMapping
+    public ResponseEntity<R> createCollection(@RequestParam String name) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.createCollection(name)));
+    }
+
+    @GetMapping
+    public ResponseEntity<R> getAllCollections() throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.getAllCollections()));
+    }
+
+    @GetMapping("/{name}")
+    public ResponseEntity<R> getCollectionById(@PathVariable String name) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.getKvByName(name)));
+    }
+
+    @DeleteMapping("/{name}")
+    public ResponseEntity<R> deleteCollection(@PathVariable String name) throws Throwable {
+       // long ret = collectionService.deleteCollection(id);
+        long ret = qaCollectionService.deleteCollection(name);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/{name}/all")
+    public ResponseEntity<R> clearCollection(@PathVariable String name) throws Throwable {
+        // long ret = collectionService.deleteCollection(id);
+        long ret = qaCollectionService.clearCollection(name);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PutMapping("/{name}")
+    public ResponseEntity<R> update(@PathVariable String name,  @RequestBody Map<String,Object> kvs) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.updateCollection(name,kvs)));
+    }
+
+    @GetMapping("/{name}/attributes")
+    public ResponseEntity<R> getAttributes(@PathVariable String name) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.getCollectionAttributes(name)));
+    }
+
+    @PutMapping("/{name}/attributes")
+    public ResponseEntity<R> updateAttributes(@PathVariable String name,  @RequestBody Map<String,Object> attributes) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.updateCollectionAttributes(name,attributes)));
+    }
+
+    @DeleteMapping("/{name}/attributes")
+    public ResponseEntity<R> removeAttribute(@PathVariable String name,@RequestBody List<String> keys) throws Throwable {
+        return ResponseEntity.ok(R.data(qaCollectionService.removeCollectionAttribute(name,keys)));
+    }
+
+//    @PutMapping("/{name}/attributes")
+//    public ResponseEntity<R> updateChunkMode(@PathVariable String name, @RequestBody Map<String,Object> attributes) throws Throwable {
+//        return ResponseEntity.ok(R.data(qaCollectionService.updateChunkMode(name,attributes)));
+//    }
+}

+ 323 - 0
server/src/main/java/com/giantan/data/dk/controller/DkDocsController.java

@@ -0,0 +1,323 @@
+package com.giantan.data.dk.controller;
+
+import com.giantan.ai.common.reponse.R;
+import com.giantan.data.dk.constant.DkConstants;
+import com.giantan.data.dk.dto.GTool;
+import com.giantan.data.dk.service.DkDocsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Map;
+
+// | 上传文件       | `POST`   | `/collections/{collId}/mds`                  |
+//| 上传 ZIP(批量) | `POST`   | `/collections/{collId}/docs/batch`(或 `/bulk`) |
+
+//| 提交新任务  | `POST`   | `/collections/{collId}/tasks`                                                                             |
+//| 获取任务状态 | `GET`    | `/collections/{collId}/tasks/{taskId}/status`  |
+//| 取消任务   | `DELETE` | `/collections/{collId}/tasks/{taskId}`                                                                    |
+//| 获取任务详情 | `GET`    | `/collections/{collId}/tasks/{taskId}`
+
+
+// deleteByName DELETE /collections/{collId}/mds?name=someName
+
+// 批处理任务的统一提交方式
+// POST /collections/{collId}/tasks
+//Content-Type: application/json
+//
+//{
+//  "type": "chunk",  // 或 "keywords", "summary"
+//  "objectIds": ["md1", "md2"],  // 可选:为空则全量
+//  "params": {
+//    "chunkSize": 500,
+//    "language": "zh"
+//  }
+//}
+
+//`/collections/{collId}/docs/actions/keywords`
+
+// - `POST /collections/{collId}/docs/actions/{action}`:简洁直观,表示“对资源集合执行动作”
+//- 配合 `mdIds` 或标签、筛选条件等参数进行部分操作
+
+@RestController
+@RequestMapping(DkConstants.API_PREFIX + "/collections/{collId}")
+public class DkDocsController {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Autowired
+    DkDocsService dkDocsService;
+
+    @PostMapping("/docs")
+    public R createEntry(@PathVariable String collId, @RequestBody GTool data) throws Throwable {
+        GTool r = dkDocsService.save(collId, data);
+        return R.data(r);
+    }
+
+    @GetMapping("/docs/all")
+    public R<List<GTool>> getAll(@PathVariable String collId) throws Throwable {
+        List<GTool> ret = null;
+        ret = dkDocsService.findAll(collId);
+        return R.data(ret);
+    }
+
+    @GetMapping("/docs/{id}")
+    public R<?> getById(@PathVariable String collId, @PathVariable String id
+    ) throws Throwable {
+        GTool ret = dkDocsService.findByIdOrGid(collId, id);
+        return R.data(ret);
+    }
+
+    @DeleteMapping("/docs/{gid}")
+    public R<?> delete(@PathVariable String collId, @PathVariable String gid
+    ) throws Throwable {
+        int deleted = dkDocsService.deleteByIdOrGid(collId, gid);
+        log.info("Delete dk: {}", gid);
+        //log.info("taskId = " + taskId);
+        return R.data(Map.of("deleted", deleted));
+    }
+
+
+    @PutMapping("/docs/{qid}")
+    public R update(@PathVariable String collId, @PathVariable String qid, @RequestBody GTool tool) throws Throwable {
+        GTool r = dkDocsService.update(collId, qid, tool);
+        return R.data(r);
+    }
+
+
+    /*
+
+
+    @PostMapping("/docs/batch")
+    public ResponseEntity<R> createBatch(@PathVariable String collId, @RequestBody List<GBaseKeyValue> kvs) throws Throwable {
+        List<Integer> ret = dkDocsService.saveAll(collId, kvs);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+
+    @DeleteMapping("/docs/by-name")
+    public R<Map<String, Object>> deleteByName(@PathVariable String collId, @RequestParam("name") String name
+    ) throws Throwable {
+        // 根据 name 删除对应 md
+        int deleted = dkDocsService.deleteByName(collId, name);
+        return R.data(Map.of("deleted", deleted));
+    }
+
+    @DeleteMapping("/docs/all")
+    public R<Map<String, Object>> deleteAll(@PathVariable String collId) throws Exception {
+        long removed = dkDocsService.deleteAll(collId);
+        return R.data(Map.of("deleted", removed));
+    }
+
+    @PutMapping("/docs")
+    public R update(@PathVariable String collId, @RequestBody GBaseKeyValue kv) throws Throwable {
+        GBaseKeyValue updatedKv = dkDocsService.update(collId, kv);
+        return R.data(updatedKv);
+    }
+
+
+
+
+    //////////////////
+    //整体 metadata 管理	/collections/{collId}/docs/{docId}/metadata
+    //单个 metadata 字段	/collections/{collId}/docs/{docId}/metadata/{key}
+
+
+    @PostMapping("/docs/{docId}/rename")
+    public R<?> rename(@PathVariable String collId, @PathVariable String docId, @RequestBody Map<String, Object> req
+    ) throws Throwable {
+        Map<String, Object> r = dkDocsService.rename(collId, docId, req);
+        return R.data(r);
+    }
+
+
+    ////////////////
+    // 更新属性
+
+    @GetMapping("/docs/{docId}/attributes")
+    public R getAttributeByKey(@PathVariable String collId, @PathVariable String docId
+    ) throws Throwable {
+        Object ret = dkDocsService.getAttributes(collId, docId);
+        return R.data(ret);
+    }
+
+    @GetMapping("/docs/{docId}/attributes/{key}")
+    public R getAttributeByKey(@PathVariable String collId, @PathVariable String docId,
+                               @PathVariable String key
+    ) throws Throwable {
+        Object ret = dkDocsService.getAttributeByKey(collId, docId, key);
+        return R.data(ret);
+    }
+
+    @PatchMapping("/docs/{docId}/attributes")
+    public R patchAttributes(@PathVariable String collId, @PathVariable String docId,
+                             @RequestBody GBaseKeyValue data
+    ) throws Throwable {
+        Object ret = dkDocsService.patchAttributes(collId, docId, data);
+        return R.data(ret);
+    }
+
+    @DeleteMapping("/docs/{docId}/attributes/{key}")
+    public R deleteAttributeByKey(@PathVariable String collId, @PathVariable String docId,
+                                  @PathVariable String key
+    ) throws Throwable {
+        Object ret = dkDocsService.deleteAttributeByKey(collId, docId, key);
+        return R.data(ret);
+    }
+
+    // 移除属性
+    @DeleteMapping("/docs/{docId}/attributes")
+    public R removeAttribute(@PathVariable String collId, @PathVariable String docId, @RequestBody List<String> keys) throws Throwable {
+        GBaseKeyValue updatedKv = dkDocsService.deleteAttributeByKeys(collId, docId, keys);
+        return R.data(updatedKv);
+    }
+
+    @PutMapping("/docs/{docId}/attributes")
+    public R updateAttribute(@PathVariable String collId, @PathVariable Integer docId, @RequestBody Map<String, Object> attributes) throws Throwable {
+        GBaseKeyValue updatedKv = dkDocsService.updateAttribute(collId, docId, attributes);
+        return R.data(updatedKv);
+    }
+
+    // 获取某些字段的所有值
+    @PostMapping("/docs/fields")
+    public R getAllEntities(@PathVariable String collId, @RequestBody List<String> fields) throws Throwable {
+        List<Map<String, Object>> entities = dkDocsService.getAllEntities(collId, fields);
+        return R.data(entities);
+    }
+
+
+    ////////////////////
+    //GET /collections/demo/paths/get?path=/a/b/c
+    //DELETE /collections/demo/paths/delete?path=/a/b/c
+    //GET /collections/demo/paths/list?prefix=/a
+    //GET /collections/demo/paths/children?path=/a
+    //GET /collections/demo/paths/descendants?path=/a
+
+    @GetMapping("/docs/by-path")
+    public R getDocsBypPath(@PathVariable String collId, @RequestParam String path) throws Throwable {
+        List<GBaseKeyValue> rets = dkDocsService.findByPath(collId, path);
+        return R.data(rets);
+    }
+
+    @GetMapping("/docs/by-prefix")
+    public R getDocsBypPrefix(@PathVariable String collId, @RequestParam String prefix) throws Throwable {
+        List<GBaseKeyValue> rets = dkDocsService.findByPrefix(collId, prefix);
+        return R.data(rets);
+    }
+
+    @DeleteMapping("/docs/by-path")
+    public R deletePathAndDescendants(@PathVariable String collId, @RequestParam String path) throws Throwable {
+        int rets = dkDocsService.deletePathAndDescendants(collId, path);
+        return R.data(rets);
+    }
+
+    // 获取记录数
+    @GetMapping("/docs/count")
+    public R getCount(@PathVariable String collId) {
+        long count = dkDocsService.count(collId);
+        return R.data(count);
+    }
+
+    @PostMapping("/docs/{docId}/tags/add")
+    public ResponseEntity<R> appendTag(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = dkDocsService.appendArrayField(collId, docId, "tags", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/tags/set")
+    public ResponseEntity<R> setTag(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = dkDocsService.setArrayField(collId, docId, "tags", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/tags/remove")
+    public ResponseEntity<R> removeTag(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = dkDocsService.removeArrayField(collId, docId, "tags", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/altlabels/add")
+    public ResponseEntity<R> appendAltlabels(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = dkDocsService.appendArrayField(collId, docId, "altlabels", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/altlabels/set")
+    public ResponseEntity<R> setAltlabels(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = dkDocsService.setArrayField(collId, docId, "altlabels", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/{docId}/altlabels/remove")
+    public ResponseEntity<R> removeAltlabels(@PathVariable String collId, @PathVariable int docId, @RequestBody List<String> values) {
+        List<String> ret = dkDocsService.removeArrayField(collId, docId, "altlabels", values);
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @PostMapping("/docs/fulltextSearch")
+    public ResponseEntity<R> fulltextSearch(@PathVariable String collId, @RequestBody Map<String, Object> query) throws Throwable {
+        List<GBaseKeyValue> rets = dkDocsService.fulltextSearch(collId, query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    @PostMapping("/docs/similaritySearch")
+    public ResponseEntity<R> similaritySearch(@PathVariable String collId, @RequestBody Map<String, Object> query) throws Throwable {
+        List<GBaseKeyValue> rets = dkDocsService.similaritySearch(collId, query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    @PostMapping("/docs/hybridSearch")
+    public ResponseEntity<R> hybridSearch(@PathVariable String collId, @RequestBody Map<String, Object> query) throws Throwable {
+        List<GBaseKeyValue> rets = dkDocsService.hybridSearch(collId, query);
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    //////////////
+    @GetMapping("/docs")
+    public ResponseEntity<R> getDocs(
+            @PathVariable String collId,
+            @RequestParam(required = false) String path,
+            @RequestParam(required = false) String prefix,
+            @RequestParam(required = false) String parent) throws Throwable {
+        List<GBaseKeyValue> rets = null;
+        if (path != null) {
+            rets = dkDocsService.findByPath(collId, path);
+        } else if (prefix != null) {
+            rets = dkDocsService.findByPrefix(collId, prefix);
+        } else if (parent != null) {
+            rets = dkDocsService.getChildren(collId, parent);
+        } else {
+            rets = dkDocsService.findAll(collId);
+        }
+        return ResponseEntity.ok(R.data(rets));
+    }
+
+    @DeleteMapping("/docs")
+    public ResponseEntity<R> deleteDocs(
+            @PathVariable String collId,
+            @RequestParam(required = false) String path,
+            @RequestParam(required = false) String prefix,
+            @RequestParam(required = false) String subtree) throws Throwable {
+        int ret = 0;
+        if (path != null) {
+            ret = dkDocsService.deleteByPath(collId, path);
+        } else if (prefix != null) {
+            ret = dkDocsService.deleteByPrefix(collId, prefix);
+        } else if (subtree != null) {
+            ret = dkDocsService.deletePathAndDescendants(collId, subtree);
+        } else {
+            throw new IllegalArgumentException("Must provide path or prefix to delete");
+        }
+        return ResponseEntity.ok(R.data(ret));
+    }
+
+    @DeleteMapping("/docs/indexes")
+    public ResponseEntity<R> deleteDocsIndexes(@PathVariable String collId) throws IOException, InterruptedException {
+        int r = dkDocsService.deleteAllIndex(collId);
+        return ResponseEntity.ok(R.data(r));
+    }
+    */
+
+}

+ 80 - 0
server/src/main/java/com/giantan/data/dk/dto/GTool.java

@@ -0,0 +1,80 @@
+package com.giantan.data.dk.dto;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Map;
+
+/*
+{
+  "id": "uuid",
+  "name": "SearchTool",
+  "description": "根据关键词搜索文档",
+  "input_schema": {
+    "query": {"type": "string", "required": true},
+    "limit": {"type": "integer", "required": false, "default": 10}
+  },
+  "output_schema": {
+    "results": {"type": "array", "items": {"id": "string", "title": "string"}},
+    "count": {"type": "integer"}
+  },
+  "vector_embedding": [0.123, -0.456, ...]
+}
+
+ */
+
+/* LLM 的输入 prompt 类似:
+用户问题: {user_query}
+以下是可能相关的工具:
+1. {tool_1.name} - {tool_1.description} - schema: {tool_1.schema}
+2. {tool_2.name} - {tool_2.description} - schema: {tool_2.schema}
+...
+
+请根据用户问题,选择最合适的工具,并给出调用参数(JSON 格式)。
+如果没有合适的工具,请回答 "无工具匹配"。
+ */
+/* 输出示例:
+{
+  "tool": "weather_api",
+  "params": {
+    "city": "北京"
+  }
+}
+ */
+//Decision Knowledge Chunk 就是一个 最小可检索、可追溯、可解释 的 LLM 决策知识单元。
+
+/*
+不需要单独类(统一用 Tool)
+如果你希望 所有内容(API 调用 + 知识单元)都统一抽象为 Tool,那 Tool 就已经够用了。
+type = "decision_chunk" 的时候,decisionKnowledge 字段就承载了知识单元,不需要额外类。
+优点:
+简单:查询、存储、索引都在一个表里
+LLM 检索时不用再区分表/类
+扩展方便(比如以后加 "workflow"、"retriever" 也能塞进 Tool)
+
+⚖️ 建议
+如果你现在只是 快速搭建一个工具库,我建议 不用建 DecisionKnowledgeChunk 类,统一用 Tool 即可。
+如果你打算未来在知识层面做更复杂的事(例如 知识单元复用、推理链管理、分层知识图谱),
+那最好建一个独立的 DecisionKnowledgeChunk 类,并和 Tool 建立一对一/多对多关系。
+ */
+@Data
+public class GTool {
+    private Long id;
+    private String gid;
+    private String name;
+    private String type;  // "decision_chunk" | "external_api"
+    private String description;
+    private String path;          // 工具类别
+    private List<String> tags;        // 工具标签
+    private Map<String, Object> inputSchema;
+    private Map<String, Object> outputSchema;
+    // 使用示例(可以存 few-shot)
+    private List<Map<String, Object>> usageExamples;
+    private Map<String, Object> decisionKnowledge;
+    private OffsetDateTime createdAt;
+    private Map<String, Object> metadata;
+    private Map<String, Object> extra;
+
+}

+ 15 - 0
server/src/main/java/com/giantan/data/dk/dto/GToolExample.java

@@ -0,0 +1,15 @@
+package com.giantan.data.dk.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class GToolExample {
+    private String id;
+    private String toolId;
+    private String userQuery;
+    private Map<String, Object> toolInput;
+    private Map<String, Object> toolOutput;
+    private long createdAt;
+}

+ 4 - 0
server/src/main/java/com/giantan/data/dk/dto/KnowledgeChunk.java

@@ -0,0 +1,4 @@
+package com.giantan.data.dk.dto;
+
+public class KnowledgeChunk {
+}

+ 29 - 0
server/src/main/java/com/giantan/data/dk/repository/DkExtraRepository.java

@@ -0,0 +1,29 @@
+package com.giantan.data.dk.repository;
+
+import com.giantan.data.kvs.repository.GDynamicRepository;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+// 用来以后 扩展的存储
+@Repository
+public class DkExtraRepository extends GDynamicRepository {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(DkExtraRepository.class);
+
+
+    @Autowired
+    private JdbcTemplate jdbc;
+
+    public DkExtraRepository() {
+        this.schema = "dkdb";
+        this.tablePrefix = "extra";
+    }
+
+    @PostConstruct
+    public void init() {
+        //collections = new GRepository(KvConfig.GKG_CATALOG, KvConfig.GKG_ENTRY_COLLECTIONS, jdbcTemplate);
+        setJdbcTemplate(this.jdbc);
+    }
+}

+ 497 - 0
server/src/main/java/com/giantan/data/dk/repository/DkIndexer.java

@@ -0,0 +1,497 @@
+package com.giantan.data.dk.repository;
+
+import com.giantan.data.dk.constant.DkConstants;
+import com.giantan.data.dk.dto.GTool;
+import com.giantan.data.index.HybridIndexer;
+import com.giantan.data.index.IHybridSearch;
+import com.giantan.data.index.IndexConfig;
+import com.giantan.data.index.dto.DocReq;
+import com.giantan.data.index.dto.DocResp;
+//import com.giantan.data.kvs.repository.GEntity;
+//import com.giantan.data.kvs.repository.GEntityConfig;
+import com.giantan.data.qa.service.IQaCollectionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Repository;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Repository
+public class DkIndexer extends HybridIndexer implements IDkIndexer {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+
+    String cellectionPrefix = "dks_";
+
+    String splitter = "\n";
+
+    @Autowired
+    IHybridSearch hybridSearch;
+
+
+    //    ICollectionMapper collMapper;
+    IQaCollectionService collectionService;
+
+    public DkIndexer() {
+        //collMapper = new DefaultCollectionMapper();
+        defaultIndexPrefix = "dks";
+    }
+
+    @Override
+    public void init(IQaCollectionService collectionService) {
+        // 根据 params的attributes
+        setCollectionService(collectionService);
+    }
+
+    public IQaCollectionService getCollectionService() {
+        return collectionService;
+    }
+
+    public void setCollectionService(IQaCollectionService collectionService) {
+        this.collectionService = collectionService;
+    }
+
+//    public ICollectionMapper getCollMapper() {
+//        return collMapper;
+//    }
+//
+//    public void setCollMapper(ICollectionMapper collMapper) {
+//        this.collMapper = collMapper;
+//    }
+
+    private void buildMetadata(String collection, GTool entity, DocReq req) {
+        Map<String, Object> metadata = req.getMetadata();
+        if (metadata == null) {
+            metadata = new HashMap<>();
+            req.setMetadata(metadata);
+        }
+        metadata.put(DOC_ID, entity.getId().toString());
+        metadata.put(COLL_ID, collection);
+    }
+
+    public String indexName(String coll) {
+        return defaultIndexPrefix + "_" + coll;
+    }
+
+    public String indexName(String prefix, String coll) {
+        return prefix + coll;
+    }
+
+    public String getMappedIndexName(String collId) {
+        String collName = collectionService.getCollectionName(Integer.parseInt(collId));
+        String mapped = indexName(collName);
+        Map<String, Object> attibutes = collectionService.getAttibutes(collId);
+        String configIndexName = getConfigIndexName(collId, attibutes);
+        if (configIndexName != null) {
+            mapped = configIndexName;
+        }
+        return mapped;
+    }
+
+    public boolean isOne2One(String collId) {
+        boolean isOne2One = true;
+        Map<String, Object> attibutes = collectionService.getAttibutes(collId);
+
+        Boolean configOne2One = getConfigOne2One(attibutes);
+        if (configOne2One != null) {
+            isOne2One = configOne2One;
+        }
+
+        return isOne2One;
+    }
+
+    private String getChunkMode(String coll) {
+        String chunkMode = defaultChunkMode;
+
+        Map<String, Object> attibutes = collectionService.getAttibutes(coll);
+
+        String configChunkMode = getConfigChunkMode(attibutes);
+        if (configChunkMode != null) {
+            chunkMode = configChunkMode;
+        }
+        return chunkMode;
+    }
+
+    private List<String> getChunkTemplates(String coll) {
+        List<String> rets = null;
+        Map<String, Object> attibutes = collectionService.getAttibutes(coll);
+
+        List<String> configChunkTemplates = getConfigChunkTemplates(attibutes);
+        if (configChunkTemplates != null) {
+            rets = configChunkTemplates;
+        }
+        return rets;
+    }
+
+    private List<DocReq> toDocReq(String coll, GTool entity) {
+        String chunkMode = getChunkMode(coll);
+        List<DocReq> rets = null;
+//        if (chunkMode.equals(IndexConfig.CHUNK_MODE_MULTIPLE)) {
+//            rets = toDocReq2(coll, entity);
+//        } else if (chunkMode.equals(IndexConfig.CHUNK_MODE_CUSTOM)) {
+//            rets = toDocReq3(coll, entity);
+//        } else {
+//            rets = toDocReq1(coll, entity);
+//        }
+
+        rets = toDocReq1(coll, entity);
+
+        return rets;
+    }
+
+    //TODO
+//    private List<DocReq> toDocReq1(String coll, GTool entity) {
+//        List<DocReq> lst = new ArrayList<DocReq>();
+//        DocReq dr1 = new DocReq();
+//        dr1.setId(entity.getGid());
+//
+//        dr1.setTags(entity.getTags());
+//        buildMetadata(coll, entity, dr1);
+//
+//        StringBuilder sb = new StringBuilder();
+//        sb.append(entity.getName());
+//        if (entity.getDescription() != null) {
+//            sb.append(entity.getDescription()).append(entity.getDescription());
+//        }
+//
+//        List<String> altlabels = entity.getAltlabels();
+//        if (altlabels != null && !altlabels.isEmpty()) {
+//            for (int i = 0; i < altlabels.size(); i++) {
+//                sb.append(splitter).append(altlabels.get(i));
+//            }
+//        }
+//        dr1.setText(sb.toString());
+//        lst.add(dr1);
+//        return lst;
+//        return null;
+//    }
+
+
+    private String buildBaseChunk(GTool entity) {
+        // 基础描述
+//        String descChunk = String.format(
+//                "[TOOL] %s\nType: %s\nPath: %s\nTags: %s\nDescription: %s",
+//                tool.getName(), tool.getType(), tool.getPath(),
+//                String.join(", ", tool.getTags()), tool.getDescription()
+//        );
+        StringBuilder ret = new StringBuilder();
+        ret.append("[TOOL] ").append(entity.getName()).append(splitter);
+        if (entity.getPath() != null) {
+            ret.append("Path: ").append(entity.getPath()).append(splitter);
+        }
+        if (entity.getTags() != null) {
+            ret.append("Tags: ").append(String.join(", ", entity.getTags())).append(splitter);
+        }
+        if (entity.getDescription()!= null){
+            ret.append("Description: ").append(entity.getDescription()).append(splitter);
+        }
+        return ret.toString();
+    }
+
+    private String buildSchemaChunk(GTool entity) {
+        //  String schemaChunk = String.format(
+        //                "[SCHEMA] %s\nInput: %s\nOutput: %s",
+        //                tool.getName(),
+        //                tool.getInputSchema(),
+        //                tool.getOutputSchema()
+        //            );
+        StringBuilder ret = new StringBuilder();
+        ret.append("[SCHEMA] ").append(entity.getName()).append(splitter);
+        if (entity.getInputSchema() != null) {
+            ret.append("Input: ").append(entity.getInputSchema()).append(splitter);
+        }
+        if (entity.getOutputSchema() != null) {
+            ret.append("Output: ").append(entity.getOutputSchema()).append(splitter);
+        }
+        return ret.toString();
+    }
+
+    private String buildExampleChunk(GTool entity,Map<String,Object> ex) {
+        //chunks.add(new ToolChunk(tool.getId(), "usage",
+        //                "[USAGE] " + tool.getName() + "\nExample: " + ex.toString()));
+        StringBuilder ret = new StringBuilder();
+        ret.append("[USAGE] ").append(entity.getName()).append(splitter);
+        ret.append("Example: ").append(ex.toString()).append(splitter);
+
+        return ret.toString();
+    }
+
+    private String buildDecisionKnowledgeChunk(GTool entity, String decision){
+        //chunks.add(new ToolChunk(tool.getId(), "knowledge",
+        //"[KNOWLEDGE] " + tool.getName() + "\nKnowledge: " + tool.getDecisionKnowledge().toString()));
+        StringBuilder ret = new StringBuilder();
+        ret.append("[KNOWLEDGE] ").append(entity.getName()).append(splitter);
+        ret.append("Knowledge: ").append(decision).append(splitter);
+
+        return ret.toString();
+    }
+
+
+    private List<DocReq> toDocReq1(String coll, GTool entity) {
+        List<DocReq> lst = new ArrayList<DocReq>();
+        //**基础描述 chunk**
+        DocReq dr1 = new DocReq();
+        dr1.setId(entity.getGid() + "-0");
+        dr1.setText(buildBaseChunk(entity));
+        dr1.setTags(entity.getTags());
+        buildMetadata(coll, entity, dr1);
+
+        lst.add(dr1);
+
+        if (entity.getInputSchema() != null || entity.getOutputSchema() != null) {
+            String schemaChunk = buildSchemaChunk(entity);
+            DocReq dr2 = new DocReq();
+            dr2.setId(entity.getGid() + "-1");
+            dr2.setText(schemaChunk);
+            dr2.setTags(entity.getTags());
+            buildMetadata(coll, entity, dr2);
+            lst.add(dr2);
+        }
+
+        int idx = 2;
+        if (entity.getUsageExamples() != null) {
+            for (Map<String,Object> ex : entity.getUsageExamples()) {
+                String exampleChunk = buildExampleChunk(entity,ex);
+                DocReq dr2 = new DocReq();
+                dr2.setId(entity.getGid() + "-"+idx);
+                dr2.setText(exampleChunk);
+                dr2.setTags(entity.getTags());
+                buildMetadata(coll, entity, dr2);
+                lst.add(dr2);
+                idx++;
+            }
+        }
+
+        if (DkConstants.TOOL_TYPE_DECISION.equals(entity.getType()) && entity.getDecisionKnowledge() != null) {
+            Map<String, Object> knowledge = entity.getDecisionKnowledge();
+            Object decisionText = knowledge.get(DkConstants.DECISION_TEXT);
+            if (decisionText != null) {
+                DocReq dr2 = new DocReq();
+                dr2.setId(entity.getGid() + "-"+idx);
+                dr2.setText(buildDecisionKnowledgeChunk(entity,decisionText.toString()));
+                dr2.setTags(entity.getTags());
+                buildMetadata(coll, entity, dr2);
+                lst.add(dr2);
+                idx++;
+            }
+        }
+        return lst;
+    }
+
+    //  ExpressionParser parser = new SpelExpressionParser();
+    //        Expression expression = parser.parseExpression(expressionString);
+    //
+    //        // 上下文
+    //        StandardEvaluationContext context = new StandardEvaluationContext();
+    //        context.setVariables(variables);
+    //
+    //        // 计算结果
+    //        String chunk = expression.getValue(context, String.class);
+
+//    private StandardEvaluationContext buildContext(GEntity entity) {
+//        Map<String, Object> variables = new HashMap<>();
+//        variables.put(GEntityConfig.NAME, entity.getName());
+//        if (entity.getAttributes() != null) {
+//            variables.put(GEntityConfig.ATTRIBUTES, entity.getAttributes());
+//        } else {
+//            variables.put(GEntityConfig.ATTRIBUTES, new HashMap<>());
+//        }
+//
+//        if (entity.getAltlabels() != null) {
+//            variables.put(GEntityConfig.ALTLABELS, entity.getAltlabels());
+//        } else {
+//            variables.put(GEntityConfig.ALTLABELS, new ArrayList<String>());
+//        }
+//        if (entity.getDescription() != null) {
+//            variables.put(GEntityConfig.DESCRIPTION, entity.getDescription());
+//        } else {
+//            variables.put(GEntityConfig.DESCRIPTION, "");
+//        }
+//        StandardEvaluationContext context = new StandardEvaluationContext();
+//        context.setVariables(variables);
+//        return context;
+//    }
+
+
+    private List<DocReq> toDocReq3(String coll, GTool entity) {
+        List<String> templates = getChunkTemplates(coll);
+        if (templates == null || templates.isEmpty()) {
+            return toDocReq1(coll, entity);
+        }
+
+        List<DocReq> lst = new ArrayList<DocReq>();
+        //TODO
+
+//        StandardEvaluationContext context = buildContext(entity);
+//        ExpressionParser parser = new SpelExpressionParser();
+//
+//        if (templates.size() == 1) {
+//            Expression expression = parser.parseExpression(templates.get(0));
+//            String chunk = expression.getValue(context, String.class);
+//            DocReq dr1 = new DocReq();
+//            dr1.setId(entity.getGid());
+//            dr1.setTags(entity.getTags());
+//            dr1.setText(chunk);
+//            buildMetadata(coll, entity, dr1);
+//            lst.add(dr1);
+//        } else {
+//            for (int i = 0; i < templates.size(); i++) {
+//                Expression expression = parser.parseExpression(templates.get(i));
+//                String chunk = expression.getValue(context, String.class);
+//                DocReq dr1 = new DocReq();
+//                dr1.setId(entity.getGid() + "-" + i);
+//                dr1.setTags(entity.getTags());
+//                dr1.setText(chunk);
+//                buildMetadata(coll, entity, dr1);
+//                lst.add(dr1);
+//            }
+//        }
+        return lst;
+    }
+
+    @Override
+    public int onAdd(String collection, GTool entity) throws IOException, InterruptedException {
+        log.info("{}{} onAdd: {}", cellectionPrefix, collection, entity.getName());
+        List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, entity));
+        return rs == null ? 0 : rs.size();
+    }
+
+    @Override
+    public int onAdd(String collection, List<GTool> entities) throws IOException, InterruptedException {
+        log.info("{}{} onAdd: size = {}", cellectionPrefix, collection, entities.size());
+        List<DocReq> ls = new ArrayList<>();
+        for (GTool e : entities) {
+            List<DocReq> docReqs = toDocReq(collection, e);
+            ls.addAll(docReqs);
+        }
+        List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), ls);
+        return rs == null ? 0 : rs.size();
+    }
+
+    @Override
+    public int onDelete(String collection, GTool entity) {
+        log.info("{}{} onDelete: {}", cellectionPrefix, collection, entity.getName());
+        String gid = entity.getGid();
+        int i = 0;
+        try {
+            i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), gid);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return i;
+    }
+
+    @Override
+    public int onDelete(String collection, List<GTool> entities) {
+        log.info("{}{} onDelete: size = {}", cellectionPrefix, collection, entities.size());
+        int count = 0;
+        try {
+            for (GTool e : entities) {
+                String gid = e.getGid();
+                count = count + hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), gid);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return count;
+    }
+
+    @Override
+    public int onUpdate(String collection, GTool entity) throws IOException, InterruptedException {
+        log.info("{}{} onUpdate: {}", cellectionPrefix, collection, entity.getName());
+
+        int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), entity.getGid());
+        List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, entity));
+        return rs == null ? 0 : rs.size();
+    }
+
+    @Override
+    public int onUpdateField(String collection, GTool entity, String field) {
+        log.info("{}{} onUpdateField: ret = {}, field = {}", cellectionPrefix, collection, entity.getGid(), field);
+        int count = 0;
+        try {
+            int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), entity.getGid());
+            List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, entity));
+            count += rs == null ? 0 : rs.size();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        return count;
+    }
+
+    @Override
+    public int onUpdateField(String collection, List<GTool> entities, String field) {
+        log.info("{}{} onUpdateField: size = {}, field = {}", cellectionPrefix, collection, entities.size(), field);
+        int count = 0;
+        try {
+            for (GTool e : entities) {
+                int i = hybridSearch.deleteDocumentsByIdFilter(getMappedIndexName(collection), e.getGid());
+                List<DocResp> rs = hybridSearch.add(getMappedIndexName(collection), toDocReq(collection, e));
+                count += rs == null ? 0 : rs.size();
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return count;
+    }
+
+    @Override
+    public int createCollection(String collection) {
+        return 0;
+    }
+
+
+    @Override
+    public int onDeleteAll(String collection) {
+        boolean b = false;
+        try {
+            if (isOne2One(collection)) {
+                b = hybridSearch.clearCollection(getMappedIndexName(collection));
+            } else {
+                //TODO 特殊处理
+                //  "filterExpression": "metadata[\"__cid\"] == \"qas_2\""
+                Map<String, Object> filterMap = Map.of("filterExpression", "metadata[\"__cid\"] == \"" + collection + "\"");
+                int r = hybridSearch.deleteDocumentsByFilter(getMappedIndexName(collection), filterMap);
+                return r;
+            }
+        } catch (Exception e) {
+            log.error("Collection {} onDeleteAll: {}", collection, e.getMessage());
+            throw new RuntimeException(e);
+        }
+        return 0;
+    }
+
+    @Override
+    public int deleteCollection(String collection) {
+        boolean b = false;
+        try {
+            if (isOne2One(collection)) {
+                b = hybridSearch.dropCollection(getMappedIndexName(collection));
+            } else {
+                //TODO 特殊处理
+                //  "filterExpression": "metadata[\"__cid\"] == \"qas_2\""
+                Map<String, Object> filterMap = Map.of("filterExpression", "metadata[\"__cid\"] == \"" + collection + "\"");
+                int r = hybridSearch.deleteDocumentsByFilter(getMappedIndexName(collection), filterMap);
+                return r;
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return b ? 0 : 1;
+    }
+
+    @Override
+    public int clearCollection(String collection) throws IOException, InterruptedException {
+        Map<String, Object> filterMap = Map.of("filterExpression", "metadata[\"__cid\"] == \"" + collection + "\"");
+        int r = hybridSearch.deleteDocumentsByFilter(getMappedIndexName(collection), filterMap);
+        return r;
+    }
+
+}

+ 469 - 0
server/src/main/java/com/giantan/data/dk/repository/DkRepository.java

@@ -0,0 +1,469 @@
+package com.giantan.data.dk.repository;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.giantan.ai.util.id.IdGenerator;
+import com.giantan.ai.util.id.UlidGenerator;
+import com.giantan.data.dk.constant.DkConfig;
+import com.giantan.data.dk.dto.GTool;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+// copy from GIndexedRepository.java
+
+@Repository
+public class DkRepository implements IGDkRepository {
+
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(DkRepository.class);
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Autowired
+    protected JdbcTemplate jdbc;
+
+    @Autowired
+    private IDkIndexer indexer;
+
+    protected String schema = "dkdb";
+    protected String tablePrefix = "dk";
+    protected String indexPrefix = "dk";
+
+    private IdGenerator idGenerator = new UlidGenerator();
+
+
+
+    public DkRepository() {
+
+    }
+
+    public String getSchema() {
+        return schema;
+    }
+
+    public void setSchema(String schema) {
+        this.schema = schema;
+    }
+
+    public String getTablePrefix() {
+        return tablePrefix;
+    }
+
+    public void setTablePrefix(String tablePrefix) {
+        this.tablePrefix = tablePrefix;
+    }
+
+    public String getIndexPrefix() {
+        return indexPrefix;
+    }
+
+    public void setIndexPrefix(String indexPrefix) {
+        this.indexPrefix = indexPrefix;
+    }
+
+    public IDkIndexer getIndexer() {
+        return indexer;
+    }
+
+    public void setIndexer(IDkIndexer indexer) {
+        this.indexer = indexer;
+    }
+
+//    protected String fullTableName(String collId) {
+//        return schema + "." + tablePrefix + "_" + collId;// collId.replace("-", "")
+//    }
+
+    protected String tableName(String collId) {
+        return tablePrefix + "_" + collId;// collId.replace("-", "")
+    }
+
+
+    public void setIdGenerator(IdGenerator idGenerator) {
+        this.idGenerator = idGenerator;
+    }
+
+    public void setJdbcTemplate(JdbcTemplate jdbc) {
+        this.jdbc = jdbc;
+    }
+
+    // 工具方法:对象转 JSON
+    private String toJson(Object obj) {
+        try {
+            return obj != null ? mapper.writeValueAsString(obj) : null;
+        } catch (Exception e) {
+            throw new RuntimeException("JSON serialization failed", e);
+        }
+    }
+
+    private String[] toArray(List<String> list) {
+        return list == null ? new String[0] : list.toArray(new String[0]);
+    }
+
+//    private Map<String, Object> readJson(String json) {
+//        try {
+//            return json == null ? Map.of() : mapper.readValue(json, Map.class);
+//        } catch (Exception e) {
+//            throw new RuntimeException("Failed to deserialize attributes", e);
+//        }
+//    }
+
+    private <T> T readJson(ResultSet rs, String column, Class<T> clazz) throws SQLException {
+        String json = rs.getString(column);
+        try {
+            return (json != null) ? mapper.readValue(json, clazz) : null;
+        } catch (Exception e) {
+            throw new SQLException("Failed to parse JSON for column: " + column, e);
+        }
+    }
+
+    public GTool toTool(ResultSet rs) throws SQLException {
+        GTool tool = new GTool();
+        tool.setId(rs.getLong("id"));
+        tool.setGid(rs.getString("gid"));
+        tool.setName(rs.getString("name"));
+        tool.setType(rs.getString("type"));
+        tool.setDescription(rs.getString("description"));
+        tool.setPath(rs.getString("path"));
+
+        // TEXT[] → List<String>
+        if (rs.getArray("tags") != null) {
+            String[] tagsArray = (String[]) rs.getArray("tags").getArray();
+            tool.setTags(Arrays.asList(tagsArray));
+        }
+
+        tool.setInputSchema(readJson(rs, "input_schema", Map.class));
+        tool.setOutputSchema(readJson(rs, "output_schema", Map.class));
+        tool.setUsageExamples(readJson(rs, "usage_examples", List.class));
+        tool.setDecisionKnowledge(readJson(rs, "decision_knowledge", Map.class));
+        tool.setMetadata(readJson(rs, "metadata", Map.class));
+        tool.setExtra(readJson(rs, "extra", Map.class));
+
+        tool.setCreatedAt(rs.getObject("created_at", OffsetDateTime.class));
+        return tool;
+    }
+
+    public GTool insert(String collId, GTool tool) {
+
+        String sql0 = """
+                INSERT INTO  %s.%s 
+                (gid, name, type, description, path, tags, input_schema, output_schema,
+                 usage_examples, decision_knowledge, created_at, metadata, extra) 
+                VALUES (?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb, ?::jsonb, ?, ?::jsonb, ?::jsonb) 
+                RETURNING *
+                """;
+
+        String sql = String.format(sql0, schema, tableName(collId));
+        return jdbc.queryForObject(sql,
+                new Object[]{
+                        tool.getGid() != null ? tool.getGid() : idGenerator.generateId(),
+                        tool.getName(),
+                        tool.getType(),
+                        tool.getDescription(),
+                        tool.getPath(),
+                        tool.getTags() != null ? tool.getTags().toArray(new String[0]) : null,
+                        toJson(tool.getInputSchema()),
+                        toJson(tool.getOutputSchema()),
+                        toJson(tool.getUsageExamples()),
+                        toJson(tool.getDecisionKnowledge()),
+                        tool.getCreatedAt() != null ? tool.getCreatedAt() : OffsetDateTime.now(ZoneOffset.UTC),
+                        toJson(tool.getMetadata()),
+                        toJson(tool.getExtra())
+                },
+                (rs, rowNum) -> toTool(rs)
+        );
+    }
+
+    @Override
+    public GTool save(String collId, GTool kvs) throws Throwable {
+        GTool ret = insert(collId, kvs);
+        if (ret == null) {
+            return null;
+        }
+        int r2 = indexer.onAdd(collId, ret);
+        return ret;
+    }
+
+    //@Override
+    public List<Integer> saveAll(String collId, List<GBaseKeyValue> kvs) throws Throwable {
+        return List.of();
+    }
+
+    @Override
+    public List<GTool> findAll(String collId) throws Throwable {
+        String sql = String.format("SELECT * FROM %s.%s", schema, tableName(collId));
+        List<GTool> rets = jdbc.query(sql, (rs, rowNum) -> toTool(rs));
+        //List<GBaseKeyValue> rets = GConverter.fromEntity(query);
+        return rets;
+    }
+
+    //    @Override
+    public List<GBaseKeyValue> findAllByIds(String collId, List<Integer> ids) throws Throwable {
+        return List.of();
+    }
+
+    @Override
+    public GTool find(String collId, int id) throws Throwable {
+        String sql = String.format("SELECT * FROM %s.%s WHERE id = ?", schema, tableName(collId));
+        List<GTool> ret = jdbc.query(
+                sql,
+                new Object[]{id},
+                (rs, rowNum) -> toTool(rs)
+        );
+        if (ret == null || ret.isEmpty()) {
+            return null;
+        } else {
+            return ret.get(0);
+        }
+    }
+
+    @Override
+    public GTool findByGid(String collId, String gid) throws Throwable {
+        String sql = String.format("SELECT * FROM %s.%s WHERE gid = ?", schema, tableName(collId));
+        List<GTool> ret = jdbc.query(
+                sql,
+                new Object[]{gid},
+                (rs, rowNum) -> toTool(rs)
+        );
+        if (ret == null || ret.isEmpty()) {
+            return null;
+        } else {
+            return ret.get(0);
+        }
+    }
+
+    @Override
+    public int deleteById(String collId, int id) {
+        String sql = String.format("DELETE FROM %s.%s WHERE id = ? RETURNING *", schema, tableName(collId));
+        List<GTool> ret = jdbc.query(sql,
+                new Object[]{id},
+                (rs, rowNum) -> toTool(rs));
+        if (ret == null || ret.isEmpty()) {
+            return 0;
+        }
+
+        int r2 = indexer.onDelete(collId, ret.get(0));
+        return 1;
+    }
+
+    @Override
+    public int deleteByGid(String collId, String gid) {
+        String sql = String.format("DELETE FROM %s.%s WHERE gid = ? RETURNING *", schema, tableName(collId));
+        List<GTool> ret = jdbc.query(sql,
+                new Object[]{gid},
+                (rs, rowNum) -> toTool(rs));
+        if (ret == null || ret.isEmpty()) {
+            return 0;
+        }
+
+        int r2 = indexer.onDelete(collId, ret.get(0));
+        return 1;
+    }
+
+    @Override
+    public GTool update(String collId, GTool tool) throws Throwable {
+
+        StringBuilder sql = new StringBuilder("UPDATE %s.%s SET ");
+        List<Object> params = new ArrayList<>();
+
+        if (tool.getName() != null) {
+            sql.append("name = ?, ");
+            params.add(tool.getName());
+        }
+        if (tool.getType() != null) {
+            sql.append("type = ?, ");
+            params.add(tool.getType());
+        }
+        if (tool.getDescription() != null) {
+            sql.append("description = ?, ");
+            params.add(tool.getDescription());
+        }
+        if (tool.getPath() != null) {
+            sql.append("path = ?, ");
+            params.add(tool.getPath());
+        }
+        if (tool.getTags() != null) {
+            sql.append("tags = ?, ");
+            params.add(tool.getTags().toArray(new String[0]));
+        }
+        if (tool.getInputSchema() != null) {
+            sql.append("input_schema = ?::jsonb, ");
+            params.add(toJson(tool.getInputSchema()));
+        }
+        if (tool.getOutputSchema() != null) {
+            sql.append("output_schema = ?::jsonb, ");
+            params.add(toJson(tool.getOutputSchema()));
+        }
+        if (tool.getUsageExamples() != null) {
+            sql.append("usage_examples = ?::jsonb, ");
+            params.add(toJson(tool.getUsageExamples()));
+        }
+        if (tool.getDecisionKnowledge() != null) {
+            sql.append("decision_knowledge = ?::jsonb, ");
+            params.add(toJson(tool.getDecisionKnowledge()));
+        }
+        if (tool.getMetadata() != null) {
+            sql.append("metadata = ?::jsonb, ");
+            params.add(toJson(tool.getMetadata()));
+        }
+        if (tool.getExtra() != null) {
+            sql.append("extra = ?::jsonb, ");
+            params.add(toJson(tool.getExtra()));
+        }
+
+        // 去掉最后一个逗号
+        if (sql.charAt(sql.length() - 2) == ',') {
+            sql.setLength(sql.length() - 2);
+        }
+
+        if (tool.getGid() != null) {
+            sql.append(" WHERE gid = ? RETURNING *;");
+            params.add(tool.getGid());
+        } else if (tool.getId() != null) {
+            sql.append(" WHERE id = ? RETURNING *;");
+            params.add(tool.getId());
+        }else{
+            return null;
+        }
+
+        String sql2 = sql.toString();
+        sql2 = String.format(sql2, schema, tableName(collId));
+        GTool ret = jdbc.queryForObject(
+                sql2,
+                params.toArray(),
+                (rs, rowNum) -> toTool(rs)
+        );
+
+        int r2 = indexer.onUpdate(collId, ret);
+        return ret;
+    }
+
+
+    //////////////////////////////
+    //    @Override
+    public List<GBaseKeyValue> findByName(String collId, String name) throws Throwable {
+        return List.of();
+    }
+
+    //    @Override
+    public List<GBaseKeyValue> findByPath(String collId, String path) {
+        return List.of();
+    }
+
+    //    @Override
+    public List<GBaseKeyValue> findByPathPrefix(String collId, String prefix) {
+        return List.of();
+    }
+
+    //    @Override
+    public long count(String collId) {
+        return 0;
+    }
+
+
+    //    @Override
+    public long delete(String collId, List<Integer> ids) {
+        return 0;
+    }
+
+    //    @Override
+    public long deleteAll(String collId) {
+        return 0;
+    }
+
+    //    @Override
+    public GBaseKeyValue removeAttribute(String collId, Integer id, List<String> keys) throws Throwable {
+        return null;
+    }
+
+    //    @Override
+    public GBaseKeyValue updateAttribute(String collId, Integer id, String key, Object value) throws Throwable {
+        return null;
+    }
+
+    //    @Override
+    public GBaseKeyValue updateAttribute(String collId, Integer id, Map<String, Object> attributes) throws Throwable {
+        return null;
+    }
+
+    //    @Override
+    public Object findAttribute(String collId, Integer id, String attribute) throws SQLException {
+        return null;
+    }
+
+    //    @Override
+    public long update(String collId, List<GBaseKeyValue> kvs) throws Throwable {
+        return 0;
+    }
+
+    //    @Override
+    public List<Map<String, Object>> getAllEntities(String collId, List<String> fields) {
+        return List.of();
+    }
+
+    //    @Override
+    public List<Map<String, Object>> getAllEntities(String collId, List<String> fields, String whereClause) {
+        return List.of();
+    }
+
+    //    @Override
+    public List<String> appendArrayField(String collId, Integer id, String field, List<String> values) {
+        return List.of();
+    }
+
+    //    @Override
+    public List<String> setArrayField(String collId, Integer id, String field, List<String> values) {
+        return List.of();
+    }
+
+    //    @Override
+    public List<String> removeArrayField(String collId, Integer id, String field, List<String> values) {
+        return List.of();
+    }
+
+    @Override
+    public int createTable(String collId) {
+        List<String> sqls = DkConfig.createTable(schema, tableName(collId));
+        String sql2 = String.join("\n", sqls);
+        jdbc.execute(sql2);
+        return 1;
+    }
+
+    @Override
+    public int deleteTable(String collId) {
+        List<String> sqls = DkConfig.dropTable(schema, tableName(collId));
+        String sql2 = String.join("\n", sqls);
+        jdbc.execute(sql2);
+        return 1;
+    }
+
+    //    @Override
+    public int deleteByPath(String collId, String path) {
+        return 0;
+    }
+
+    //    @Override
+    public int deleteByPrefix(String collId, String path) {
+        return 0;
+    }
+
+    //    @Override
+    public int deletePathAndDescendants(String collId, String path) {
+        return 0;
+    }
+
+    //    @Override
+    public List<GBaseKeyValue> getChildren(String collId, String parent) {
+        return List.of();
+    }
+}

+ 21 - 0
server/src/main/java/com/giantan/data/dk/repository/DkTaskRepository.java

@@ -0,0 +1,21 @@
+package com.giantan.data.dk.repository;
+
+import com.giantan.data.tasks.repository.DynamicTaskRepository;
+import jakarta.annotation.PostConstruct;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class DkTaskRepository extends DynamicTaskRepository {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(DkTaskRepository.class);
+
+    public DkTaskRepository(JdbcTemplate jdbc) {
+        super(jdbc);
+    }
+
+    @PostConstruct
+    public void init() {
+        setSchema("dkdb", "tasks_");
+    }
+}

+ 22 - 0
server/src/main/java/com/giantan/data/dk/repository/DkTaxonomyRepository.java

@@ -0,0 +1,22 @@
+package com.giantan.data.dk.repository;
+
+import com.giantan.data.taxonomy.repository.DynamicTaxonomyRepository;
+import jakarta.annotation.PostConstruct;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class DkTaxonomyRepository extends DynamicTaxonomyRepository {
+
+    //private JdbcTemplate jdbc;
+
+    public DkTaxonomyRepository(JdbcTemplate jdbc) {
+        super(jdbc);
+    }
+
+    @PostConstruct
+    public void init() {
+        setSchema("dkdb", "taxonomy_");
+    }
+
+}

+ 36 - 0
server/src/main/java/com/giantan/data/dk/repository/IDkIndexer.java

@@ -0,0 +1,36 @@
+package com.giantan.data.dk.repository;
+
+import com.giantan.data.dk.dto.GTool;
+import com.giantan.data.qa.service.IQaCollectionService;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface IDkIndexer {
+
+    int onAdd(String collection, GTool entity) throws IOException, InterruptedException;
+
+    int onAdd(String collection, List<GTool> entities) throws IOException, InterruptedException;
+
+    int onDelete(String collection, GTool entity);
+
+    int onDelete(String collection, List<GTool> entities);
+
+    int onDeleteAll(String collection);
+
+    int onUpdate(String collection, GTool entity) throws IOException, InterruptedException;
+
+    int onUpdateField(String collection, GTool ret, String field) ;
+
+    int onUpdateField(String collection, List<GTool> rets, String field);
+
+    int createCollection(String collection);
+
+    int deleteCollection(String collection);
+
+
+    // 用于获取collection的attributes 信息
+    void init(IQaCollectionService collectionService);
+
+    int clearCollection(String collection) throws IOException, InterruptedException;
+}

+ 82 - 0
server/src/main/java/com/giantan/data/dk/repository/IGDkRepository.java

@@ -0,0 +1,82 @@
+package com.giantan.data.dk.repository;
+
+import com.giantan.data.dk.dto.GTool;
+
+import java.util.List;
+
+public interface IGDkRepository {
+
+    List<GTool> findAll(String collId) throws Throwable;
+
+    GTool find(String collId, int id) throws Throwable;
+
+    GTool findByGid(String collId, String gid) throws Throwable;
+
+    GTool update(String collId, GTool tool) throws Throwable;
+
+    int createTable(String collId);
+
+    int deleteTable(String collId);
+
+    GTool save(String collId, GTool kvs) throws Throwable;
+
+    int deleteById(String collId, int id);
+
+    int deleteByGid(String collId, String gid);
+//    GBaseKeyValue save(String collId, GBaseKeyValue kvs) throws Throwable;
+//
+//    List<Integer> saveAll(String collId, List<GBaseKeyValue> kvs) throws Throwable;
+//
+//    List<GBaseKeyValue> findAllByIds(String collId, List<Integer> ids) throws Throwable;
+//
+//    List<GBaseKeyValue> findByName(String collId, String name) throws Throwable;
+//
+//    List<GBaseKeyValue> findByPath(String collId, String path);
+//
+//    List<GBaseKeyValue> findByPathPrefix(String collId, String prefix);
+//
+//    long count(String collId);
+//
+//    long delete(String collId, Integer id);
+//
+//    long delete(String collId, List<Integer> ids);
+//
+//    long deleteAll(String collId);
+//
+//    GBaseKeyValue update(String collId, GBaseKeyValue kv) throws Throwable;
+//
+//    GBaseKeyValue removeAttribute(String collId, Integer id, List<String> keys) throws Throwable;
+//
+//    GBaseKeyValue updateAttribute(String collId, Integer id, String key, Object value) throws Throwable;
+//
+//    GBaseKeyValue updateAttribute(String collId, Integer id, Map<String, Object> attributes) throws Throwable;
+//
+//    Object findAttribute(String collId, Integer id, String attribute) throws SQLException;
+//
+//    long update(String collId, List<GBaseKeyValue> kvs) throws Throwable;
+//
+//    List<Map<String, Object>> getAllEntities(String collId, List<String> fields);
+//
+//    List<Map<String, Object>> getAllEntities(String collId, List<String> fields, String whereClause);
+//
+//    List<String> appendArrayField(String collId, Integer id, String field, List<String> values);
+//
+//    List<String> setArrayField(String collId, Integer id, String field, List<String> values);
+//
+//    List<String> removeArrayField(String collId, Integer id, String field, List<String> values);
+//
+
+//
+//    int deleteByPath(String collId, String path);
+//
+//    int deleteByPrefix(String collId, String path);
+//
+//    //DELETE FROM %s.%s
+//    //WHERE (path LIKE ? OR path = '/ab')
+//    //RETURNING *;
+//    // 上述的deleteByPrefix 只要是前缀都删除  例如 删 /ab  那么 /abc 也删了
+//    // 而该方法 不会
+//    int deletePathAndDescendants(String collId, String path);
+//
+//    List<GBaseKeyValue> getChildren(String collId, String parent);
+}

+ 365 - 0
server/src/main/java/com/giantan/data/dk/service/DkCollectionService.java

@@ -0,0 +1,365 @@
+package com.giantan.data.dk.service;
+
+import com.giantan.data.dk.repository.*;
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+import com.giantan.data.kvs.repository.GEntityConfig;
+import com.giantan.data.kvs.repository.GRepository;
+import com.giantan.data.mds.service.CollectionInstance;
+import com.giantan.data.mds.service.impl.MdCores;
+import com.giantan.data.qa.service.IQaCollectionService;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class DkCollectionService implements IQaCollectionService {
+    private static final org.slf4j.Logger log
+            = org.slf4j.LoggerFactory.getLogger(DkCollectionService.class);
+
+    public static final String GKG_CATALOG = "gkg_catalog";
+    // 在 GKG_CATALOG 系统目录下的 table name
+    public static final String GKG_ENTRY_COLLECTIONS = "dk_collections";
+
+    @Autowired
+    protected JdbcTemplate jdbcTemplate;
+
+    protected GRepository collections;
+
+    @Autowired
+    DkRepository dkRepository;
+
+    @Autowired
+    DkTaxonomyRepository dkTaxonomyRepository;
+
+    @Autowired
+    DkIndexer dkIndexer;
+
+    @Autowired
+    DkTaskRepository dkTaskRepository;
+
+    @Autowired
+    DkExtraRepository dkExtraRepository;
+
+    protected MdCores mdCores;
+
+    public DkCollectionService() {
+
+    }
+
+    @PostConstruct
+    public void init() {
+        collections = new GRepository(GKG_CATALOG, GKG_ENTRY_COLLECTIONS, jdbcTemplate);
+        mdCores = new MdCores();
+        dkIndexer.init(this);
+    }
+
+    private int isIntId(String mdId) {
+        if (mdId.length() > 12) {
+            return -1;
+        } else {
+            try {
+                int i = Integer.parseInt(mdId);
+                return i;
+            } catch (Exception e) {
+
+            }
+        }
+        return -1;
+    }
+
+    public int getCollectionId(String name) {
+        CollectionInstance instance = mdCores.getByName(name);
+        if (instance != null) {
+            return instance.getId();
+        } else {
+            try {
+                List<GBaseKeyValue> r = collections.findByName(name);
+                if (r != null && r.size() > 0) {
+                    GBaseKeyValue kv = r.get(0);
+                    instance = CollectionInstance.build(kv.getIntId(), kv.getGid(), kv.getName(), kv.get(GEntityConfig.ATTRIBUTES));
+                    mdCores.put(name, instance);
+                    return instance.getId();
+                }else{
+                    log.warn("Collection with name '" + name + "' not exist.");
+                    throw new ResponseStatusException(HttpStatus.NOT_FOUND,
+                            "Collection with name '" + name + "' does not exist.");
+                }
+            } catch (ResponseStatusException e) {
+                // 直接透传,不要覆盖
+                throw e;
+            } catch (Throwable e) {
+                log.error("Failed to get collection by name={}", name, e);
+                throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
+                        "Error occurred while retrieving collection with name '" + name + "'", e);
+
+            }
+            //throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
+            //        "Collection with name '" + name + "' not exist.");
+        }
+    }
+
+    @Override
+    public String getCollectionName(int id) {
+        CollectionInstance instance = mdCores.getById(id);
+        if (instance != null) {
+            return instance.getName();
+        }
+        return null;
+    }
+
+    public int getCollectionOrNew(String name) {
+        CollectionInstance collection = mdCores.getByName(name);
+        if (collection == null) {
+            try {
+                List<GBaseKeyValue> keys = collections.findByName(name);
+                GBaseKeyValue r = null;
+                if (isEmpty(keys)) {
+                    //r = createCollection(name);
+                    r = creatingCollection(name);
+                } else {
+                    r = keys.get(0);
+                }
+                CollectionInstance instance = CollectionInstance.build(r.getIntId(), r.getGid(), r.getName(), r.get(GEntityConfig.ATTRIBUTES));
+                mdCores.put(name, instance);
+                return instance.getId();
+            } catch (Throwable e) {
+                throw new RuntimeException(e);
+            }
+            // ICollection collection1 = buildCollection(id);
+            //stores.put(name, collection1);
+            //collection = collection1;
+        }
+        return collection.getId();
+    }
+
+    public boolean exists(String name) throws Throwable {
+        List<GBaseKeyValue> keys = collections.findByName(name);
+        if (isEmpty(keys)) {
+            return false;
+        }
+        return true;
+    }
+
+    public GBaseKeyValue getKvByName(String name) throws Throwable {
+        List<GBaseKeyValue> keys = collections.findByName(name);
+        if (isEmpty(keys)) {
+            return null;
+        }
+        return keys.get(0);
+    }
+
+    public List<GBaseKeyValue> getAllCollections() throws Throwable {
+        List<GBaseKeyValue> rets = collections.findAll();
+        return rets;
+    }
+
+    protected boolean isEmpty(List ls) {
+        if (ls == null || ls.size() == 0) {
+            return true;
+        }
+        return false;
+    }
+
+    private void onCollectionUpdate(String name, GBaseKeyValue entity) {
+        if (entity != null) {
+            CollectionInstance info = mdCores.getByName(name);
+            info.updateAttributes(entity.get(GEntityConfig.ATTRIBUTES));
+        }
+    }
+
+    //@Override
+    public GBaseKeyValue updateCollection(String name, Map<String, Object> entity) throws Throwable {
+        //GBaseKeyValue kv = getKvByName(name);
+        //if (kv == null) {
+        //    return null;
+        //}
+
+        int collectionId = getCollectionId(name);
+        GBaseKeyValue gkv = new GBaseKeyValue(entity);
+        gkv.put("id", collectionId);
+        GBaseKeyValue updated = collections.update(gkv);
+
+        onCollectionUpdate(name, updated);
+        return updated;
+    }
+
+    public Map<String, Object> getCollectionAttributes(String name) throws Throwable {
+        GBaseKeyValue ret = getKvByName(name);
+        if (ret == null) {
+            return null;
+        }
+        return (Map<String, Object>) ret.get(GEntityConfig.ATTRIBUTES);
+    }
+
+    public Map<String, Object> updateCollectionAttributes(String name, Map<String, Object> attributes) throws Throwable {
+        int id = getCollectionId(name);
+        GBaseKeyValue ret = collections.updateAttribute(id, attributes);
+        onCollectionUpdate(name, ret);
+        return (Map<String, Object>) ret.get(GEntityConfig.ATTRIBUTES);
+    }
+
+    public Map<String, Object> removeCollectionAttribute(String name, List<String> keys) throws Throwable {
+        int id = getCollectionId(name);
+        GBaseKeyValue ret = collections.removeAttribute(id, keys);
+        onCollectionUpdate(name, ret);
+        return (Map<String, Object>) ret.get(GEntityConfig.ATTRIBUTES);
+    }
+
+    private void removeFromStore(String name) {
+        CollectionInstance remove = mdCores.remove(name);
+    }
+
+
+    public long deleteCollection(String name) throws Throwable {
+
+        GBaseKeyValue kv = getKvByName(name);
+        if (kv == null) return 0;
+
+        Integer intId = kv.getIntId();
+        String id = kv.getId();
+
+        removeFromStore(name);
+
+        //deleteEntryTable2(KvConfig.ENTRY_SCHEMA, getTableName(intId.toString()));
+        try {
+            dkRepository.deleteTable(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        try {
+            dkTaxonomyRepository.deleteTable(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        try {
+            dkIndexer.deleteCollection(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        try {
+            dkTaskRepository.deleteTable(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        try {
+            dkExtraRepository.deleteTable(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        long deleted = collections.delete(intId);
+
+        return deleted;
+    }
+
+    public long clearCollection(String name) throws Throwable {
+        GBaseKeyValue kv = getKvByName(name);
+        if (kv == null) return 0;
+
+        String id = kv.getId();
+        try {
+            dkRepository.deleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        try {
+            dkTaxonomyRepository.deleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        try {
+            dkIndexer.onDeleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        try {
+            dkTaskRepository.deleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+
+        try {
+            dkExtraRepository.deleteAll(id);
+        } catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        return 1;
+    }
+
+    public GBaseKeyValue createCollection(String name) throws Throwable {
+        if (exists(name)) {
+            throw new ResponseStatusException(HttpStatus.CONFLICT,
+                    "Collection with name '" + name + "' already exists.");
+        }
+        return creatingCollection(name);
+    }
+
+    protected GBaseKeyValue creatingCollection(String name) throws Throwable {
+        GBaseKeyValue kv = GBaseKeyValue.build();
+        kv.setName(name);
+        GBaseKeyValue ret = collections.save(kv);
+
+        //// 创建 table
+        String id = ret.getId();
+        if (id == null) {
+            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
+                    "Collection with name '" + name + "' create failed.");
+        }
+
+        //createEntryTable(KvConfig.ENTRY_SCHEMA, getTableName(id));
+        dkRepository.createTable(id);
+        dkTaxonomyRepository.createTable(id);
+        dkIndexer.createCollection(id);
+        dkTaskRepository.createTable(id);
+        dkExtraRepository.createTable(id);
+
+        return ret;
+    }
+
+    @Override
+    public Map<String, Object> getAttibutes(String collId) {
+        CollectionInstance instance = mdCores.getById(Integer.parseInt(collId));
+        if (instance == null) {
+            return null;
+        }
+        return instance.getAttributes();
+    }
+
+    @Override
+    public Map<String, Object> getAttributesByName(String name) {
+        int collId = getCollectionId(name);
+        CollectionInstance instance = mdCores.getById(collId);
+        //CollectionInstance instance = mdCores.getByName(name);
+        if (instance == null) {
+            return null;
+        }
+        return instance.getAttributes();
+    }
+
+//    public GBaseKeyValue updateChunkMode(String name, Map<String, Object> attributes) throws Throwable {
+//        GBaseKeyValue ret = getKvByName(name);
+//        if (ret == null) {
+//            return null;
+//        }
+//        int id = ret.getIntId();
+//
+//
+//        Object o = attributes.get(QaConstants.CHUNK_MODE);
+//        if (o != null){
+//            GBaseKeyValue r = collections.updateAttribute(id, QaConstants.CHUNK_MODE, o);
+//            return r;
+//        }
+//        //return getCollectionAttributes(id);
+//        return collections.find(id);
+//    }
+}

+ 102 - 0
server/src/main/java/com/giantan/data/dk/service/DkDocsService.java

@@ -0,0 +1,102 @@
+package com.giantan.data.dk.service;
+
+import com.giantan.data.dk.dto.GTool;
+import com.giantan.data.dk.repository.DkRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class DkDocsService {
+
+    @Autowired
+    DkCollectionService dkCollectionService;
+
+    @Autowired
+    DkRepository dkRepository;
+
+    protected Integer getCollId(String coll) {
+        int id = dkCollectionService.getCollectionId(coll);
+        if (id <= 0) {
+            return null;
+        }
+        return id;
+    }
+
+    protected String getStrOfCollId(String coll) {
+        int id = dkCollectionService.getCollectionId(coll);
+        if (id <= 0) {
+            return null;
+        }
+        return Integer.toString(id);
+    }
+
+
+    private int isIntId(String mdId) {
+        if (mdId.length() > 12) {
+            return -1;
+        } else {
+            try {
+                int i = Integer.parseInt(mdId);
+                return i;
+            } catch (Exception e) {
+
+            }
+        }
+        return -1;
+    }
+
+
+    public GTool save(String coll, GTool data) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        GTool r = dkRepository.save(collId, data);
+        return r;
+    }
+
+    public List<GTool> findAll(String coll) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        if (collId != null) {
+            List<GTool> rets = dkRepository.findAll(collId);
+            return rets;
+        }
+        return null;
+    }
+
+    public GTool findByIdOrGid(String coll, String qid) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        int id = isIntId(qid);
+        GTool r = null;
+        if (id >= 0) {
+            r = dkRepository.find(collId, id);
+        } else {
+            r = dkRepository.findByGid(collId, qid);
+        }
+        return r;
+    }
+
+    public int deleteByIdOrGid(String coll, String qid) {
+        String collId = getStrOfCollId(coll);
+        int id = isIntId(qid);
+        int r = 0;
+        if (id >= 0) {
+            r = dkRepository.deleteById(collId, id);
+        } else {
+            r = dkRepository.deleteByGid(collId, qid);
+        }
+        return r;
+    }
+
+    public GTool update(String coll, String qid, GTool tool) throws Throwable {
+        String collId = getStrOfCollId(coll);
+        int id = isIntId(qid);
+        if (id >= 0) {
+            tool.setId(Long.valueOf(id));
+        }else{
+            tool.setGid(qid);
+        }
+        GTool r = dkRepository.update(collId, tool);
+        return r;
+    }
+
+}

+ 91 - 0
server/src/main/java/com/giantan/data/dk/service/IDkDocsService.java

@@ -0,0 +1,91 @@
+package com.giantan.data.dk.service;
+
+import com.giantan.data.kvs.kvstore.GBaseKeyValue;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public interface IDkDocsService {
+    long deleteAll(String coll) throws Exception;
+
+    List<GBaseKeyValue> findAll(String coll) throws Throwable;
+
+    //
+
+
+    GBaseKeyValue findByGid(String coll, String gid) throws Throwable;
+
+    //    String collId = getStrOfCollId(coll);
+//        int intId = getIntId(coll, nodeId);
+//        String path = getPath(collId, intId);
+//        List<GBaseKeyValue> ret = qaDynamicRepository.findByPath(collId, path);
+//        return ret;
+    List<GBaseKeyValue> findByPath(String coll, String path) throws Throwable;
+
+    List<GBaseKeyValue> findByPrefix(String coll, String prefix) throws Throwable;
+
+    GBaseKeyValue findByMdid(String coll, String mdId) throws Throwable;
+
+    int delete(String coll, String gid) throws Throwable;
+
+    int deleteByMdid(String coll, String gid) throws Throwable;
+
+    int deleteByName(String coll, String name) throws Throwable;
+
+    int deleteByPath(String collId, String path);
+    // 严格按 目录 删除
+    int deletePathAndDescendants(String coll, String path) throws Throwable;
+
+    int deleteByPrefix(String coll, String prefix) throws Throwable;
+
+    //Object getMetadataByKey(String coll, String mdId, String key) throws Throwable;
+
+    Object getAttributes(String coll, String mdId) throws Throwable;
+
+    Object getAttributeByKey(String coll, String mdId, String key) throws Throwable;
+
+    //Object patchMetadata(String coll, String mdId, GBaseKeyValue data) throws Throwable;
+
+    Object patchAttributes(String coll, String mdId, Map<String, Object> data) throws Throwable;
+
+    Object deleteAttributeByKey(String coll, String mdId, String key) throws Throwable;
+
+    GBaseKeyValue deleteAttributeByKeys(String coll, String mdId, List<String> keys) throws Throwable;
+
+    long deleteCollection(String coll) throws Throwable;
+
+    Map<String, Object> rename(String coll, String mdId, Map<String, Object> req) throws Throwable;
+
+    GBaseKeyValue save(String coll, GBaseKeyValue entity) throws Throwable;
+
+    List<Integer> saveAll(String coll, List<GBaseKeyValue> kvs) throws Throwable;
+
+    GBaseKeyValue update(String coll, GBaseKeyValue kv) throws Throwable;
+
+    GBaseKeyValue updateAttribute(String coll, Integer docId, Map<String, Object> attributes) throws Throwable;
+
+    List<Map<String, Object>> getAllEntities(String coll, List<String> fields);
+
+    long count(String coll);
+
+    List<String> appendArrayField(String coll, int docId, String tags, List<String> values);
+
+    List<String> setArrayField(String coll, int docId, String tags, List<String> values);
+
+    List<String> removeArrayField(String coll, int docId, String tags, List<String> values);
+
+    String getIndexName(String coll);
+
+    List<GBaseKeyValue> fulltextSearch(String coll, Map<String, Object> query) throws Throwable;
+    List<GBaseKeyValue> similaritySearch(String coll, Map<String, Object> query) throws Throwable;
+    List<GBaseKeyValue> hybridSearch(String coll, Map<String, Object> query) throws Throwable;
+
+    GBaseKeyValue findByIdOrGid(String coll, String id) throws Throwable;
+
+    List<GBaseKeyValue> getChildren(String coll, String parent);
+
+    int deleteAllIndex(String coll) throws IOException, InterruptedException;
+
+    int fullIndexing(String coll) throws IOException, InterruptedException;
+}

+ 1 - 1
server/src/main/java/com/giantan/data/mds/MdsApplication.java

@@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
 
 @SpringBootApplication
 @EnableAsync
-@ComponentScan(basePackages = {"com.giantan.data.mds", "com.giantan.data.index", "com.giantan.data.qa", "com.giantan.data.common"})
+@ComponentScan(basePackages = {"com.giantan.data.mds", "com.giantan.data.index", "com.giantan.data.qa", "com.giantan.data.dk", "com.giantan.data.common"})
 //@ComponentScan("com.giantan.data.se")
 public class MdsApplication {
     private static final org.slf4j.Logger log

+ 3 - 1
server/src/main/java/com/giantan/data/mds/service/impl/MdCores.java

@@ -29,7 +29,9 @@ public class MdCores {
 
     public CollectionInstance remove(String name){
         CollectionInstance removed = stores.remove(name);
-        idstores.remove(removed.getId());
+        if (removed != null) {
+            idstores.remove(removed.getId());
+        }
         return removed;
     }
 

+ 5 - 5
server/src/main/java/com/giantan/data/qa/controller/QaDocsController.java

@@ -52,15 +52,15 @@ public class QaDocsController {
     QaDocsService qaDocsService;
 
     @PostMapping("/docs")
-    public ResponseEntity createEntry(@PathVariable String collId, @RequestBody Map<String, Object> data) throws Throwable {
+    public R createEntry(@PathVariable String collId, @RequestBody Map<String, Object> data) throws Throwable {
         GBaseKeyValue r = qaDocsService.save(collId, GBaseKeyValue.build(data));
-        return ResponseEntity.ok(r);
+        return R.data(r);
     }
 
     @PostMapping("/docs/batch")
-    public ResponseEntity<R> createBatch(@PathVariable String collId, @RequestBody List<GBaseKeyValue> kvs) throws Throwable {
+    public R createBatch(@PathVariable String collId, @RequestBody List<GBaseKeyValue> kvs) throws Throwable {
         List<Integer> ret = qaDocsService.saveAll(collId, kvs);
-        return ResponseEntity.ok(R.data(ret));
+        return R.data(ret);
     }
 
     @GetMapping("/docs/{id}")
@@ -81,7 +81,7 @@ public class QaDocsController {
         //fileProcessingService.processAsyncDirect(collId, taskId, file, params);
         //System.out.println("params = " + params);
         int deleted = qaDocsService.deleteByMdid(collId, gid);
-        log.info("删除文件: {}", gid);
+        log.info("Delete qa: {}", gid);
         //log.info("taskId = " + taskId);
         return R.data(Map.of("deleted", deleted));
     }

+ 0 - 1
server/src/main/java/com/giantan/data/qa/controller/QaTaxonomyController.java

@@ -7,7 +7,6 @@ import com.giantan.data.qa.constant.QaConstants;
 import com.giantan.data.qa.service.QaTaxonomyService;
 import com.giantan.data.taxonomy.model.TaxonomyNode;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
 import java.lang.invoke.MethodHandles;

+ 0 - 2
server/src/main/java/com/giantan/data/qa/service/IQaCollectionService.java

@@ -1,7 +1,5 @@
 package com.giantan.data.qa.service;
 
-import com.giantan.data.mds.service.CollectionInstance;
-
 import java.util.Map;
 
 public interface IQaCollectionService {

+ 2 - 3
server/src/main/java/com/giantan/data/qa/service/QaCollectionService.java

@@ -221,11 +221,8 @@ public class QaCollectionService implements IQaCollectionService {
         Integer intId = kv.getIntId();
         String id = kv.getId();
 
-        long deleted = collections.delete(intId);
-
         removeFromStore(name);
 
-        //deleteEntryTable2(KvConfig.ENTRY_SCHEMA, getTableName(intId.toString()));
         try {
             qaRepository.deleteTable(id);
         } catch (Exception e) {
@@ -255,6 +252,8 @@ public class QaCollectionService implements IQaCollectionService {
             log.error(e.getMessage());
         }
 
+        long deleted = collections.delete(intId);
+
         return deleted;
     }
 

+ 51 - 0
server/src/test/java/com/giantan/data/mds/DkRepositoryTest.java

@@ -0,0 +1,51 @@
+package com.giantan.data.mds;
+
+import com.giantan.data.dk.constant.DkConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.List;
+
+@SpringBootTest
+public class DkRepositoryTest {
+//    public static final String GKG_CATALOG = "gkg_catalog";
+//    //////////////
+//    // 有关 entry的数据库表
+//    public static final String GKG_ENTRIES = "entries2";
+//
+//    public static final String GKG_GENTRY = "demo1";
+
+
+    @Autowired
+    JdbcTemplate jdbcTemplate;
+
+    @Test
+    void createTable() throws Throwable {
+        //List<String> sqls = GEntityConfig.createGEntityTable(GKG_CATALOG, EntryConfig.GKG_ENTRY_COLLECTIONS);
+        //List<String> sqls = GEntityConfig.createGEntityTable(KvConfig.GKG_CATALOG, KvConfig.GKG_ENTRY_COLLECTIONS);
+        List<String> sqls = DkConfig.createTable("dkdb", "demo1");
+        String result = String.join("\n", sqls);
+        System.out.println(result);
+        jdbcTemplate.execute(result);
+    }
+
+    @Test
+    void dropTable() throws Throwable {
+        List<String> sqls = DkConfig.dropTable("dkdb", "demo1");
+        String result = String.join("\n", sqls);
+        System.out.println(result);
+        jdbcTemplate.execute(result);
+    }
+
+
+    @Test
+    void clearTable() throws Throwable {
+        List<String> sqls = DkConfig.clearAll("dkdb", "demo1");
+        String result = String.join("\n", sqls);
+        System.out.println(result);
+        jdbcTemplate.execute(result);
+    }
+
+}