76ba500417
CI / docker (push) Successful in 2m4s
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>
263 lines
6.9 KiB
Go
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,
|
|
}
|
|
}
|