// Package service 管理知识库文件的完整生命周期: // 注册、创建、切换、删除以及启动恢复。 package service import ( "fmt" "log" "os" "path/filepath" "time" "AI-Expert-Sidebar/internal/database" "AI-Expert-Sidebar/internal/models" ) // ListLibraries 返回所有已注册的知识库,并为每个库填充实时条目数。 // // 条目数通过 countEntries 只读方式查询各 .db 文件,不会影响活跃库的连接。 // "注册表在 settings.db,内容在各 *.db" 的分离设计使此操作天然并发安全。 func ListLibraries() ([]models.Library, error) { sdb := database.GetSettings() if sdb == nil { return nil, fmt.Errorf("设置数据库未就绪") } var libs []models.Library if err := sdb.Order("created_at asc").Find(&libs).Error; err != nil { return nil, err } // 填充每个库的当前条目数(前端展示用,不参与业务逻辑) for i, lib := range libs { libs[i].EntryCount = countEntries(lib.FilePath) } return libs, nil } // CreateLibrary 在应用数据目录下创建一个新的 .db 文件,并在 settings.db 中注册。 // // # 文件命名策略 // // 将知识库名(如"植物百科")直接作为文件名(植物百科.db), // 方便用户在 Finder 中识别。若同名文件已存在,追加 Unix 时间戳保证唯一性。 // 字符过滤(sanitizeFileName)防止名称中含有 / \ : 等非法文件系统字符。 // // # 事务性保证 // // 先创建 .db 文件,再写入 settings.db 注册记录。 // 若数据库写入失败,立即删除已创建的 .db 文件(回滚), // 避免"有文件无注册"的孤儿状态。 func CreateLibrary(name, description string) (*models.Library, error) { sdb := database.GetSettings() dir := database.DataDir fileName := sanitizeFileName(name) + ".db" filePath := filepath.Join(dir, fileName) // 若文件已存在(同名库被删除后只去注册没删文件),追加时间戳避免覆盖 if _, err := os.Stat(filePath); err == nil { filePath = filepath.Join(dir, sanitizeFileName(name)+"_"+fmt.Sprintf("%d", time.Now().Unix())+".db") } // 先建文件(含表结构),确保文件合法后才写注册表 if err := database.NewLibraryDB(filePath); err != nil { return nil, fmt.Errorf("创建知识库文件失败: %w", err) } lib := models.Library{Name: name, Description: description, FilePath: filePath} if err := sdb.Create(&lib).Error; err != nil { os.Remove(filePath) // 注册失败则回滚文件创建 return nil, err } log.Printf("[Library] 已创建: %s → %s", name, filePath) return &lib, nil } // SwitchLibrary 将名为 name 的知识库设置为当前活跃库。 // // 配合 database.OpenLibrary 完成: // 1. 打开新的 SQLite 连接; // 2. AutoMigrate(保证旧版 .db 文件的 schema 更新); // 3. 更新 settings.db 中的 "active_library" 偏好键。 func SwitchLibrary(name string) error { sdb := database.GetSettings() var lib models.Library if err := sdb.Where("name = ?", name).First(&lib).Error; err != nil { return fmt.Errorf("知识库 %q 未找到", name) } return database.OpenLibrary(lib) } // DeleteLibrary 从 settings.db 中删除知识库的注册记录。 // // deleteFile=false 时只删注册,.db 文件保留在磁盘, // 用户可以日后重新注册(或手动备份)。 // deleteFile=true 时物理删除文件,数据不可恢复。 // // 注意:调用方应在删除前检查当前活跃库(不能删除正在使用的库)。 func DeleteLibrary(name string, deleteFile bool) error { sdb := database.GetSettings() var lib models.Library if err := sdb.Where("name = ?", name).First(&lib).Error; err != nil { return fmt.Errorf("知识库 %q 未找到", name) } if err := sdb.Delete(&lib).Error; err != nil { return err } if deleteFile { return os.Remove(lib.FilePath) } return nil } // InitLibraries 在应用启动时恢复上次使用的知识库, // 若无历史记录则自动创建"默认知识库"。 // // # 自愈能力 // // 若 settings.db 中记录的知识库文件已被用户手动删除, // SwitchLibrary 会返回 error;InitLibraries 捕获此 error 后 // 转而查找下一个可用库,或创建默认库, // 确保应用启动不崩溃、不卡死("自动修复")。 func InitLibraries() error { sdb := database.GetSettings() // 尝试恢复上次的活跃库偏好 var setting models.AppSetting if sdb.Where("key = ?", "active_library").First(&setting).Error == nil { if err := SwitchLibrary(setting.Value); err == nil { return nil // 成功恢复 } // 上次的库文件可能已被删除,继续往下走 } // 没有偏好或偏好指向的文件已消失:打开注册表中的第一个 var lib models.Library if sdb.Order("created_at asc").First(&lib).Error == nil { return database.OpenLibrary(lib) } // 注册表也为空:首次启动,创建默认库 lib2, err := CreateLibrary("默认知识库", "程序自动创建的默认知识库") if err != nil { return err } return database.OpenLibrary(*lib2) } // ── 内部辅助函数 ─────────────────────────────────────────────────────────────── // countEntries 以只读方式打开指定 .db 文件,统计 entries 表的行数。 // 返回 -1 表示文件无法打开(已被删除等情况),前端可据此显示"?"。 func countEntries(filePath string) int { db, err := database.NewLibraryDBReadOnly(filePath) if err != nil { return -1 } var count int64 db.Model(&models.Entry{}).Count(&count) return int(count) } // sanitizeFileName 将知识库名称转换为合法的文件名, // 替换 Windows/macOS/Linux 都不允许的字符(/ \ : * ? " < > |)为下划线。 func sanitizeFileName(name string) string { result := make([]rune, 0, len(name)) for _, r := range name { if r == '/' || r == '\\' || r == ':' || r == '*' || r == '?' || r == '"' || r == '<' || r == '>' || r == '|' { result = append(result, '_') } else { result = append(result, r) } } if len(result) == 0 { return "library" // 防止空名称 } return string(result) }