feat(sora): 新增 Sora 平台支持并修复高危安全和性能问题
新增功能: - 新增 Sora 账号管理和 OAuth 认证 - 新增 Sora 视频/图片生成 API 网关 - 新增 Sora 任务调度和缓存机制 - 新增 Sora 使用统计和计费支持 - 前端增加 Sora 平台配置界面 安全修复(代码审核): - [SEC-001] 限制媒体下载响应体大小(图片 20MB、视频 200MB),防止 DoS 攻击 - [SEC-002] 限制 SDK API 响应大小(1MB),防止内存耗尽 - [SEC-003] 修复 SSRF 风险,添加 URL 验证并强制使用代理配置 BUG 修复(代码审核): - [BUG-001] 修复 for 循环内 defer 累积导致的资源泄漏 - [BUG-002] 修复图片并发槽位获取失败时已持有锁未释放的永久泄漏 性能优化(代码审核): - [PERF-001] 添加 Sentinel Token 缓存(3 分钟有效期),减少 PoW 计算开销 技术细节: - 使用 io.LimitReader 限制所有外部输入的大小 - 添加 urlvalidator 验证防止 SSRF 攻击 - 使用 sync.Map 实现线程安全的包级缓存 - 优化并发槽位管理,添加 releaseAll 模式防止泄漏 影响范围: - 后端:新增 Sora 相关数据模型、服务、网关和管理接口 - 前端:新增 Sora 平台配置、账号管理和监控界面 - 配置:新增 Sora 相关配置项和环境变量 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,23 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
FallbackModelAntigravity: settings.FallbackModelAntigravity,
|
||||
EnableIdentityPatch: settings.EnableIdentityPatch,
|
||||
IdentityPatchPrompt: settings.IdentityPatchPrompt,
|
||||
SoraBaseURL: settings.SoraBaseURL,
|
||||
SoraTimeout: settings.SoraTimeout,
|
||||
SoraMaxRetries: settings.SoraMaxRetries,
|
||||
SoraPollInterval: settings.SoraPollInterval,
|
||||
SoraCallLogicMode: settings.SoraCallLogicMode,
|
||||
SoraCacheEnabled: settings.SoraCacheEnabled,
|
||||
SoraCacheBaseDir: settings.SoraCacheBaseDir,
|
||||
SoraCacheVideoDir: settings.SoraCacheVideoDir,
|
||||
SoraCacheMaxBytes: settings.SoraCacheMaxBytes,
|
||||
SoraCacheAllowedHosts: settings.SoraCacheAllowedHosts,
|
||||
SoraCacheUserDirEnabled: settings.SoraCacheUserDirEnabled,
|
||||
SoraWatermarkFreeEnabled: settings.SoraWatermarkFreeEnabled,
|
||||
SoraWatermarkFreeParseMethod: settings.SoraWatermarkFreeParseMethod,
|
||||
SoraWatermarkFreeCustomParseURL: settings.SoraWatermarkFreeCustomParseURL,
|
||||
SoraWatermarkFreeCustomParseToken: settings.SoraWatermarkFreeCustomParseToken,
|
||||
SoraWatermarkFreeFallbackOnFailure: settings.SoraWatermarkFreeFallbackOnFailure,
|
||||
SoraTokenRefreshEnabled: settings.SoraTokenRefreshEnabled,
|
||||
OpsMonitoringEnabled: opsEnabled && settings.OpsMonitoringEnabled,
|
||||
OpsRealtimeMonitoringEnabled: settings.OpsRealtimeMonitoringEnabled,
|
||||
OpsQueryModeDefault: settings.OpsQueryModeDefault,
|
||||
@@ -138,6 +155,25 @@ type UpdateSettingsRequest struct {
|
||||
EnableIdentityPatch bool `json:"enable_identity_patch"`
|
||||
IdentityPatchPrompt string `json:"identity_patch_prompt"`
|
||||
|
||||
// Sora configuration
|
||||
SoraBaseURL string `json:"sora_base_url"`
|
||||
SoraTimeout int `json:"sora_timeout"`
|
||||
SoraMaxRetries int `json:"sora_max_retries"`
|
||||
SoraPollInterval float64 `json:"sora_poll_interval"`
|
||||
SoraCallLogicMode string `json:"sora_call_logic_mode"`
|
||||
SoraCacheEnabled bool `json:"sora_cache_enabled"`
|
||||
SoraCacheBaseDir string `json:"sora_cache_base_dir"`
|
||||
SoraCacheVideoDir string `json:"sora_cache_video_dir"`
|
||||
SoraCacheMaxBytes int64 `json:"sora_cache_max_bytes"`
|
||||
SoraCacheAllowedHosts []string `json:"sora_cache_allowed_hosts"`
|
||||
SoraCacheUserDirEnabled bool `json:"sora_cache_user_dir_enabled"`
|
||||
SoraWatermarkFreeEnabled bool `json:"sora_watermark_free_enabled"`
|
||||
SoraWatermarkFreeParseMethod string `json:"sora_watermark_free_parse_method"`
|
||||
SoraWatermarkFreeCustomParseURL string `json:"sora_watermark_free_custom_parse_url"`
|
||||
SoraWatermarkFreeCustomParseToken string `json:"sora_watermark_free_custom_parse_token"`
|
||||
SoraWatermarkFreeFallbackOnFailure bool `json:"sora_watermark_free_fallback_on_failure"`
|
||||
SoraTokenRefreshEnabled bool `json:"sora_token_refresh_enabled"`
|
||||
|
||||
// Ops monitoring (vNext)
|
||||
OpsMonitoringEnabled *bool `json:"ops_monitoring_enabled"`
|
||||
OpsRealtimeMonitoringEnabled *bool `json:"ops_realtime_monitoring_enabled"`
|
||||
@@ -227,6 +263,32 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sora 参数校验与清理
|
||||
req.SoraBaseURL = strings.TrimSpace(req.SoraBaseURL)
|
||||
if req.SoraBaseURL == "" {
|
||||
req.SoraBaseURL = previousSettings.SoraBaseURL
|
||||
}
|
||||
if req.SoraBaseURL != "" {
|
||||
if err := config.ValidateAbsoluteHTTPURL(req.SoraBaseURL); err != nil {
|
||||
response.BadRequest(c, "Sora Base URL must be an absolute http(s) URL")
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.SoraTimeout <= 0 {
|
||||
req.SoraTimeout = previousSettings.SoraTimeout
|
||||
}
|
||||
if req.SoraMaxRetries < 0 {
|
||||
req.SoraMaxRetries = previousSettings.SoraMaxRetries
|
||||
}
|
||||
if req.SoraPollInterval <= 0 {
|
||||
req.SoraPollInterval = previousSettings.SoraPollInterval
|
||||
}
|
||||
if req.SoraCacheMaxBytes < 0 {
|
||||
req.SoraCacheMaxBytes = 0
|
||||
}
|
||||
req.SoraCacheAllowedHosts = normalizeStringList(req.SoraCacheAllowedHosts)
|
||||
req.SoraWatermarkFreeCustomParseURL = strings.TrimSpace(req.SoraWatermarkFreeCustomParseURL)
|
||||
|
||||
// Ops metrics collector interval validation (seconds).
|
||||
if req.OpsMetricsIntervalSeconds != nil {
|
||||
v := *req.OpsMetricsIntervalSeconds
|
||||
@@ -240,40 +302,57 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
settings := &service.SystemSettings{
|
||||
RegistrationEnabled: req.RegistrationEnabled,
|
||||
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
||||
PromoCodeEnabled: req.PromoCodeEnabled,
|
||||
SMTPHost: req.SMTPHost,
|
||||
SMTPPort: req.SMTPPort,
|
||||
SMTPUsername: req.SMTPUsername,
|
||||
SMTPPassword: req.SMTPPassword,
|
||||
SMTPFrom: req.SMTPFrom,
|
||||
SMTPFromName: req.SMTPFromName,
|
||||
SMTPUseTLS: req.SMTPUseTLS,
|
||||
TurnstileEnabled: req.TurnstileEnabled,
|
||||
TurnstileSiteKey: req.TurnstileSiteKey,
|
||||
TurnstileSecretKey: req.TurnstileSecretKey,
|
||||
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
|
||||
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
||||
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
||||
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
|
||||
SiteName: req.SiteName,
|
||||
SiteLogo: req.SiteLogo,
|
||||
SiteSubtitle: req.SiteSubtitle,
|
||||
APIBaseURL: req.APIBaseURL,
|
||||
ContactInfo: req.ContactInfo,
|
||||
DocURL: req.DocURL,
|
||||
HomeContent: req.HomeContent,
|
||||
HideCcsImportButton: req.HideCcsImportButton,
|
||||
DefaultConcurrency: req.DefaultConcurrency,
|
||||
DefaultBalance: req.DefaultBalance,
|
||||
EnableModelFallback: req.EnableModelFallback,
|
||||
FallbackModelAnthropic: req.FallbackModelAnthropic,
|
||||
FallbackModelOpenAI: req.FallbackModelOpenAI,
|
||||
FallbackModelGemini: req.FallbackModelGemini,
|
||||
FallbackModelAntigravity: req.FallbackModelAntigravity,
|
||||
EnableIdentityPatch: req.EnableIdentityPatch,
|
||||
IdentityPatchPrompt: req.IdentityPatchPrompt,
|
||||
RegistrationEnabled: req.RegistrationEnabled,
|
||||
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
||||
PromoCodeEnabled: req.PromoCodeEnabled,
|
||||
SMTPHost: req.SMTPHost,
|
||||
SMTPPort: req.SMTPPort,
|
||||
SMTPUsername: req.SMTPUsername,
|
||||
SMTPPassword: req.SMTPPassword,
|
||||
SMTPFrom: req.SMTPFrom,
|
||||
SMTPFromName: req.SMTPFromName,
|
||||
SMTPUseTLS: req.SMTPUseTLS,
|
||||
TurnstileEnabled: req.TurnstileEnabled,
|
||||
TurnstileSiteKey: req.TurnstileSiteKey,
|
||||
TurnstileSecretKey: req.TurnstileSecretKey,
|
||||
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
|
||||
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
||||
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
||||
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
|
||||
SiteName: req.SiteName,
|
||||
SiteLogo: req.SiteLogo,
|
||||
SiteSubtitle: req.SiteSubtitle,
|
||||
APIBaseURL: req.APIBaseURL,
|
||||
ContactInfo: req.ContactInfo,
|
||||
DocURL: req.DocURL,
|
||||
HomeContent: req.HomeContent,
|
||||
HideCcsImportButton: req.HideCcsImportButton,
|
||||
DefaultConcurrency: req.DefaultConcurrency,
|
||||
DefaultBalance: req.DefaultBalance,
|
||||
EnableModelFallback: req.EnableModelFallback,
|
||||
FallbackModelAnthropic: req.FallbackModelAnthropic,
|
||||
FallbackModelOpenAI: req.FallbackModelOpenAI,
|
||||
FallbackModelGemini: req.FallbackModelGemini,
|
||||
FallbackModelAntigravity: req.FallbackModelAntigravity,
|
||||
EnableIdentityPatch: req.EnableIdentityPatch,
|
||||
IdentityPatchPrompt: req.IdentityPatchPrompt,
|
||||
SoraBaseURL: req.SoraBaseURL,
|
||||
SoraTimeout: req.SoraTimeout,
|
||||
SoraMaxRetries: req.SoraMaxRetries,
|
||||
SoraPollInterval: req.SoraPollInterval,
|
||||
SoraCallLogicMode: req.SoraCallLogicMode,
|
||||
SoraCacheEnabled: req.SoraCacheEnabled,
|
||||
SoraCacheBaseDir: req.SoraCacheBaseDir,
|
||||
SoraCacheVideoDir: req.SoraCacheVideoDir,
|
||||
SoraCacheMaxBytes: req.SoraCacheMaxBytes,
|
||||
SoraCacheAllowedHosts: req.SoraCacheAllowedHosts,
|
||||
SoraCacheUserDirEnabled: req.SoraCacheUserDirEnabled,
|
||||
SoraWatermarkFreeEnabled: req.SoraWatermarkFreeEnabled,
|
||||
SoraWatermarkFreeParseMethod: req.SoraWatermarkFreeParseMethod,
|
||||
SoraWatermarkFreeCustomParseURL: req.SoraWatermarkFreeCustomParseURL,
|
||||
SoraWatermarkFreeCustomParseToken: req.SoraWatermarkFreeCustomParseToken,
|
||||
SoraWatermarkFreeFallbackOnFailure: req.SoraWatermarkFreeFallbackOnFailure,
|
||||
SoraTokenRefreshEnabled: req.SoraTokenRefreshEnabled,
|
||||
OpsMonitoringEnabled: func() bool {
|
||||
if req.OpsMonitoringEnabled != nil {
|
||||
return *req.OpsMonitoringEnabled
|
||||
@@ -349,6 +428,23 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity,
|
||||
EnableIdentityPatch: updatedSettings.EnableIdentityPatch,
|
||||
IdentityPatchPrompt: updatedSettings.IdentityPatchPrompt,
|
||||
SoraBaseURL: updatedSettings.SoraBaseURL,
|
||||
SoraTimeout: updatedSettings.SoraTimeout,
|
||||
SoraMaxRetries: updatedSettings.SoraMaxRetries,
|
||||
SoraPollInterval: updatedSettings.SoraPollInterval,
|
||||
SoraCallLogicMode: updatedSettings.SoraCallLogicMode,
|
||||
SoraCacheEnabled: updatedSettings.SoraCacheEnabled,
|
||||
SoraCacheBaseDir: updatedSettings.SoraCacheBaseDir,
|
||||
SoraCacheVideoDir: updatedSettings.SoraCacheVideoDir,
|
||||
SoraCacheMaxBytes: updatedSettings.SoraCacheMaxBytes,
|
||||
SoraCacheAllowedHosts: updatedSettings.SoraCacheAllowedHosts,
|
||||
SoraCacheUserDirEnabled: updatedSettings.SoraCacheUserDirEnabled,
|
||||
SoraWatermarkFreeEnabled: updatedSettings.SoraWatermarkFreeEnabled,
|
||||
SoraWatermarkFreeParseMethod: updatedSettings.SoraWatermarkFreeParseMethod,
|
||||
SoraWatermarkFreeCustomParseURL: updatedSettings.SoraWatermarkFreeCustomParseURL,
|
||||
SoraWatermarkFreeCustomParseToken: updatedSettings.SoraWatermarkFreeCustomParseToken,
|
||||
SoraWatermarkFreeFallbackOnFailure: updatedSettings.SoraWatermarkFreeFallbackOnFailure,
|
||||
SoraTokenRefreshEnabled: updatedSettings.SoraTokenRefreshEnabled,
|
||||
OpsMonitoringEnabled: updatedSettings.OpsMonitoringEnabled,
|
||||
OpsRealtimeMonitoringEnabled: updatedSettings.OpsRealtimeMonitoringEnabled,
|
||||
OpsQueryModeDefault: updatedSettings.OpsQueryModeDefault,
|
||||
@@ -477,6 +573,57 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
if before.IdentityPatchPrompt != after.IdentityPatchPrompt {
|
||||
changed = append(changed, "identity_patch_prompt")
|
||||
}
|
||||
if before.SoraBaseURL != after.SoraBaseURL {
|
||||
changed = append(changed, "sora_base_url")
|
||||
}
|
||||
if before.SoraTimeout != after.SoraTimeout {
|
||||
changed = append(changed, "sora_timeout")
|
||||
}
|
||||
if before.SoraMaxRetries != after.SoraMaxRetries {
|
||||
changed = append(changed, "sora_max_retries")
|
||||
}
|
||||
if before.SoraPollInterval != after.SoraPollInterval {
|
||||
changed = append(changed, "sora_poll_interval")
|
||||
}
|
||||
if before.SoraCallLogicMode != after.SoraCallLogicMode {
|
||||
changed = append(changed, "sora_call_logic_mode")
|
||||
}
|
||||
if before.SoraCacheEnabled != after.SoraCacheEnabled {
|
||||
changed = append(changed, "sora_cache_enabled")
|
||||
}
|
||||
if before.SoraCacheBaseDir != after.SoraCacheBaseDir {
|
||||
changed = append(changed, "sora_cache_base_dir")
|
||||
}
|
||||
if before.SoraCacheVideoDir != after.SoraCacheVideoDir {
|
||||
changed = append(changed, "sora_cache_video_dir")
|
||||
}
|
||||
if before.SoraCacheMaxBytes != after.SoraCacheMaxBytes {
|
||||
changed = append(changed, "sora_cache_max_bytes")
|
||||
}
|
||||
if strings.Join(before.SoraCacheAllowedHosts, ",") != strings.Join(after.SoraCacheAllowedHosts, ",") {
|
||||
changed = append(changed, "sora_cache_allowed_hosts")
|
||||
}
|
||||
if before.SoraCacheUserDirEnabled != after.SoraCacheUserDirEnabled {
|
||||
changed = append(changed, "sora_cache_user_dir_enabled")
|
||||
}
|
||||
if before.SoraWatermarkFreeEnabled != after.SoraWatermarkFreeEnabled {
|
||||
changed = append(changed, "sora_watermark_free_enabled")
|
||||
}
|
||||
if before.SoraWatermarkFreeParseMethod != after.SoraWatermarkFreeParseMethod {
|
||||
changed = append(changed, "sora_watermark_free_parse_method")
|
||||
}
|
||||
if before.SoraWatermarkFreeCustomParseURL != after.SoraWatermarkFreeCustomParseURL {
|
||||
changed = append(changed, "sora_watermark_free_custom_parse_url")
|
||||
}
|
||||
if before.SoraWatermarkFreeCustomParseToken != after.SoraWatermarkFreeCustomParseToken {
|
||||
changed = append(changed, "sora_watermark_free_custom_parse_token")
|
||||
}
|
||||
if before.SoraWatermarkFreeFallbackOnFailure != after.SoraWatermarkFreeFallbackOnFailure {
|
||||
changed = append(changed, "sora_watermark_free_fallback_on_failure")
|
||||
}
|
||||
if before.SoraTokenRefreshEnabled != after.SoraTokenRefreshEnabled {
|
||||
changed = append(changed, "sora_token_refresh_enabled")
|
||||
}
|
||||
if before.OpsMonitoringEnabled != after.OpsMonitoringEnabled {
|
||||
changed = append(changed, "ops_monitoring_enabled")
|
||||
}
|
||||
@@ -492,6 +639,19 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
||||
return changed
|
||||
}
|
||||
|
||||
func normalizeStringList(values []string) []string {
|
||||
if len(values) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
normalized := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||
normalized = append(normalized, trimmed)
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// TestSMTPRequest 测试SMTP连接请求
|
||||
type TestSMTPRequest struct {
|
||||
SMTPHost string `json:"smtp_host" binding:"required"`
|
||||
|
||||
Reference in New Issue
Block a user