Files
Cyrene/backend/gateway/internal/store/file_store.go
T
AskaEth 87214b9441 feat: Phase 1+2 架构进化 — 连续思考链/主动消息决策/情感状态机/离线自主思考 (86文件)
Phase 1 (基础设施):
- ThinkChain 思考链连续性 + 差异化思考提示词 (persistent)
- AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional)
- MessageScheduler 自适应消息节奏 (Idle/Available/Busy)
- SessionEnrichmentStore 渐进式上下文丰富 (5层)
- ConversationBus 事件总线 + ResponseCache (dedup)
- pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf
- NPE 守卫/链路优化/数据库表修复/Go workspace

Phase 2 (人格交互):
- EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减)
- ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验)
- Gateway↔ai-core 在线状态感知链路 (presence notification)
- 离线思考频率控制 + 重连问候 + 离线消息排队

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:25:12 +08:00

173 lines
4.8 KiB
Go

package store
import (
"database/sql"
"fmt"
"github.com/yourname/cyrene-ai/pkg/logger"
"time"
)
// File 文件元数据模型
type File struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Filename string `json:"filename"`
StoredPath string `json:"stored_path"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
Hash string `json:"hash"`
IsPublic bool `json:"is_public"`
CreatedAt time.Time `json:"created_at"`
}
// FileStore 文件元数据持久化存储
type FileStore struct {
db *sql.DB
}
// NewFileStore 使用已有数据库连接初始化文件存储并自动建表
func NewFileStore(db *sql.DB) (*FileStore, error) {
store := &FileStore{db: db}
if err := store.migrate(); err != nil {
return nil, fmt.Errorf("文件表迁移失败: %w", err)
}
logger.Println("[FileStore] 文件持久化存储已初始化")
return store, nil
}
// migrate 自动创建文件表结构
func (s *FileStore) migrate() error {
queries := []string{
`CREATE TABLE IF NOT EXISTS files (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
filename VARCHAR(500) NOT NULL,
stored_path VARCHAR(1000) NOT NULL,
mime_type VARCHAR(255) NOT NULL DEFAULT 'application/octet-stream',
size BIGINT NOT NULL DEFAULT 0,
hash VARCHAR(64) NOT NULL DEFAULT '',
is_public BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
`CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_files_hash ON files(hash)`,
`CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(user_id, created_at DESC)`,
}
for _, q := range queries {
if _, err := s.db.Exec(q); err != nil {
return fmt.Errorf("迁移SQL执行失败: %w\nSQL: %s", err, q)
}
}
return nil
}
// CreateFile 创建文件元数据记录
func (s *FileStore) CreateFile(f *File) error {
_, err := s.db.Exec(
`INSERT INTO files (id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
f.ID, f.UserID, f.Filename, f.StoredPath, f.MimeType, f.Size, f.Hash, f.IsPublic, f.CreatedAt,
)
if err != nil {
return fmt.Errorf("创建文件记录失败: %w", err)
}
return nil
}
// GetFile 根据ID获取文件元数据
func (s *FileStore) GetFile(id string) (*File, error) {
var f File
err := s.db.QueryRow(
`SELECT id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at
FROM files WHERE id = $1`,
id,
).Scan(&f.ID, &f.UserID, &f.Filename, &f.StoredPath, &f.MimeType, &f.Size, &f.Hash, &f.IsPublic, &f.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("查询文件失败: %w", err)
}
return &f, nil
}
// GetUserFiles 获取用户的所有文件,支持分页
func (s *FileStore) GetUserFiles(userID string, page, limit int) ([]File, int, error) {
if page <= 0 {
page = 1
}
if limit <= 0 || limit > 100 {
limit = 20
}
offset := (page - 1) * limit
// 获取总数
var total int
if err := s.db.QueryRow(
`SELECT COUNT(*) FROM files WHERE user_id = $1`,
userID,
).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("查询文件总数失败: %w", err)
}
// 分页查询
rows, err := s.db.Query(
`SELECT id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at
FROM files WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`,
userID, limit, offset,
)
if err != nil {
return nil, 0, fmt.Errorf("查询用户文件失败: %w", err)
}
defer rows.Close()
var files []File
for rows.Next() {
var f File
if err := rows.Scan(&f.ID, &f.UserID, &f.Filename, &f.StoredPath, &f.MimeType, &f.Size, &f.Hash, &f.IsPublic, &f.CreatedAt); err != nil {
return nil, 0, fmt.Errorf("扫描文件行失败: %w", err)
}
files = append(files, f)
}
if files == nil {
files = []File{}
}
return files, total, rows.Err()
}
// GetFileByHash 根据SHA256哈希查找文件(用于去重)
func (s *FileStore) GetFileByHash(hash string) (*File, error) {
if hash == "" {
return nil, nil
}
var f File
err := s.db.QueryRow(
`SELECT id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at
FROM files WHERE hash = $1
ORDER BY created_at ASC LIMIT 1`,
hash,
).Scan(&f.ID, &f.UserID, &f.Filename, &f.StoredPath, &f.MimeType, &f.Size, &f.Hash, &f.IsPublic, &f.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("按哈希查询文件失败: %w", err)
}
return &f, nil
}
// DeleteFile 删除文件元数据记录
func (s *FileStore) DeleteFile(id string) error {
_, err := s.db.Exec(`DELETE FROM files WHERE id = $1`, id)
if err != nil {
return fmt.Errorf("删除文件记录失败: %w", err)
}
return nil
}