2025-12-18 13:50:39 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
2025-12-20 15:11:43 +08:00
|
|
|
|
"crypto/rand"
|
|
|
|
|
|
"encoding/hex"
|
2026-01-11 21:54:52 -08:00
|
|
|
|
"encoding/json"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2025-12-25 17:15:01 +08:00
|
|
|
|
"strconv"
|
2026-01-09 20:57:32 +08:00
|
|
|
|
"strings"
|
2025-12-25 17:15:01 +08:00
|
|
|
|
|
2025-12-24 21:07:21 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
2025-12-31 23:42:01 +08:00
|
|
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
2025-12-25 20:52:47 +08:00
|
|
|
|
ErrRegistrationDisabled = infraerrors.Forbidden("REGISTRATION_DISABLED", "registration is currently disabled")
|
|
|
|
|
|
ErrSettingNotFound = infraerrors.NotFound("SETTING_NOT_FOUND", "setting not found")
|
2025-12-18 13:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-25 17:15:01 +08:00
|
|
|
|
type SettingRepository interface {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
Get(ctx context.Context, key string) (*Setting, error)
|
2025-12-25 17:15:01 +08:00
|
|
|
|
GetValue(ctx context.Context, key string) (string, error)
|
|
|
|
|
|
Set(ctx context.Context, key, value string) error
|
|
|
|
|
|
GetMultiple(ctx context.Context, keys []string) (map[string]string, error)
|
|
|
|
|
|
SetMultiple(ctx context.Context, settings map[string]string) error
|
|
|
|
|
|
GetAll(ctx context.Context) (map[string]string, error)
|
|
|
|
|
|
Delete(ctx context.Context, key string) error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// SettingService 系统设置服务
|
|
|
|
|
|
type SettingService struct {
|
2025-12-25 17:15:01 +08:00
|
|
|
|
settingRepo SettingRepository
|
2025-12-18 13:50:39 +08:00
|
|
|
|
cfg *config.Config
|
2026-01-10 18:37:44 +08:00
|
|
|
|
onUpdate func() // Callback when settings are updated (for cache invalidation)
|
|
|
|
|
|
version string // Application version
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewSettingService 创建系统设置服务实例
|
2025-12-25 17:15:01 +08:00
|
|
|
|
func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return &SettingService{
|
|
|
|
|
|
settingRepo: settingRepo,
|
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetAllSettings 获取所有系统设置
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *SettingService) GetAllSettings(ctx context.Context) (*SystemSettings, error) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
settings, err := s.settingRepo.GetAll(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("get all settings: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s.parseSettings(settings), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetPublicSettings 获取公开设置(无需登录)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings, error) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
keys := []string{
|
2025-12-26 15:40:24 +08:00
|
|
|
|
SettingKeyRegistrationEnabled,
|
|
|
|
|
|
SettingKeyEmailVerifyEnabled,
|
|
|
|
|
|
SettingKeyTurnstileEnabled,
|
|
|
|
|
|
SettingKeyTurnstileSiteKey,
|
|
|
|
|
|
SettingKeySiteName,
|
|
|
|
|
|
SettingKeySiteLogo,
|
|
|
|
|
|
SettingKeySiteSubtitle,
|
2026-01-04 19:27:53 +08:00
|
|
|
|
SettingKeyAPIBaseURL,
|
2025-12-26 15:40:24 +08:00
|
|
|
|
SettingKeyContactInfo,
|
2026-01-04 19:27:53 +08:00
|
|
|
|
SettingKeyDocURL,
|
2026-01-10 18:37:44 +08:00
|
|
|
|
SettingKeyHomeContent,
|
2026-01-09 13:52:27 +08:00
|
|
|
|
SettingKeyLinuxDoConnectEnabled,
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("get public settings: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 13:52:27 +08:00
|
|
|
|
linuxDoEnabled := false
|
|
|
|
|
|
if raw, ok := settings[SettingKeyLinuxDoConnectEnabled]; ok {
|
|
|
|
|
|
linuxDoEnabled = raw == "true"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
linuxDoEnabled = s.cfg != nil && s.cfg.LinuxDo.Enabled
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
return &PublicSettings{
|
|
|
|
|
|
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
|
|
|
|
|
EmailVerifyEnabled: settings[SettingKeyEmailVerifyEnabled] == "true",
|
|
|
|
|
|
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
|
|
|
|
|
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
|
|
|
|
|
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
|
|
|
|
|
SiteLogo: settings[SettingKeySiteLogo],
|
|
|
|
|
|
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
2026-01-04 19:27:53 +08:00
|
|
|
|
APIBaseURL: settings[SettingKeyAPIBaseURL],
|
2025-12-26 15:40:24 +08:00
|
|
|
|
ContactInfo: settings[SettingKeyContactInfo],
|
2026-01-04 19:27:53 +08:00
|
|
|
|
DocURL: settings[SettingKeyDocURL],
|
2026-01-10 18:37:44 +08:00
|
|
|
|
HomeContent: settings[SettingKeyHomeContent],
|
2026-01-09 13:52:27 +08:00
|
|
|
|
LinuxDoOAuthEnabled: linuxDoEnabled,
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 18:37:44 +08:00
|
|
|
|
// SetOnUpdateCallback sets a callback function to be called when settings are updated
|
|
|
|
|
|
// This is used for cache invalidation (e.g., HTML cache in frontend server)
|
|
|
|
|
|
func (s *SettingService) SetOnUpdateCallback(callback func()) {
|
|
|
|
|
|
s.onUpdate = callback
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SetVersion sets the application version for injection into public settings
|
|
|
|
|
|
func (s *SettingService) SetVersion(version string) {
|
|
|
|
|
|
s.version = version
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetPublicSettingsForInjection returns public settings in a format suitable for HTML injection
|
|
|
|
|
|
// This implements the web.PublicSettingsProvider interface
|
2026-01-10 19:08:41 +08:00
|
|
|
|
func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any, error) {
|
2026-01-10 18:37:44 +08:00
|
|
|
|
settings, err := s.GetPublicSettings(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Return a struct that matches the frontend's expected format
|
|
|
|
|
|
return &struct {
|
|
|
|
|
|
RegistrationEnabled bool `json:"registration_enabled"`
|
|
|
|
|
|
EmailVerifyEnabled bool `json:"email_verify_enabled"`
|
|
|
|
|
|
TurnstileEnabled bool `json:"turnstile_enabled"`
|
|
|
|
|
|
TurnstileSiteKey string `json:"turnstile_site_key,omitempty"`
|
|
|
|
|
|
SiteName string `json:"site_name"`
|
|
|
|
|
|
SiteLogo string `json:"site_logo,omitempty"`
|
|
|
|
|
|
SiteSubtitle string `json:"site_subtitle,omitempty"`
|
|
|
|
|
|
APIBaseURL string `json:"api_base_url,omitempty"`
|
|
|
|
|
|
ContactInfo string `json:"contact_info,omitempty"`
|
|
|
|
|
|
DocURL string `json:"doc_url,omitempty"`
|
|
|
|
|
|
HomeContent string `json:"home_content,omitempty"`
|
|
|
|
|
|
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
|
|
|
|
|
Version string `json:"version,omitempty"`
|
|
|
|
|
|
}{
|
|
|
|
|
|
RegistrationEnabled: settings.RegistrationEnabled,
|
|
|
|
|
|
EmailVerifyEnabled: settings.EmailVerifyEnabled,
|
|
|
|
|
|
TurnstileEnabled: settings.TurnstileEnabled,
|
|
|
|
|
|
TurnstileSiteKey: settings.TurnstileSiteKey,
|
|
|
|
|
|
SiteName: settings.SiteName,
|
|
|
|
|
|
SiteLogo: settings.SiteLogo,
|
|
|
|
|
|
SiteSubtitle: settings.SiteSubtitle,
|
|
|
|
|
|
APIBaseURL: settings.APIBaseURL,
|
|
|
|
|
|
ContactInfo: settings.ContactInfo,
|
|
|
|
|
|
DocURL: settings.DocURL,
|
|
|
|
|
|
HomeContent: settings.HomeContent,
|
|
|
|
|
|
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
|
|
|
|
|
Version: s.version,
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateSettings 更新系统设置
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSettings) error {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
updates := make(map[string]string)
|
|
|
|
|
|
|
|
|
|
|
|
// 注册设置
|
2025-12-26 15:40:24 +08:00
|
|
|
|
updates[SettingKeyRegistrationEnabled] = strconv.FormatBool(settings.RegistrationEnabled)
|
|
|
|
|
|
updates[SettingKeyEmailVerifyEnabled] = strconv.FormatBool(settings.EmailVerifyEnabled)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 邮件服务设置(只有非空才更新密码)
|
2026-01-04 19:27:53 +08:00
|
|
|
|
updates[SettingKeySMTPHost] = settings.SMTPHost
|
|
|
|
|
|
updates[SettingKeySMTPPort] = strconv.Itoa(settings.SMTPPort)
|
|
|
|
|
|
updates[SettingKeySMTPUsername] = settings.SMTPUsername
|
2026-01-04 17:02:38 +08:00
|
|
|
|
if settings.SMTPPassword != "" {
|
2026-01-04 19:27:53 +08:00
|
|
|
|
updates[SettingKeySMTPPassword] = settings.SMTPPassword
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
2026-01-04 19:27:53 +08:00
|
|
|
|
updates[SettingKeySMTPFrom] = settings.SMTPFrom
|
|
|
|
|
|
updates[SettingKeySMTPFromName] = settings.SMTPFromName
|
|
|
|
|
|
updates[SettingKeySMTPUseTLS] = strconv.FormatBool(settings.SMTPUseTLS)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// Cloudflare Turnstile 设置(只有非空才更新密钥)
|
2025-12-26 15:40:24 +08:00
|
|
|
|
updates[SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled)
|
|
|
|
|
|
updates[SettingKeyTurnstileSiteKey] = settings.TurnstileSiteKey
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if settings.TurnstileSecretKey != "" {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
updates[SettingKeyTurnstileSecretKey] = settings.TurnstileSecretKey
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 09:14:32 +08:00
|
|
|
|
// LinuxDo Connect OAuth 登录
|
2026-01-09 13:52:27 +08:00
|
|
|
|
updates[SettingKeyLinuxDoConnectEnabled] = strconv.FormatBool(settings.LinuxDoConnectEnabled)
|
|
|
|
|
|
updates[SettingKeyLinuxDoConnectClientID] = settings.LinuxDoConnectClientID
|
|
|
|
|
|
updates[SettingKeyLinuxDoConnectRedirectURL] = settings.LinuxDoConnectRedirectURL
|
|
|
|
|
|
if settings.LinuxDoConnectClientSecret != "" {
|
|
|
|
|
|
updates[SettingKeyLinuxDoConnectClientSecret] = settings.LinuxDoConnectClientSecret
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// OEM设置
|
2025-12-26 15:40:24 +08:00
|
|
|
|
updates[SettingKeySiteName] = settings.SiteName
|
|
|
|
|
|
updates[SettingKeySiteLogo] = settings.SiteLogo
|
|
|
|
|
|
updates[SettingKeySiteSubtitle] = settings.SiteSubtitle
|
2026-01-04 19:27:53 +08:00
|
|
|
|
updates[SettingKeyAPIBaseURL] = settings.APIBaseURL
|
2025-12-26 15:40:24 +08:00
|
|
|
|
updates[SettingKeyContactInfo] = settings.ContactInfo
|
2026-01-04 19:27:53 +08:00
|
|
|
|
updates[SettingKeyDocURL] = settings.DocURL
|
2026-01-10 18:37:44 +08:00
|
|
|
|
updates[SettingKeyHomeContent] = settings.HomeContent
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 默认配置
|
2025-12-26 15:40:24 +08:00
|
|
|
|
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
|
|
|
|
|
|
updates[SettingKeyDefaultBalance] = strconv.FormatFloat(settings.DefaultBalance, 'f', 8, 64)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2026-01-03 06:37:08 -08:00
|
|
|
|
// Model fallback configuration
|
|
|
|
|
|
updates[SettingKeyEnableModelFallback] = strconv.FormatBool(settings.EnableModelFallback)
|
|
|
|
|
|
updates[SettingKeyFallbackModelAnthropic] = settings.FallbackModelAnthropic
|
|
|
|
|
|
updates[SettingKeyFallbackModelOpenAI] = settings.FallbackModelOpenAI
|
|
|
|
|
|
updates[SettingKeyFallbackModelGemini] = settings.FallbackModelGemini
|
|
|
|
|
|
updates[SettingKeyFallbackModelAntigravity] = settings.FallbackModelAntigravity
|
|
|
|
|
|
|
2026-01-04 22:49:40 +08:00
|
|
|
|
// Identity patch configuration (Claude -> Gemini)
|
|
|
|
|
|
updates[SettingKeyEnableIdentityPatch] = strconv.FormatBool(settings.EnableIdentityPatch)
|
|
|
|
|
|
updates[SettingKeyIdentityPatchPrompt] = settings.IdentityPatchPrompt
|
|
|
|
|
|
|
2026-01-09 20:57:32 +08:00
|
|
|
|
// Ops monitoring (vNext)
|
|
|
|
|
|
updates[SettingKeyOpsMonitoringEnabled] = strconv.FormatBool(settings.OpsMonitoringEnabled)
|
|
|
|
|
|
updates[SettingKeyOpsRealtimeMonitoringEnabled] = strconv.FormatBool(settings.OpsRealtimeMonitoringEnabled)
|
|
|
|
|
|
updates[SettingKeyOpsQueryModeDefault] = string(ParseOpsQueryMode(settings.OpsQueryModeDefault))
|
2026-01-10 01:38:47 +08:00
|
|
|
|
if settings.OpsMetricsIntervalSeconds > 0 {
|
|
|
|
|
|
updates[SettingKeyOpsMetricsIntervalSeconds] = strconv.Itoa(settings.OpsMetricsIntervalSeconds)
|
|
|
|
|
|
}
|
2026-01-09 20:57:32 +08:00
|
|
|
|
|
2026-01-10 18:37:44 +08:00
|
|
|
|
err := s.settingRepo.SetMultiple(ctx, updates)
|
|
|
|
|
|
if err == nil && s.onUpdate != nil {
|
|
|
|
|
|
s.onUpdate() // Invalidate cache after settings update
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsRegistrationEnabled 检查是否开放注册
|
|
|
|
|
|
func (s *SettingService) IsRegistrationEnabled(ctx context.Context) bool {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEnabled)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
2026-01-12 09:14:32 +08:00
|
|
|
|
// 安全默认:如果设置不存在或查询出错,默认关闭注册
|
|
|
|
|
|
return false
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
return value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsEmailVerifyEnabled 检查是否开启邮件验证
|
|
|
|
|
|
func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyEmailVerifyEnabled)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetSiteName 获取网站名称
|
|
|
|
|
|
func (s *SettingService) GetSiteName(ctx context.Context) string {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeySiteName)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil || value == "" {
|
|
|
|
|
|
return "Sub2API"
|
|
|
|
|
|
}
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetDefaultConcurrency 获取默认并发量
|
|
|
|
|
|
func (s *SettingService) GetDefaultConcurrency(ctx context.Context) int {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultConcurrency)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return s.cfg.Default.UserConcurrency
|
|
|
|
|
|
}
|
|
|
|
|
|
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
|
|
|
|
|
return v
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.cfg.Default.UserConcurrency
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetDefaultBalance 获取默认余额
|
|
|
|
|
|
func (s *SettingService) GetDefaultBalance(ctx context.Context) float64 {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultBalance)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return s.cfg.Default.UserBalance
|
|
|
|
|
|
}
|
|
|
|
|
|
if v, err := strconv.ParseFloat(value, 64); err == nil && v >= 0 {
|
|
|
|
|
|
return v
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.cfg.Default.UserBalance
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// InitializeDefaultSettings 初始化默认设置
|
|
|
|
|
|
func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|
|
|
|
|
// 检查是否已有设置
|
2025-12-26 15:40:24 +08:00
|
|
|
|
_, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEnabled)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err == nil {
|
|
|
|
|
|
// 已有设置,不需要初始化
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-12-25 20:52:47 +08:00
|
|
|
|
if !errors.Is(err, ErrSettingNotFound) {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return fmt.Errorf("check existing settings: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化默认设置
|
|
|
|
|
|
defaults := map[string]string{
|
2025-12-26 15:40:24 +08:00
|
|
|
|
SettingKeyRegistrationEnabled: "true",
|
|
|
|
|
|
SettingKeyEmailVerifyEnabled: "false",
|
|
|
|
|
|
SettingKeySiteName: "Sub2API",
|
|
|
|
|
|
SettingKeySiteLogo: "",
|
|
|
|
|
|
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
|
|
|
|
|
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
2026-01-04 19:27:53 +08:00
|
|
|
|
SettingKeySMTPPort: "587",
|
|
|
|
|
|
SettingKeySMTPUseTLS: "false",
|
2026-01-03 06:37:08 -08:00
|
|
|
|
// Model fallback defaults
|
|
|
|
|
|
SettingKeyEnableModelFallback: "false",
|
|
|
|
|
|
SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
|
|
|
|
|
|
SettingKeyFallbackModelOpenAI: "gpt-4o",
|
|
|
|
|
|
SettingKeyFallbackModelGemini: "gemini-2.5-pro",
|
|
|
|
|
|
SettingKeyFallbackModelAntigravity: "gemini-2.5-pro",
|
2026-01-04 22:49:40 +08:00
|
|
|
|
// Identity patch defaults
|
|
|
|
|
|
SettingKeyEnableIdentityPatch: "true",
|
|
|
|
|
|
SettingKeyIdentityPatchPrompt: "",
|
2026-01-09 20:57:32 +08:00
|
|
|
|
|
|
|
|
|
|
// Ops monitoring defaults (vNext)
|
|
|
|
|
|
SettingKeyOpsMonitoringEnabled: "true",
|
|
|
|
|
|
SettingKeyOpsRealtimeMonitoringEnabled: "true",
|
|
|
|
|
|
SettingKeyOpsQueryModeDefault: "auto",
|
2026-01-10 01:38:47 +08:00
|
|
|
|
SettingKeyOpsMetricsIntervalSeconds: "60",
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s.settingRepo.SetMultiple(ctx, defaults)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// parseSettings 解析设置到结构体
|
2025-12-26 15:40:24 +08:00
|
|
|
|
func (s *SettingService) parseSettings(settings map[string]string) *SystemSettings {
|
|
|
|
|
|
result := &SystemSettings{
|
2026-01-04 21:06:12 +08:00
|
|
|
|
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
|
|
|
|
|
EmailVerifyEnabled: settings[SettingKeyEmailVerifyEnabled] == "true",
|
|
|
|
|
|
SMTPHost: settings[SettingKeySMTPHost],
|
|
|
|
|
|
SMTPUsername: settings[SettingKeySMTPUsername],
|
|
|
|
|
|
SMTPFrom: settings[SettingKeySMTPFrom],
|
|
|
|
|
|
SMTPFromName: settings[SettingKeySMTPFromName],
|
|
|
|
|
|
SMTPUseTLS: settings[SettingKeySMTPUseTLS] == "true",
|
|
|
|
|
|
SMTPPasswordConfigured: settings[SettingKeySMTPPassword] != "",
|
|
|
|
|
|
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
|
|
|
|
|
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
2026-01-02 17:40:57 +08:00
|
|
|
|
TurnstileSecretKeyConfigured: settings[SettingKeyTurnstileSecretKey] != "",
|
2026-01-04 21:06:12 +08:00
|
|
|
|
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
|
|
|
|
|
SiteLogo: settings[SettingKeySiteLogo],
|
|
|
|
|
|
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
|
|
|
|
|
APIBaseURL: settings[SettingKeyAPIBaseURL],
|
|
|
|
|
|
ContactInfo: settings[SettingKeyContactInfo],
|
|
|
|
|
|
DocURL: settings[SettingKeyDocURL],
|
2026-01-10 18:37:44 +08:00
|
|
|
|
HomeContent: settings[SettingKeyHomeContent],
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析整数类型
|
2026-01-04 19:27:53 +08:00
|
|
|
|
if port, err := strconv.Atoi(settings[SettingKeySMTPPort]); err == nil {
|
2026-01-04 17:02:38 +08:00
|
|
|
|
result.SMTPPort = port
|
2025-12-18 13:50:39 +08:00
|
|
|
|
} else {
|
2026-01-04 17:02:38 +08:00
|
|
|
|
result.SMTPPort = 587
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if concurrency, err := strconv.Atoi(settings[SettingKeyDefaultConcurrency]); err == nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
result.DefaultConcurrency = concurrency
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result.DefaultConcurrency = s.cfg.Default.UserConcurrency
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析浮点数类型
|
2025-12-26 15:40:24 +08:00
|
|
|
|
if balance, err := strconv.ParseFloat(settings[SettingKeyDefaultBalance], 64); err == nil {
|
2025-12-18 13:50:39 +08:00
|
|
|
|
result.DefaultBalance = balance
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result.DefaultBalance = s.cfg.Default.UserBalance
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:37:08 -08:00
|
|
|
|
// 敏感信息直接返回,方便测试连接时使用
|
2026-01-04 19:27:53 +08:00
|
|
|
|
result.SMTPPassword = settings[SettingKeySMTPPassword]
|
2025-12-26 15:40:24 +08:00
|
|
|
|
result.TurnstileSecretKey = settings[SettingKeyTurnstileSecretKey]
|
2025-12-18 13:50:39 +08:00
|
|
|
|
|
2026-01-09 19:32:06 +08:00
|
|
|
|
// LinuxDo Connect 设置:
|
|
|
|
|
|
// - 兼容 config.yaml/env(避免老部署因为未迁移到数据库设置而被意外关闭)
|
|
|
|
|
|
// - 支持在后台“系统设置”中覆盖并持久化(存储于 DB)
|
2026-01-09 13:52:27 +08:00
|
|
|
|
linuxDoBase := config.LinuxDoConnectConfig{}
|
|
|
|
|
|
if s.cfg != nil {
|
|
|
|
|
|
linuxDoBase = s.cfg.LinuxDo
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if raw, ok := settings[SettingKeyLinuxDoConnectEnabled]; ok {
|
|
|
|
|
|
result.LinuxDoConnectEnabled = raw == "true"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result.LinuxDoConnectEnabled = linuxDoBase.Enabled
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if v, ok := settings[SettingKeyLinuxDoConnectClientID]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
|
result.LinuxDoConnectClientID = strings.TrimSpace(v)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result.LinuxDoConnectClientID = linuxDoBase.ClientID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if v, ok := settings[SettingKeyLinuxDoConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
|
result.LinuxDoConnectRedirectURL = strings.TrimSpace(v)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result.LinuxDoConnectRedirectURL = linuxDoBase.RedirectURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.LinuxDoConnectClientSecret = strings.TrimSpace(settings[SettingKeyLinuxDoConnectClientSecret])
|
|
|
|
|
|
if result.LinuxDoConnectClientSecret == "" {
|
|
|
|
|
|
result.LinuxDoConnectClientSecret = strings.TrimSpace(linuxDoBase.ClientSecret)
|
|
|
|
|
|
}
|
|
|
|
|
|
result.LinuxDoConnectClientSecretConfigured = result.LinuxDoConnectClientSecret != ""
|
|
|
|
|
|
|
2026-01-03 06:37:08 -08:00
|
|
|
|
// Model fallback settings
|
|
|
|
|
|
result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
|
|
|
|
|
|
result.FallbackModelAnthropic = s.getStringOrDefault(settings, SettingKeyFallbackModelAnthropic, "claude-3-5-sonnet-20241022")
|
|
|
|
|
|
result.FallbackModelOpenAI = s.getStringOrDefault(settings, SettingKeyFallbackModelOpenAI, "gpt-4o")
|
|
|
|
|
|
result.FallbackModelGemini = s.getStringOrDefault(settings, SettingKeyFallbackModelGemini, "gemini-2.5-pro")
|
|
|
|
|
|
result.FallbackModelAntigravity = s.getStringOrDefault(settings, SettingKeyFallbackModelAntigravity, "gemini-2.5-pro")
|
|
|
|
|
|
|
2026-01-04 22:49:40 +08:00
|
|
|
|
// Identity patch settings (default: enabled, to preserve existing behavior)
|
|
|
|
|
|
if v, ok := settings[SettingKeyEnableIdentityPatch]; ok && v != "" {
|
|
|
|
|
|
result.EnableIdentityPatch = v == "true"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result.EnableIdentityPatch = true
|
|
|
|
|
|
}
|
|
|
|
|
|
result.IdentityPatchPrompt = settings[SettingKeyIdentityPatchPrompt]
|
|
|
|
|
|
|
2026-01-09 20:57:32 +08:00
|
|
|
|
// Ops monitoring settings (default: enabled, fail-open)
|
|
|
|
|
|
result.OpsMonitoringEnabled = !isFalseSettingValue(settings[SettingKeyOpsMonitoringEnabled])
|
|
|
|
|
|
result.OpsRealtimeMonitoringEnabled = !isFalseSettingValue(settings[SettingKeyOpsRealtimeMonitoringEnabled])
|
|
|
|
|
|
result.OpsQueryModeDefault = string(ParseOpsQueryMode(settings[SettingKeyOpsQueryModeDefault]))
|
2026-01-10 01:38:47 +08:00
|
|
|
|
result.OpsMetricsIntervalSeconds = 60
|
|
|
|
|
|
if raw := strings.TrimSpace(settings[SettingKeyOpsMetricsIntervalSeconds]); raw != "" {
|
|
|
|
|
|
if v, err := strconv.Atoi(raw); err == nil {
|
|
|
|
|
|
if v < 60 {
|
|
|
|
|
|
v = 60
|
|
|
|
|
|
}
|
|
|
|
|
|
if v > 3600 {
|
|
|
|
|
|
v = 3600
|
|
|
|
|
|
}
|
|
|
|
|
|
result.OpsMetricsIntervalSeconds = v
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-09 20:57:32 +08:00
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 20:57:32 +08:00
|
|
|
|
func isFalseSettingValue(value string) bool {
|
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
|
|
|
|
case "false", "0", "off", "disabled":
|
|
|
|
|
|
return true
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// getStringOrDefault 获取字符串值或默认值
|
|
|
|
|
|
func (s *SettingService) getStringOrDefault(settings map[string]string, key, defaultValue string) string {
|
|
|
|
|
|
if value, ok := settings[key]; ok && value != "" {
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
|
|
|
|
|
return defaultValue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsTurnstileEnabled 检查是否启用 Turnstile 验证
|
|
|
|
|
|
func (s *SettingService) IsTurnstileEnabled(ctx context.Context) bool {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyTurnstileEnabled)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetTurnstileSecretKey 获取 Turnstile Secret Key
|
|
|
|
|
|
func (s *SettingService) GetTurnstileSecretKey(ctx context.Context) string {
|
2025-12-26 15:40:24 +08:00
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyTurnstileSecretKey)
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
2025-12-20 15:11:43 +08:00
|
|
|
|
|
2026-01-04 22:49:40 +08:00
|
|
|
|
// IsIdentityPatchEnabled 检查是否启用身份补丁(Claude -> Gemini systemInstruction 注入)
|
|
|
|
|
|
func (s *SettingService) IsIdentityPatchEnabled(ctx context.Context) bool {
|
|
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyEnableIdentityPatch)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 默认开启,保持兼容
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetIdentityPatchPrompt 获取自定义身份补丁提示词(为空表示使用内置默认模板)
|
|
|
|
|
|
func (s *SettingService) GetIdentityPatchPrompt(ctx context.Context) string {
|
|
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyIdentityPatchPrompt)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
return value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
// GenerateAdminAPIKey 生成新的管理员 API Key
|
|
|
|
|
|
func (s *SettingService) GenerateAdminAPIKey(ctx context.Context) (string, error) {
|
2025-12-20 15:11:43 +08:00
|
|
|
|
// 生成 32 字节随机数 = 64 位十六进制字符
|
|
|
|
|
|
bytes := make([]byte, 32)
|
|
|
|
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("generate random bytes: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
key := AdminAPIKeyPrefix + hex.EncodeToString(bytes)
|
2025-12-20 15:11:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 存储到 settings 表
|
2026-01-04 19:27:53 +08:00
|
|
|
|
if err := s.settingRepo.Set(ctx, SettingKeyAdminAPIKey, key); err != nil {
|
2025-12-20 15:11:43 +08:00
|
|
|
|
return "", fmt.Errorf("save admin api key: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return key, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
// GetAdminAPIKeyStatus 获取管理员 API Key 状态
|
2025-12-20 15:11:43 +08:00
|
|
|
|
// 返回脱敏的 key、是否存在、错误
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (s *SettingService) GetAdminAPIKeyStatus(ctx context.Context) (maskedKey string, exists bool, err error) {
|
|
|
|
|
|
key, err := s.settingRepo.GetValue(ctx, SettingKeyAdminAPIKey)
|
2025-12-20 15:11:43 +08:00
|
|
|
|
if err != nil {
|
2025-12-25 20:52:47 +08:00
|
|
|
|
if errors.Is(err, ErrSettingNotFound) {
|
2025-12-20 15:11:43 +08:00
|
|
|
|
return "", false, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return "", false, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
|
return "", false, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 脱敏:显示前 10 位和后 4 位
|
|
|
|
|
|
if len(key) > 14 {
|
|
|
|
|
|
maskedKey = key[:10] + "..." + key[len(key)-4:]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
maskedKey = key
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return maskedKey, true, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
// GetAdminAPIKey 获取完整的管理员 API Key(仅供内部验证使用)
|
2025-12-20 15:11:43 +08:00
|
|
|
|
// 如果未配置返回空字符串和 nil 错误,只有数据库错误时才返回 error
|
2026-01-04 19:27:53 +08:00
|
|
|
|
func (s *SettingService) GetAdminAPIKey(ctx context.Context) (string, error) {
|
|
|
|
|
|
key, err := s.settingRepo.GetValue(ctx, SettingKeyAdminAPIKey)
|
2025-12-20 15:11:43 +08:00
|
|
|
|
if err != nil {
|
2025-12-25 20:52:47 +08:00
|
|
|
|
if errors.Is(err, ErrSettingNotFound) {
|
2025-12-20 15:11:43 +08:00
|
|
|
|
return "", nil // 未配置,返回空字符串
|
|
|
|
|
|
}
|
|
|
|
|
|
return "", err // 数据库错误
|
|
|
|
|
|
}
|
|
|
|
|
|
return key, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 19:27:53 +08:00
|
|
|
|
// DeleteAdminAPIKey 删除管理员 API Key
|
|
|
|
|
|
func (s *SettingService) DeleteAdminAPIKey(ctx context.Context) error {
|
|
|
|
|
|
return s.settingRepo.Delete(ctx, SettingKeyAdminAPIKey)
|
2026-01-03 06:37:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IsModelFallbackEnabled 检查是否启用模型兜底机制
|
|
|
|
|
|
func (s *SettingService) IsModelFallbackEnabled(ctx context.Context) bool {
|
|
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyEnableModelFallback)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false // Default: disabled
|
|
|
|
|
|
}
|
|
|
|
|
|
return value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetFallbackModel 获取指定平台的兜底模型
|
|
|
|
|
|
func (s *SettingService) GetFallbackModel(ctx context.Context, platform string) string {
|
|
|
|
|
|
var key string
|
|
|
|
|
|
var defaultModel string
|
|
|
|
|
|
|
|
|
|
|
|
switch platform {
|
|
|
|
|
|
case PlatformAnthropic:
|
|
|
|
|
|
key = SettingKeyFallbackModelAnthropic
|
|
|
|
|
|
defaultModel = "claude-3-5-sonnet-20241022"
|
|
|
|
|
|
case PlatformOpenAI:
|
|
|
|
|
|
key = SettingKeyFallbackModelOpenAI
|
|
|
|
|
|
defaultModel = "gpt-4o"
|
|
|
|
|
|
case PlatformGemini:
|
|
|
|
|
|
key = SettingKeyFallbackModelGemini
|
|
|
|
|
|
defaultModel = "gemini-2.5-pro"
|
|
|
|
|
|
case PlatformAntigravity:
|
|
|
|
|
|
key = SettingKeyFallbackModelAntigravity
|
|
|
|
|
|
defaultModel = "gemini-2.5-pro"
|
|
|
|
|
|
default:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, key)
|
|
|
|
|
|
if err != nil || value == "" {
|
|
|
|
|
|
return defaultModel
|
|
|
|
|
|
}
|
|
|
|
|
|
return value
|
2025-12-20 15:11:43 +08:00
|
|
|
|
}
|
2026-01-11 10:59:01 +08:00
|
|
|
|
|
|
|
|
|
|
// GetLinuxDoConnectOAuthConfig 返回用于登录的"最终生效" LinuxDo Connect 配置。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 优先级:
|
|
|
|
|
|
// - 若对应系统设置键存在,则覆盖 config.yaml/env 的值
|
|
|
|
|
|
// - 否则回退到 config.yaml/env 的值
|
|
|
|
|
|
func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (config.LinuxDoConnectConfig, error) {
|
|
|
|
|
|
if s == nil || s.cfg == nil {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "config not loaded")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
effective := s.cfg.LinuxDo
|
|
|
|
|
|
|
|
|
|
|
|
keys := []string{
|
|
|
|
|
|
SettingKeyLinuxDoConnectEnabled,
|
|
|
|
|
|
SettingKeyLinuxDoConnectClientID,
|
|
|
|
|
|
SettingKeyLinuxDoConnectClientSecret,
|
|
|
|
|
|
SettingKeyLinuxDoConnectRedirectURL,
|
|
|
|
|
|
}
|
|
|
|
|
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, fmt.Errorf("get linuxdo connect settings: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if raw, ok := settings[SettingKeyLinuxDoConnectEnabled]; ok {
|
|
|
|
|
|
effective.Enabled = raw == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
if v, ok := settings[SettingKeyLinuxDoConnectClientID]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
|
effective.ClientID = strings.TrimSpace(v)
|
|
|
|
|
|
}
|
|
|
|
|
|
if v, ok := settings[SettingKeyLinuxDoConnectClientSecret]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
|
effective.ClientSecret = strings.TrimSpace(v)
|
|
|
|
|
|
}
|
|
|
|
|
|
if v, ok := settings[SettingKeyLinuxDoConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
|
effective.RedirectURL = strings.TrimSpace(v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !effective.Enabled {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "oauth login is disabled")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 基础健壮性校验(避免把用户重定向到一个必然失败或不安全的 OAuth 流程里)。
|
|
|
|
|
|
if strings.TrimSpace(effective.ClientID) == "" {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client id not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(effective.AuthorizeURL) == "" {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(effective.TokenURL) == "" {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(effective.UserInfoURL) == "" {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(effective.RedirectURL) == "" {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.TrimSpace(effective.FrontendRedirectURL) == "" {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.AuthorizeURL); err != nil {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth authorize url invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.TokenURL); err != nil {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token url invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.UserInfoURL); err != nil {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth userinfo url invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := config.ValidateAbsoluteHTTPURL(effective.RedirectURL); err != nil {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth redirect url invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := config.ValidateFrontendRedirectURL(effective.FrontendRedirectURL); err != nil {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth frontend redirect url invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
method := strings.ToLower(strings.TrimSpace(effective.TokenAuthMethod))
|
|
|
|
|
|
switch method {
|
|
|
|
|
|
case "", "client_secret_post", "client_secret_basic":
|
|
|
|
|
|
if strings.TrimSpace(effective.ClientSecret) == "" {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth client secret not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
case "none":
|
|
|
|
|
|
if !effective.UsePKCE {
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth pkce must be enabled when token_auth_method=none")
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
return config.LinuxDoConnectConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "oauth token_auth_method invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return effective, nil
|
|
|
|
|
|
}
|
2026-01-11 21:54:52 -08:00
|
|
|
|
|
|
|
|
|
|
// GetStreamTimeoutSettings 获取流超时处理配置
|
|
|
|
|
|
func (s *SettingService) GetStreamTimeoutSettings(ctx context.Context) (*StreamTimeoutSettings, error) {
|
|
|
|
|
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyStreamTimeoutSettings)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
if errors.Is(err, ErrSettingNotFound) {
|
|
|
|
|
|
return DefaultStreamTimeoutSettings(), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, fmt.Errorf("get stream timeout settings: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if value == "" {
|
|
|
|
|
|
return DefaultStreamTimeoutSettings(), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var settings StreamTimeoutSettings
|
|
|
|
|
|
if err := json.Unmarshal([]byte(value), &settings); err != nil {
|
|
|
|
|
|
return DefaultStreamTimeoutSettings(), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证并修正配置值
|
|
|
|
|
|
if settings.TempUnschedMinutes < 1 {
|
|
|
|
|
|
settings.TempUnschedMinutes = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if settings.TempUnschedMinutes > 60 {
|
|
|
|
|
|
settings.TempUnschedMinutes = 60
|
|
|
|
|
|
}
|
|
|
|
|
|
if settings.ThresholdCount < 1 {
|
|
|
|
|
|
settings.ThresholdCount = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if settings.ThresholdCount > 10 {
|
|
|
|
|
|
settings.ThresholdCount = 10
|
|
|
|
|
|
}
|
|
|
|
|
|
if settings.ThresholdWindowMinutes < 1 {
|
|
|
|
|
|
settings.ThresholdWindowMinutes = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if settings.ThresholdWindowMinutes > 60 {
|
|
|
|
|
|
settings.ThresholdWindowMinutes = 60
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 action
|
|
|
|
|
|
switch settings.Action {
|
|
|
|
|
|
case StreamTimeoutActionTempUnsched, StreamTimeoutActionError, StreamTimeoutActionNone:
|
|
|
|
|
|
// valid
|
|
|
|
|
|
default:
|
|
|
|
|
|
settings.Action = StreamTimeoutActionTempUnsched
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &settings, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SetStreamTimeoutSettings 设置流超时处理配置
|
|
|
|
|
|
func (s *SettingService) SetStreamTimeoutSettings(ctx context.Context, settings *StreamTimeoutSettings) error {
|
|
|
|
|
|
if settings == nil {
|
|
|
|
|
|
return fmt.Errorf("settings cannot be nil")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证配置值
|
|
|
|
|
|
if settings.TempUnschedMinutes < 1 || settings.TempUnschedMinutes > 60 {
|
|
|
|
|
|
return fmt.Errorf("temp_unsched_minutes must be between 1-60")
|
|
|
|
|
|
}
|
|
|
|
|
|
if settings.ThresholdCount < 1 || settings.ThresholdCount > 10 {
|
|
|
|
|
|
return fmt.Errorf("threshold_count must be between 1-10")
|
|
|
|
|
|
}
|
|
|
|
|
|
if settings.ThresholdWindowMinutes < 1 || settings.ThresholdWindowMinutes > 60 {
|
|
|
|
|
|
return fmt.Errorf("threshold_window_minutes must be between 1-60")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch settings.Action {
|
|
|
|
|
|
case StreamTimeoutActionTempUnsched, StreamTimeoutActionError, StreamTimeoutActionNone:
|
|
|
|
|
|
// valid
|
|
|
|
|
|
default:
|
|
|
|
|
|
return fmt.Errorf("invalid action: %s", settings.Action)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data, err := json.Marshal(settings)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("marshal stream timeout settings: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return s.settingRepo.Set(ctx, SettingKeyStreamTimeoutSettings, string(data))
|
|
|
|
|
|
}
|