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 }