From cb72759b568baf34281bd15f821ac9834c17bbb9 Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Thu, 03 Sep 2026 14:24:29 +0800
Subject: [PATCH] merge: 合并 origin/master schema v2 重构(RWR/增量对账/explore),保留 tag/entity 实验于 experiment 分支 + 本地 expandPath 修复与 draft list 过滤
---
cmd/explore.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 111 insertions(+), 0 deletions(-)
diff --git a/cmd/explore.go b/cmd/explore.go
new file mode 100644
index 0000000..eb808c3
--- /dev/null
+++ b/cmd/explore.go
@@ -0,0 +1,111 @@
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/aisim/kb-cli/internal/index"
+ "github.com/aisim/kb-cli/internal/llm"
+ "github.com/aisim/kb-cli/internal/search"
+ "github.com/spf13/cobra"
+)
+
+var (
+ exploreBudget int
+ exploreTopN int
+ exploreJSON bool
+)
+
+var exploreCmd = &cobra.Command{
+ Use: "explore <问题>",
+ Short: "一次调用获取相关文档原文 + 关联清单(供 agent 使用)",
+ Long: `# explore - 精准上下文
+kb-cli explore <问题> [--budget 字节] [--top N] [--json] # 按字节预算返回相关文档原文、关联文档、悬空链接`,
+ Args: cobra.MinimumNArgs(1),
+ RunE: runExplore,
+}
+
+func init() {
+ rootCmd.AddCommand(exploreCmd)
+ exploreCmd.Flags().IntVar(&exploreBudget, "budget", 0, "字节预算(0=配置默认 16000)")
+ exploreCmd.Flags().IntVar(&exploreTopN, "top", 0, "文档数(0=配置默认 5)")
+ exploreCmd.Flags().BoolVar(&exploreJSON, "json", false, "JSON 输出")
+
+ // 参数不足时显示 help(与 search 命令同款模式)
+ exploreCmd.SetUsageTemplate(exploreCmd.Long)
+}
+
+func runExplore(cmd *cobra.Command, args []string) error {
+ // 打开索引
+ store, err := index.Open(dbPath)
+ if err != nil {
+ return fmt.Errorf("打开索引失败: %w", err)
+ }
+ defer store.Close()
+
+ // pre-flight:快速检查索引是否有变更(只 stat 比对,不读内容)
+ dirty, err := index.QuickCheck(store, vaultPath)
+ if err != nil {
+ return fmt.Errorf("检查索引状态失败: %w", err)
+ }
+
+ if dirty {
+ fmt.Fprintln(os.Stderr, "索引有变更,正在增量同步...")
+ if err := syncIndex(store); err != nil {
+ return fmt.Errorf("增量同步失败: %w", err)
+ }
+ }
+
+ // 关键词按词拆分(与 search 一致):长复合词走 LIKE 整串匹配,文档里
+ // "电子秤补气失败" 通常不是连续子串,拆成 电子秤/补气失败 才能命中
+ var allKeywords []string
+ for _, arg := range args {
+ allKeywords = append(allKeywords, strings.Fields(arg)...)
+ }
+ if len(allKeywords) == 0 {
+ return fmt.Errorf("请提供至少一个关键词")
+ }
+
+ // 默认值优先读配置(config.yaml 的 explore 节),读不到再用代码缺省
+ budget := exploreBudget
+ topN := exploreTopN
+ cfg := llm.LoadConfig()
+ if budget <= 0 && cfg.Explore.DefaultBudget > 0 {
+ budget = cfg.Explore.DefaultBudget
+ }
+ if topN <= 0 && cfg.Explore.TopN > 0 {
+ topN = cfg.Explore.TopN
+ }
+ hardBudget := cfg.Explore.HardBudget // 0 = Explore 内部用缺省 32000
+
+ res, err := search.Explore(store, allKeywords, search.ExploreOptions{Budget: budget, TopN: topN, HardBudget: hardBudget})
+ if err != nil {
+ return fmt.Errorf("explore 失败: %w", err)
+ }
+ if len(res.Docs) == 0 {
+ fmt.Println("未找到相关文档")
+ return nil
+ }
+
+ if exploreJSON {
+ b, _ := json.MarshalIndent(res, "", " ")
+ fmt.Println(string(b))
+ return nil
+ }
+ for _, d := range res.Docs {
+ fmt.Printf("## %s(%s,score %d)\n", d.Title, d.Path, d.Score)
+ fmt.Print(d.Body)
+ fmt.Println()
+ if rel, ok := res.Related[d.Path]; ok && len(rel) > 0 {
+ fmt.Printf("关联: %s\n", strings.Join(rel, ", "))
+ }
+ fmt.Println()
+ }
+ if len(res.UnresolvedLinks) > 0 {
+ fmt.Printf("⚠️ 悬空链接: %s\n", strings.Join(res.UnresolvedLinks, ", "))
+ }
+ fmt.Println("以上为文档原文直出,agent 无需再读文件")
+ return nil
+}
--
Gitblit v1.10.0