feat: 添加注释

This commit is contained in:
Blizzard
2026-04-01 15:29:35 +08:00
parent aef2e152dc
commit 6162c9110c
28 changed files with 1293 additions and 298 deletions
+64 -22
View File
@@ -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 会返回 errorInitLibraries 捕获此 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)
}