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>
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user