package cmd import ( "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/aisim/kb-cli/internal/index" "github.com/spf13/cobra" ) var indexCmd = &cobra.Command{ Use: "index", Short: "索引管理", } var indexBuildCmd = &cobra.Command{ Use: "build", Short: "构建或重建知识库索引", Long: `kb-cli index build [--vault=<路径>] [--db=<路径>] # 构建或重建知识库索引`, RunE: runIndexBuild, } var indexStatusCmd = &cobra.Command{ Use: "status", Short: "查看索引状态信息", Long: `kb-cli index status [--vault=<路径>] [--db=<路径>] # 查看索引状态(节点数、边数、commit、构建时间)`, RunE: runIndexStatus, } var indexGcCmd = &cobra.Command{ Use: "gc", Short: "清理孤立节点和边", Long: `kb-cli index gc [--vault=<路径>] [--db=<路径>] # 清理索引中文件已删除的孤立节点和边`, RunE: runIndexGc, } var gitCmd = &cobra.Command{ Use: "git", Short: "Git 同步操作", } var gitSyncCmd = &cobra.Command{ Use: "sync", Short: "同步知识库到 Git 仓库", Long: `kb-cli git sync [--vault=<路径>] # 执行 git add、commit、push 同步知识库`, RunE: runGitSync, } func init() { rootCmd.AddCommand(indexCmd) indexCmd.AddCommand(indexBuildCmd) indexCmd.AddCommand(indexStatusCmd) indexCmd.AddCommand(indexGcCmd) rootCmd.AddCommand(gitCmd) gitCmd.AddCommand(gitSyncCmd) } func runIndexBuild(cmd *cobra.Command, args []string) error { // 打开索引 store, err := index.Open(dbPath) if err != nil { return fmt.Errorf("打开索引失败: %w", err) } defer store.Close() // 获取当前 commit commit, err := index.GetGitCommit(vaultPath) if err != nil { fmt.Fprintln(os.Stderr, "警告: 无法获取 git commit:", err) commit = "" } fmt.Fprintln(os.Stderr, "正在重建索引...") if err := rebuildIndex(store, commit); err != nil { return fmt.Errorf("重建索引失败: %w", err) } fmt.Fprintln(os.Stderr, "索引重建完成") return nil } func runIndexStatus(cmd *cobra.Command, args []string) error { // 打开索引 store, err := index.Open(dbPath) if err != nil { return fmt.Errorf("打开索引失败: %w", err) } defer store.Close() // 获取元信息 gitCommit, err := store.GetMeta("git_commit") if err != nil { return fmt.Errorf("获取元信息失败: %w", err) } builtAt, _ := store.GetMeta("built_at") // 获取节点数 nodeCount, err := store.NodeCount() if err != nil { return fmt.Errorf("获取节点数失败: %w", err) } // 获取边数 edgeCount, err := store.EdgeCount() if err != nil { return fmt.Errorf("获取边数失败: %w", err) } // 获取当前 commit currentCommit, err := index.GetGitCommit(vaultPath) if err != nil { currentCommit = "" } // 输出状态 fmt.Printf("索引状态:\n") fmt.Printf(" 知识库路径: %s\n", vaultPath) fmt.Printf(" 索引文件: %s\n", dbPath) fmt.Printf(" 节点数: %d\n", nodeCount) fmt.Printf(" 边数: %d\n", edgeCount) fmt.Printf(" 索引 commit: %s\n", gitCommit) fmt.Printf(" 当前 commit: %s\n", currentCommit) fmt.Printf(" 构建时间: %s\n", builtAt) // 检查是否需要更新 needsRebuild, _, err := index.NeedsRebuild(store, vaultPath) if err != nil { return fmt.Errorf("检查索引状态失败: %w", err) } if needsRebuild { fmt.Printf(" 状态: 需要更新\n") } else { fmt.Printf(" 状态: 最新\n") } return nil } func runIndexGc(cmd *cobra.Command, args []string) error { // 展开 ~ 为实际路径 expandedVaultPath := vaultPath if strings.HasPrefix(vaultPath, "~/") { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("获取用户目录失败: %w", err) } expandedVaultPath = filepath.Join(home, vaultPath[2:]) } // 打开索引 store, err := index.Open(dbPath) if err != nil { return fmt.Errorf("打开索引失败: %w", err) } defer store.Close() fmt.Fprintln(os.Stderr, "正在清理孤立节点和边...") // 获取所有节点路径 nodes, err := store.GetAllNodes() if err != nil { return fmt.Errorf("获取节点失败: %w", err) } // 检查文件是否存在 var orphanPaths []string for _, node := range nodes { fullPath := filepath.Join(expandedVaultPath, node.Path) if _, err := os.Stat(fullPath); os.IsNotExist(err) { orphanPaths = append(orphanPaths, node.Path) } } if len(orphanPaths) == 0 { fmt.Fprintln(os.Stderr, "没有发现孤立节点") return nil } fmt.Fprintf(os.Stderr, "发现 %d 个孤立节点,正在清理...\n", len(orphanPaths)) // 删除孤立节点 deletedCount, err := store.DeleteNodesByPaths(orphanPaths) if err != nil { return fmt.Errorf("删除节点失败: %w", err) } fmt.Fprintf(os.Stderr, "已清理 %d 个孤立节点\n", deletedCount) return nil } func runGitSync(cmd *cobra.Command, args []string) error { // 展开 ~ 为实际路径 expandedVaultPath := vaultPath if strings.HasPrefix(vaultPath, "~/") { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("获取用户目录失败: %w", err) } expandedVaultPath = filepath.Join(home, vaultPath[2:]) } fmt.Fprintln(os.Stderr, "正在同步知识库到 Git 仓库...") // 检查是否是 git 仓库 gitDir := filepath.Join(expandedVaultPath, ".git") if _, err := os.Stat(gitDir); os.IsNotExist(err) { return fmt.Errorf("知识库目录不是 Git 仓库: %s", expandedVaultPath) } // 执行 git add -A addCmd := exec.Command("git", "-C", expandedVaultPath, "add", "-A") if output, err := addCmd.CombinedOutput(); err != nil { return fmt.Errorf("git add 失败: %w\n%s", err, output) } // 检查是否有变更 statusCmd := exec.Command("git", "-C", expandedVaultPath, "status", "--porcelain") statusOutput, err := statusCmd.Output() if err != nil { return fmt.Errorf("git status 失败: %w", err) } if len(statusOutput) == 0 { fmt.Fprintln(os.Stderr, "没有变更需要提交") return nil } // 生成 commit 信息 commitMsg := fmt.Sprintf("kb-cli: 自动同步 %s", time.Now().Format("2006-01-02 15:04:05")) commitCmd := exec.Command("git", "-C", expandedVaultPath, "commit", "-m", commitMsg) if output, err := commitCmd.CombinedOutput(); err != nil { return fmt.Errorf("git commit 失败: %w\n%s", err, output) } // 执行 git push pushCmd := exec.Command("git", "-C", expandedVaultPath, "push") if output, err := pushCmd.CombinedOutput(); err != nil { return fmt.Errorf("git push 失败: %w\n%s", err, output) } fmt.Fprintln(os.Stderr, "Git 同步完成") return nil }