| New file |
| | |
| | | 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 |
| | | } |