Files
renjue 76ba500417
CI / docker (push) Successful in 2m4s
Initial commit: Luminary AI Gateway
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress,
ingress key governance, monitoring, and security controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 21:46:16 +08:00

263 lines
6.9 KiB
Go

package settings
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"sync"
"time"
"github.com/rose_cat707/luminary/internal/middleware"
"github.com/rose_cat707/luminary/internal/model"
"gorm.io/gorm"
)
// Runtime holds mutable system settings loaded from the database.
type Runtime struct {
mu sync.RWMutex
db *gorm.DB
data model.SystemSettings
}
func NewRuntime(db *gorm.DB) *Runtime {
return &Runtime{db: db}
}
func (r *Runtime) Load() error {
var s model.SystemSettings
err := r.db.First(&s, 1).Error
if err == gorm.ErrRecordNotFound {
s = defaultSettings()
s = mergeLegacySettings(s)
if err := r.db.Create(&s).Error; err != nil {
return err
}
} else if err != nil {
return err
}
r.mu.Lock()
r.data = s
r.mu.Unlock()
r.applySideEffects()
return nil
}
// AfterLoad runs post-bootstrap checks that depend on other tables.
func (r *Runtime) AfterLoad() {
r.EnsureEncryptionKeyWorks()
}
func defaultSettings() model.SystemSettings {
key := make([]byte, 32)
_, _ = rand.Read(key)
return model.SystemSettings{
ID: 1,
EncryptionKey: hex.EncodeToString(key),
SessionTTLSeconds: int64((24 * time.Hour).Seconds()),
LoginMaxAttempts: 5,
LoginLockoutSeconds: int64((15 * time.Minute).Seconds()),
TrustedProxiesJSON: "[]",
UsageFlushSeconds: 5,
HealthCheckSeconds: 30,
GatewayMaxRetries: 3,
GatewayRetryBackoffMs: 200,
ImageStoragePath: "images",
SignedURLTTLSeconds: 3600,
PredictionWaitSeconds: 60,
}
}
func (r *Runtime) Snapshot() model.SystemSettings {
r.mu.RLock()
defer r.mu.RUnlock()
return r.data
}
func (r *Runtime) EncryptionKey() string {
r.mu.RLock()
defer r.mu.RUnlock()
return r.data.EncryptionKey
}
func (r *Runtime) SessionTTL() time.Duration {
r.mu.RLock()
defer r.mu.RUnlock()
return time.Duration(r.data.SessionTTLSeconds) * time.Second
}
func (r *Runtime) LoginMaxAttempts() int {
r.mu.RLock()
defer r.mu.RUnlock()
return r.data.LoginMaxAttempts
}
func (r *Runtime) LoginLockout() time.Duration {
r.mu.RLock()
defer r.mu.RUnlock()
return time.Duration(r.data.LoginLockoutSeconds) * time.Second
}
func (r *Runtime) TrustedProxies() []string {
r.mu.RLock()
defer r.mu.RUnlock()
var out []string
_ = json.Unmarshal([]byte(r.data.TrustedProxiesJSON), &out)
return out
}
func (r *Runtime) UsageFlushInterval() time.Duration {
r.mu.RLock()
defer r.mu.RUnlock()
return time.Duration(r.data.UsageFlushSeconds) * time.Second
}
func (r *Runtime) HealthCheckInterval() time.Duration {
r.mu.RLock()
defer r.mu.RUnlock()
return time.Duration(r.data.HealthCheckSeconds) * time.Second
}
func (r *Runtime) GatewayMaxRetries() int {
r.mu.RLock()
defer r.mu.RUnlock()
return r.data.GatewayMaxRetries
}
func (r *Runtime) GatewayRetryBackoffMs() int {
r.mu.RLock()
defer r.mu.RUnlock()
return r.data.GatewayRetryBackoffMs
}
func (r *Runtime) ImageStoragePath() string {
r.mu.RLock()
defer r.mu.RUnlock()
if r.data.ImageStoragePath == "" {
return "images"
}
return r.data.ImageStoragePath
}
func (r *Runtime) PublicBaseURL() string {
r.mu.RLock()
defer r.mu.RUnlock()
return r.data.PublicBaseURL
}
func (r *Runtime) SignedURLTTL() time.Duration {
r.mu.RLock()
defer r.mu.RUnlock()
sec := r.data.SignedURLTTLSeconds
if sec <= 0 {
sec = 3600
}
return time.Duration(sec) * time.Second
}
func (r *Runtime) PredictionWaitSeconds() int64 {
r.mu.RLock()
defer r.mu.RUnlock()
if r.data.PredictionWaitSeconds <= 0 {
return 60
}
return r.data.PredictionWaitSeconds
}
type UpdateInput struct {
EncryptionKey *string `json:"encryption_key"`
SessionTTLSeconds *int64 `json:"session_ttl_seconds"`
LoginMaxAttempts *int `json:"login_max_attempts"`
LoginLockoutSeconds *int64 `json:"login_lockout_seconds"`
TrustedProxies []string `json:"trusted_proxies"`
UsageFlushSeconds *int64 `json:"usage_flush_seconds"`
HealthCheckSeconds *int64 `json:"health_check_seconds"`
GatewayMaxRetries *int `json:"gateway_max_retries"`
GatewayRetryBackoffMs *int `json:"gateway_retry_backoff_ms"`
ImageStoragePath *string `json:"image_storage_path"`
PublicBaseURL *string `json:"public_base_url"`
SignedURLTTLSeconds *int64 `json:"signed_url_ttl_seconds"`
PredictionWaitSeconds *int64 `json:"prediction_wait_seconds"`
}
func (r *Runtime) Update(in UpdateInput) error {
r.mu.Lock()
s := r.data
if in.EncryptionKey != nil && *in.EncryptionKey != "" {
s.EncryptionKey = *in.EncryptionKey
}
if in.SessionTTLSeconds != nil && *in.SessionTTLSeconds > 0 {
s.SessionTTLSeconds = *in.SessionTTLSeconds
}
if in.LoginMaxAttempts != nil && *in.LoginMaxAttempts > 0 {
s.LoginMaxAttempts = *in.LoginMaxAttempts
}
if in.LoginLockoutSeconds != nil && *in.LoginLockoutSeconds > 0 {
s.LoginLockoutSeconds = *in.LoginLockoutSeconds
}
if in.TrustedProxies != nil {
b, _ := json.Marshal(in.TrustedProxies)
s.TrustedProxiesJSON = string(b)
}
if in.UsageFlushSeconds != nil && *in.UsageFlushSeconds > 0 {
s.UsageFlushSeconds = *in.UsageFlushSeconds
}
if in.HealthCheckSeconds != nil && *in.HealthCheckSeconds > 0 {
s.HealthCheckSeconds = *in.HealthCheckSeconds
}
if in.GatewayMaxRetries != nil && *in.GatewayMaxRetries > 0 {
s.GatewayMaxRetries = *in.GatewayMaxRetries
}
if in.GatewayRetryBackoffMs != nil && *in.GatewayRetryBackoffMs >= 0 {
s.GatewayRetryBackoffMs = *in.GatewayRetryBackoffMs
}
if in.ImageStoragePath != nil {
s.ImageStoragePath = *in.ImageStoragePath
}
if in.PublicBaseURL != nil {
s.PublicBaseURL = *in.PublicBaseURL
}
if in.SignedURLTTLSeconds != nil && *in.SignedURLTTLSeconds > 0 {
s.SignedURLTTLSeconds = *in.SignedURLTTLSeconds
}
if in.PredictionWaitSeconds != nil && *in.PredictionWaitSeconds > 0 {
s.PredictionWaitSeconds = *in.PredictionWaitSeconds
}
if err := r.db.Save(&s).Error; err != nil {
return err
}
r.data = s
r.mu.Unlock()
r.applySideEffects()
return nil
}
func (r *Runtime) applySideEffects() {
middleware.ConfigureTrustedProxies(r.TrustedProxies())
}
func (r *Runtime) PublicView() map[string]any {
s := r.Snapshot()
masked := "(已设置)"
if s.EncryptionKey == "" {
masked = "(未设置)"
}
var proxies []string
_ = json.Unmarshal([]byte(s.TrustedProxiesJSON), &proxies)
return map[string]any{
"encryption_key_set": s.EncryptionKey != "",
"encryption_key_hint": masked,
"session_ttl_seconds": s.SessionTTLSeconds,
"login_max_attempts": s.LoginMaxAttempts,
"login_lockout_seconds": s.LoginLockoutSeconds,
"trusted_proxies": proxies,
"usage_flush_seconds": s.UsageFlushSeconds,
"health_check_seconds": s.HealthCheckSeconds,
"gateway_max_retries": s.GatewayMaxRetries,
"gateway_retry_backoff_ms": s.GatewayRetryBackoffMs,
"image_storage_path": s.ImageStoragePath,
"public_base_url": s.PublicBaseURL,
"signed_url_ttl_seconds": s.SignedURLTTLSeconds,
"prediction_wait_seconds": s.PredictionWaitSeconds,
}
}