Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
510 lines
16 KiB
Go
510 lines
16 KiB
Go
package admin
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// GetAdminAPIKey 获取管理员 API Key 状态
|
|
// GET /api/v1/admin/settings/admin-api-key
|
|
func (h *SettingHandler) GetAdminAPIKey(c *gin.Context) {
|
|
maskedKey, exists, err := h.settingService.GetAdminAPIKeyStatus(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"exists": exists,
|
|
"masked_key": maskedKey,
|
|
})
|
|
}
|
|
|
|
// RegenerateAdminAPIKey 生成/重新生成管理员 API Key
|
|
// POST /api/v1/admin/settings/admin-api-key/regenerate
|
|
func (h *SettingHandler) RegenerateAdminAPIKey(c *gin.Context) {
|
|
key, err := h.settingService.GenerateAdminAPIKey(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"key": key, // 完整 key 只在生成时返回一次
|
|
})
|
|
}
|
|
|
|
// DeleteAdminAPIKey 删除管理员 API Key
|
|
// DELETE /api/v1/admin/settings/admin-api-key
|
|
func (h *SettingHandler) DeleteAdminAPIKey(c *gin.Context) {
|
|
if err := h.settingService.DeleteAdminAPIKey(c.Request.Context()); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"message": "Admin API key deleted"})
|
|
}
|
|
|
|
// GetOverloadCooldownSettings 获取529过载冷却配置
|
|
// GET /api/v1/admin/settings/overload-cooldown
|
|
func (h *SettingHandler) GetOverloadCooldownSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetOverloadCooldownSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.OverloadCooldownSettings{
|
|
Enabled: settings.Enabled,
|
|
CooldownMinutes: settings.CooldownMinutes,
|
|
})
|
|
}
|
|
|
|
// UpdateOverloadCooldownSettingsRequest 更新529过载冷却配置请求
|
|
type UpdateOverloadCooldownSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
CooldownMinutes int `json:"cooldown_minutes"`
|
|
}
|
|
|
|
// UpdateOverloadCooldownSettings 更新529过载冷却配置
|
|
// PUT /api/v1/admin/settings/overload-cooldown
|
|
func (h *SettingHandler) UpdateOverloadCooldownSettings(c *gin.Context) {
|
|
var req UpdateOverloadCooldownSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
settings := &service.OverloadCooldownSettings{
|
|
Enabled: req.Enabled,
|
|
CooldownMinutes: req.CooldownMinutes,
|
|
}
|
|
|
|
if err := h.settingService.SetOverloadCooldownSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
updatedSettings, err := h.settingService.GetOverloadCooldownSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.OverloadCooldownSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
CooldownMinutes: updatedSettings.CooldownMinutes,
|
|
})
|
|
}
|
|
|
|
// GetRateLimit429CooldownSettings 获取429默认回避配置
|
|
// GET /api/v1/admin/settings/rate-limit-429-cooldown
|
|
func (h *SettingHandler) GetRateLimit429CooldownSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetRateLimit429CooldownSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.RateLimit429CooldownSettings{
|
|
Enabled: settings.Enabled,
|
|
CooldownSeconds: settings.CooldownSeconds,
|
|
})
|
|
}
|
|
|
|
// UpdateRateLimit429CooldownSettingsRequest 更新429默认回避配置请求
|
|
type UpdateRateLimit429CooldownSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
CooldownSeconds int `json:"cooldown_seconds"`
|
|
}
|
|
|
|
// UpdateRateLimit429CooldownSettings 更新429默认回避配置
|
|
// PUT /api/v1/admin/settings/rate-limit-429-cooldown
|
|
func (h *SettingHandler) UpdateRateLimit429CooldownSettings(c *gin.Context) {
|
|
var req UpdateRateLimit429CooldownSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
settings := &service.RateLimit429CooldownSettings{
|
|
Enabled: req.Enabled,
|
|
CooldownSeconds: req.CooldownSeconds,
|
|
}
|
|
|
|
if err := h.settingService.SetRateLimit429CooldownSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
updatedSettings, err := h.settingService.GetRateLimit429CooldownSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.RateLimit429CooldownSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
CooldownSeconds: updatedSettings.CooldownSeconds,
|
|
})
|
|
}
|
|
|
|
// GetPanelRateLimitSettings 获取面板 API 限流配置
|
|
// GET /api/v1/admin/settings/panel-rate-limit
|
|
func (h *SettingHandler) GetPanelRateLimitSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetPanelRateLimitSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.PanelRateLimitSettings{
|
|
Enabled: settings.Enabled,
|
|
UserRPM: settings.UserRPM,
|
|
HeavyRPM: settings.HeavyRPM,
|
|
ExemptAdmin: settings.ExemptAdmin,
|
|
PublicIPRPM: settings.PublicIPRPM,
|
|
})
|
|
}
|
|
|
|
// UpdatePanelRateLimitSettingsRequest 更新面板 API 限流配置请求
|
|
type UpdatePanelRateLimitSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
UserRPM int `json:"user_rpm"`
|
|
HeavyRPM int `json:"heavy_rpm"`
|
|
ExemptAdmin bool `json:"exempt_admin"`
|
|
PublicIPRPM int `json:"public_ip_rpm"`
|
|
}
|
|
|
|
// UpdatePanelRateLimitSettings 更新面板 API 限流配置
|
|
// PUT /api/v1/admin/settings/panel-rate-limit
|
|
func (h *SettingHandler) UpdatePanelRateLimitSettings(c *gin.Context) {
|
|
var req UpdatePanelRateLimitSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
settings := &service.PanelRateLimitSettings{
|
|
Enabled: req.Enabled,
|
|
UserRPM: req.UserRPM,
|
|
HeavyRPM: req.HeavyRPM,
|
|
ExemptAdmin: req.ExemptAdmin,
|
|
PublicIPRPM: req.PublicIPRPM,
|
|
}
|
|
|
|
if err := h.settingService.SetPanelRateLimitSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
updatedSettings, err := h.settingService.GetPanelRateLimitSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.PanelRateLimitSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
UserRPM: updatedSettings.UserRPM,
|
|
HeavyRPM: updatedSettings.HeavyRPM,
|
|
ExemptAdmin: updatedSettings.ExemptAdmin,
|
|
PublicIPRPM: updatedSettings.PublicIPRPM,
|
|
})
|
|
}
|
|
|
|
// GetStreamTimeoutSettings 获取流超时处理配置
|
|
// GET /api/v1/admin/settings/stream-timeout
|
|
func (h *SettingHandler) GetStreamTimeoutSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetStreamTimeoutSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.StreamTimeoutSettings{
|
|
Enabled: settings.Enabled,
|
|
Action: settings.Action,
|
|
TempUnschedMinutes: settings.TempUnschedMinutes,
|
|
ThresholdCount: settings.ThresholdCount,
|
|
ThresholdWindowMinutes: settings.ThresholdWindowMinutes,
|
|
})
|
|
}
|
|
|
|
// GetRectifierSettings 获取请求整流器配置
|
|
// GET /api/v1/admin/settings/rectifier
|
|
func (h *SettingHandler) GetRectifierSettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetRectifierSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
patterns := settings.APIKeySignaturePatterns
|
|
if patterns == nil {
|
|
patterns = []string{}
|
|
}
|
|
response.Success(c, dto.RectifierSettings{
|
|
Enabled: settings.Enabled,
|
|
ThinkingSignatureEnabled: settings.ThinkingSignatureEnabled,
|
|
ThinkingBudgetEnabled: settings.ThinkingBudgetEnabled,
|
|
APIKeySignatureEnabled: settings.APIKeySignatureEnabled,
|
|
APIKeySignaturePatterns: patterns,
|
|
})
|
|
}
|
|
|
|
// UpdateRectifierSettingsRequest 更新整流器配置请求
|
|
type UpdateRectifierSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"`
|
|
ThinkingBudgetEnabled bool `json:"thinking_budget_enabled"`
|
|
APIKeySignatureEnabled bool `json:"apikey_signature_enabled"`
|
|
APIKeySignaturePatterns []string `json:"apikey_signature_patterns"`
|
|
}
|
|
|
|
// UpdateRectifierSettings 更新请求整流器配置
|
|
// PUT /api/v1/admin/settings/rectifier
|
|
func (h *SettingHandler) UpdateRectifierSettings(c *gin.Context) {
|
|
var req UpdateRectifierSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// 校验并清理自定义匹配关键词
|
|
const maxPatterns = 50
|
|
const maxPatternLen = 500
|
|
if len(req.APIKeySignaturePatterns) > maxPatterns {
|
|
response.BadRequest(c, "Too many signature patterns (max 50)")
|
|
return
|
|
}
|
|
var cleanedPatterns []string
|
|
for _, p := range req.APIKeySignaturePatterns {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
if len(p) > maxPatternLen {
|
|
response.BadRequest(c, "Signature pattern too long (max 500 characters)")
|
|
return
|
|
}
|
|
cleanedPatterns = append(cleanedPatterns, p)
|
|
}
|
|
|
|
settings := &service.RectifierSettings{
|
|
Enabled: req.Enabled,
|
|
ThinkingSignatureEnabled: req.ThinkingSignatureEnabled,
|
|
ThinkingBudgetEnabled: req.ThinkingBudgetEnabled,
|
|
APIKeySignatureEnabled: req.APIKeySignatureEnabled,
|
|
APIKeySignaturePatterns: cleanedPatterns,
|
|
}
|
|
|
|
if err := h.settingService.SetRectifierSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// 重新获取设置返回
|
|
updatedSettings, err := h.settingService.GetRectifierSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
updatedPatterns := updatedSettings.APIKeySignaturePatterns
|
|
if updatedPatterns == nil {
|
|
updatedPatterns = []string{}
|
|
}
|
|
response.Success(c, dto.RectifierSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
ThinkingSignatureEnabled: updatedSettings.ThinkingSignatureEnabled,
|
|
ThinkingBudgetEnabled: updatedSettings.ThinkingBudgetEnabled,
|
|
APIKeySignatureEnabled: updatedSettings.APIKeySignatureEnabled,
|
|
APIKeySignaturePatterns: updatedPatterns,
|
|
})
|
|
}
|
|
|
|
// GetBetaPolicySettings 获取 Beta 策略配置
|
|
// GET /api/v1/admin/settings/beta-policy
|
|
func (h *SettingHandler) GetBetaPolicySettings(c *gin.Context) {
|
|
settings, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
rules := make([]dto.BetaPolicyRule, len(settings.Rules))
|
|
for i, r := range settings.Rules {
|
|
rules[i] = dto.BetaPolicyRule(r)
|
|
}
|
|
response.Success(c, dto.BetaPolicySettings{Rules: rules})
|
|
}
|
|
|
|
// UpdateBetaPolicySettingsRequest 更新 Beta 策略配置请求
|
|
type UpdateBetaPolicySettingsRequest struct {
|
|
Rules []dto.BetaPolicyRule `json:"rules"`
|
|
}
|
|
|
|
// UpdateBetaPolicySettings 更新 Beta 策略配置
|
|
// PUT /api/v1/admin/settings/beta-policy
|
|
func (h *SettingHandler) UpdateBetaPolicySettings(c *gin.Context) {
|
|
var req UpdateBetaPolicySettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
rules := make([]service.BetaPolicyRule, len(req.Rules))
|
|
for i, r := range req.Rules {
|
|
rules[i] = service.BetaPolicyRule(r)
|
|
}
|
|
|
|
settings := &service.BetaPolicySettings{Rules: rules}
|
|
if err := h.settingService.SetBetaPolicySettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// Re-fetch to return updated settings
|
|
updated, err := h.settingService.GetBetaPolicySettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
outRules := make([]dto.BetaPolicyRule, len(updated.Rules))
|
|
for i, r := range updated.Rules {
|
|
outRules[i] = dto.BetaPolicyRule(r)
|
|
}
|
|
response.Success(c, dto.BetaPolicySettings{Rules: outRules})
|
|
}
|
|
|
|
// UpdateStreamTimeoutSettingsRequest 更新流超时配置请求
|
|
type UpdateStreamTimeoutSettingsRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
Action string `json:"action"`
|
|
TempUnschedMinutes int `json:"temp_unsched_minutes"`
|
|
ThresholdCount int `json:"threshold_count"`
|
|
ThresholdWindowMinutes int `json:"threshold_window_minutes"`
|
|
}
|
|
|
|
// UpdateStreamTimeoutSettings 更新流超时处理配置
|
|
// PUT /api/v1/admin/settings/stream-timeout
|
|
func (h *SettingHandler) UpdateStreamTimeoutSettings(c *gin.Context) {
|
|
var req UpdateStreamTimeoutSettingsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
settings := &service.StreamTimeoutSettings{
|
|
Enabled: req.Enabled,
|
|
Action: req.Action,
|
|
TempUnschedMinutes: req.TempUnschedMinutes,
|
|
ThresholdCount: req.ThresholdCount,
|
|
ThresholdWindowMinutes: req.ThresholdWindowMinutes,
|
|
}
|
|
|
|
if err := h.settingService.SetStreamTimeoutSettings(c.Request.Context(), settings); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
// 重新获取设置返回
|
|
updatedSettings, err := h.settingService.GetStreamTimeoutSettings(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
response.Success(c, dto.StreamTimeoutSettings{
|
|
Enabled: updatedSettings.Enabled,
|
|
Action: updatedSettings.Action,
|
|
TempUnschedMinutes: updatedSettings.TempUnschedMinutes,
|
|
ThresholdCount: updatedSettings.ThresholdCount,
|
|
ThresholdWindowMinutes: updatedSettings.ThresholdWindowMinutes,
|
|
})
|
|
}
|
|
|
|
// GetWebSearchEmulationConfig 获取 Web Search 模拟配置
|
|
// GET /api/v1/admin/settings/web-search-emulation
|
|
func (h *SettingHandler) GetWebSearchEmulationConfig(c *gin.Context) {
|
|
cfg, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, service.PopulateWebSearchUsage(c.Request.Context(), cfg))
|
|
}
|
|
|
|
// UpdateWebSearchEmulationConfig 更新 Web Search 模拟配置
|
|
// PUT /api/v1/admin/settings/web-search-emulation
|
|
func (h *SettingHandler) UpdateWebSearchEmulationConfig(c *gin.Context) {
|
|
var cfg service.WebSearchEmulationConfig
|
|
if err := c.ShouldBindJSON(&cfg); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.settingService.SaveWebSearchEmulationConfig(c.Request.Context(), &cfg); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
|
|
// Re-read (with sanitized api keys) to return current state
|
|
updated, err := h.settingService.GetWebSearchEmulationConfig(c.Request.Context())
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, service.PopulateWebSearchUsage(c.Request.Context(), updated))
|
|
}
|
|
|
|
// ResetWebSearchUsage 重置指定 provider 的配额用量
|
|
// POST /api/v1/admin/settings/web-search-emulation/reset-usage
|
|
func (h *SettingHandler) ResetWebSearchUsage(c *gin.Context) {
|
|
var req struct {
|
|
ProviderType string `json:"provider_type"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
if req.ProviderType == "" {
|
|
response.BadRequest(c, "provider_type is required")
|
|
return
|
|
}
|
|
if err := service.ResetWebSearchUsage(c.Request.Context(), req.ProviderType); err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, nil)
|
|
}
|
|
|
|
// TestWebSearchEmulation 测试 Web Search 搜索
|
|
// POST /api/v1/admin/settings/web-search-emulation/test
|
|
func (h *SettingHandler) TestWebSearchEmulation(c *gin.Context) {
|
|
var req struct {
|
|
Query string `json:"query"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Query) == "" {
|
|
req.Query = "搜索今年世界大事件"
|
|
}
|
|
|
|
result, err := service.TestWebSearch(c.Request.Context(), req.Query)
|
|
if err != nil {
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
response.Success(c, result)
|
|
}
|