feat: 添加注释
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// Package service 管理知识库文件的完整生命周期:
|
||||
// 注册、创建、切换、删除以及启动恢复。
|
||||
package service
|
||||
|
||||
import (
|
||||
@@ -11,65 +13,91 @@ import (
|
||||
"AI-Expert-Sidebar/internal/models"
|
||||
)
|
||||
|
||||
// ListLibraries returns all registered knowledge libraries with entry counts.
|
||||
// ListLibraries 返回所有已注册的知识库,并为每个库填充实时条目数。
|
||||
//
|
||||
// 条目数通过 countEntries 只读方式查询各 .db 文件,不会影响活跃库的连接。
|
||||
// "注册表在 settings.db,内容在各 *.db" 的分离设计使此操作天然并发安全。
|
||||
func ListLibraries() ([]models.Library, error) {
|
||||
sdb := database.GetSettings()
|
||||
if sdb == nil {
|
||||
return nil, fmt.Errorf("settings DB not ready")
|
||||
return nil, fmt.Errorf("设置数据库未就绪")
|
||||
}
|
||||
var libs []models.Library
|
||||
if err := sdb.Order("created_at asc").Find(&libs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Populate entry count for each library
|
||||
// 填充每个库的当前条目数(前端展示用,不参与业务逻辑)
|
||||
for i, lib := range libs {
|
||||
libs[i].EntryCount = countEntries(lib.FilePath)
|
||||
}
|
||||
return libs, nil
|
||||
}
|
||||
|
||||
// CreateLibrary registers a new knowledge library and creates its SQLite file.
|
||||
// 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)
|
||||
|
||||
// Ensure uniqueness of file path
|
||||
// 若文件已存在(同名库被删除后只去注册没删文件),追加时间戳避免覆盖
|
||||
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("create library DB: %w", err)
|
||||
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) // rollback file
|
||||
os.Remove(filePath) // 注册失败则回滚文件创建
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[Library] Created: %s → %s", name, filePath)
|
||||
log.Printf("[Library] 已创建: %s → %s", name, filePath)
|
||||
return &lib, nil
|
||||
}
|
||||
|
||||
// SwitchLibrary makes the named library active.
|
||||
// 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("library %q not found", name)
|
||||
return fmt.Errorf("知识库 %q 未找到", name)
|
||||
}
|
||||
return database.OpenLibrary(lib)
|
||||
}
|
||||
|
||||
// DeleteLibrary removes a library from the registry (and optionally its file).
|
||||
// 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("library %q not found", name)
|
||||
return fmt.Errorf("知识库 %q 未找到", name)
|
||||
}
|
||||
if err := sdb.Delete(&lib).Error; err != nil {
|
||||
return err
|
||||
@@ -80,31 +108,42 @@ func DeleteLibrary(name string, deleteFile bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitLibraries restores the last active library or creates the default one.
|
||||
// InitLibraries 在应用启动时恢复上次使用的知识库,
|
||||
// 若无历史记录则自动创建"默认知识库"。
|
||||
//
|
||||
// # 自愈能力
|
||||
//
|
||||
// 若 settings.db 中记录的知识库文件已被用户手动删除,
|
||||
// SwitchLibrary 会返回 error;InitLibraries 捕获此 error 后
|
||||
// 转而查找下一个可用库,或创建默认库,
|
||||
// 确保应用启动不崩溃、不卡死("自动修复")。
|
||||
func InitLibraries() error {
|
||||
sdb := database.GetSettings()
|
||||
// Check active_library preference
|
||||
// 尝试恢复上次的活跃库偏好
|
||||
var setting models.AppSetting
|
||||
if sdb.Where("key = ?", "active_library").First(&setting).Error == nil {
|
||||
if err := SwitchLibrary(setting.Value); err == nil {
|
||||
return nil // restored successfully
|
||||
return nil // 成功恢复
|
||||
}
|
||||
// 上次的库文件可能已被删除,继续往下走
|
||||
}
|
||||
// No preference or stale — find first library
|
||||
// 没有偏好或偏好指向的文件已消失:打开注册表中的第一个
|
||||
var lib models.Library
|
||||
if sdb.Order("created_at asc").First(&lib).Error == nil {
|
||||
return database.OpenLibrary(lib)
|
||||
}
|
||||
// No libraries at all — create default
|
||||
lib2, err := CreateLibrary("默认知识库", "自动创建的默认知识库")
|
||||
// 注册表也为空:首次启动,创建默认库
|
||||
lib2, err := CreateLibrary("默认知识库", "程序自动创建的默认知识库")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return database.OpenLibrary(*lib2)
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
// ── 内部辅助函数 ───────────────────────────────────────────────────────────────
|
||||
|
||||
// countEntries 以只读方式打开指定 .db 文件,统计 entries 表的行数。
|
||||
// 返回 -1 表示文件无法打开(已被删除等情况),前端可据此显示"?"。
|
||||
func countEntries(filePath string) int {
|
||||
db, err := database.NewLibraryDBReadOnly(filePath)
|
||||
if err != nil {
|
||||
@@ -115,17 +154,20 @@ func countEntries(filePath string) int {
|
||||
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 == '|' {
|
||||
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 "library" // 防止空名称
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user