Files
AI-Expert-Sidebar/internal/service/library_svc.go
T
2026-04-01 15:29:35 +08:00

174 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 会返回 errorInitLibraries 捕获此 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)
}