5301557ad2
CI / docker (push) Has been cancelled
引入用户端对话/生图与管理端 Provider 配置,统一 invoke 编排、Gateway、请求日志与模型管理;前端按设计规范拆分布局,Go module 更名为 github.com/rose_cat707/Atlas;生图长任务 SSE 推送;管理端登录防爆破;协议与服务类型联动;Replicate 模型同步修复;用户端创作台 UI 重构;管理端创作历史替代请求日志。 Co-authored-by: Cursor <cursoragent@cursor.com>
163 lines
3.8 KiB
Go
163 lines
3.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/pressly/goose/v3"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
//go:embed migrations/*.sql
|
|
var migrationsFS embed.FS
|
|
|
|
type Store struct {
|
|
db *sql.DB
|
|
Cache *HotCache
|
|
WriteQueue *WriteQueue
|
|
}
|
|
|
|
func Open(dataDir string) (*Store, error) {
|
|
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
dbPath := filepath.Join(dataDir, "app.db")
|
|
db, err := sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db.SetMaxOpenConns(1)
|
|
s := &Store{db: db}
|
|
if err := s.migrate(); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
s.Cache = NewHotCache(30 * time.Second)
|
|
s.WriteQueue = NewWriteQueue(s, 200*time.Millisecond)
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Store) DB() *sql.DB { return s.db }
|
|
|
|
func (s *Store) Close() error {
|
|
if s.WriteQueue != nil {
|
|
s.WriteQueue.Close()
|
|
}
|
|
return s.db.Close()
|
|
}
|
|
|
|
func (s *Store) migrate() error {
|
|
goose.SetBaseFS(migrationsFS)
|
|
if err := goose.SetDialect("sqlite3"); err != nil {
|
|
return err
|
|
}
|
|
return goose.Up(s.db, "migrations")
|
|
}
|
|
|
|
func (s *Store) GetSetting(ctx context.Context, key string) (string, error) {
|
|
var value string
|
|
err := s.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, key).Scan(&value)
|
|
return value, err
|
|
}
|
|
|
|
func (s *Store) GetSettings(ctx context.Context) (map[string]string, error) {
|
|
rows, err := s.db.QueryContext(ctx, `SELECT key, value FROM settings`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make(map[string]string)
|
|
for rows.Next() {
|
|
var k, v string
|
|
if err := rows.Scan(&k, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
out[k] = v
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) SetSettings(ctx context.Context, settings map[string]string) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
stmt, err := tx.PrepareContext(ctx, `INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
for k, v := range settings {
|
|
if _, err := stmt.ExecContext(ctx, k, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *Store) ListAuditLogs(ctx context.Context, limit, offset int) ([]AuditLog, int, error) {
|
|
var total int
|
|
if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM audit_logs`).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT id, action, resource, detail, created_at FROM audit_logs ORDER BY id DESC LIMIT ? OFFSET ?`,
|
|
limit, offset,
|
|
)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
var items []AuditLog
|
|
for rows.Next() {
|
|
var item AuditLog
|
|
if err := rows.Scan(&item.ID, &item.Action, &item.Resource, &item.Detail, &item.CreatedAt); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, total, rows.Err()
|
|
}
|
|
|
|
func (s *Store) InsertAuditLog(ctx context.Context, action, resource, detail string) error {
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO audit_logs (action, resource, detail) VALUES (?, ?, ?)`,
|
|
action, resource, detail,
|
|
)
|
|
return err
|
|
}
|
|
|
|
type AuditLog struct {
|
|
ID int64 `json:"id"`
|
|
Action string `json:"action"`
|
|
Resource string `json:"resource"`
|
|
Detail string `json:"detail"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
func MaskSecret(value string) string {
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
if len(value) <= 4 {
|
|
return "****"
|
|
}
|
|
return value[:2] + "****" + value[len(value)-2:]
|
|
}
|
|
|
|
func ValidateSettingKey(key string) error {
|
|
switch key {
|
|
case "app_name", "api_port", "admin_password", "auth_enabled", "ui_locale",
|
|
"file_sign_secret", "write_flush_interval_ms", "task_progress_flush_interval_ms", "cache_ttl_seconds":
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown setting: %s", key)
|
|
}
|
|
}
|