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