feat: 支持自定义端点配置与展示
This commit is contained in:
@@ -110,6 +110,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
SoraClientEnabled: settings.SoraClientEnabled,
|
||||
CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems),
|
||||
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
|
||||
DefaultConcurrency: settings.DefaultConcurrency,
|
||||
DefaultBalance: settings.DefaultBalance,
|
||||
DefaultSubscriptions: defaultSubscriptions,
|
||||
@@ -176,6 +177,7 @@ type UpdateSettingsRequest struct {
|
||||
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
|
||||
SoraClientEnabled bool `json:"sora_client_enabled"`
|
||||
CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"`
|
||||
CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"`
|
||||
|
||||
// 默认配置
|
||||
DefaultConcurrency int `json:"default_concurrency"`
|
||||
@@ -417,6 +419,55 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
customMenuJSON = string(menuBytes)
|
||||
}
|
||||
|
||||
// 自定义端点验证
|
||||
const (
|
||||
maxCustomEndpoints = 10
|
||||
maxEndpointNameLen = 50
|
||||
maxEndpointURLLen = 2048
|
||||
maxEndpointDescriptionLen = 200
|
||||
)
|
||||
|
||||
customEndpointsJSON := previousSettings.CustomEndpoints
|
||||
if req.CustomEndpoints != nil {
|
||||
endpoints := *req.CustomEndpoints
|
||||
if len(endpoints) > maxCustomEndpoints {
|
||||
response.BadRequest(c, "Too many custom endpoints (max 10)")
|
||||
return
|
||||
}
|
||||
for _, ep := range endpoints {
|
||||
if strings.TrimSpace(ep.Name) == "" {
|
||||
response.BadRequest(c, "Custom endpoint name is required")
|
||||
return
|
||||
}
|
||||
if len(ep.Name) > maxEndpointNameLen {
|
||||
response.BadRequest(c, "Custom endpoint name is too long (max 50 characters)")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(ep.Endpoint) == "" {
|
||||
response.BadRequest(c, "Custom endpoint URL is required")
|
||||
return
|
||||
}
|
||||
if len(ep.Endpoint) > maxEndpointURLLen {
|
||||
response.BadRequest(c, "Custom endpoint URL is too long (max 2048 characters)")
|
||||
return
|
||||
}
|
||||
if err := config.ValidateAbsoluteHTTPURL(strings.TrimSpace(ep.Endpoint)); err != nil {
|
||||
response.BadRequest(c, "Custom endpoint URL must be an absolute http(s) URL")
|
||||
return
|
||||
}
|
||||
if len(ep.Description) > maxEndpointDescriptionLen {
|
||||
response.BadRequest(c, "Custom endpoint description is too long (max 200 characters)")
|
||||
return
|
||||
}
|
||||
}
|
||||
endpointBytes, err := json.Marshal(endpoints)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "Failed to serialize custom endpoints")
|
||||
return
|
||||
}
|
||||
customEndpointsJSON = string(endpointBytes)
|
||||
}
|
||||
|
||||
// Ops metrics collector interval validation (seconds).
|
||||
if req.OpsMetricsIntervalSeconds != nil {
|
||||
v := *req.OpsMetricsIntervalSeconds
|
||||
@@ -495,6 +546,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
PurchaseSubscriptionURL: purchaseURL,
|
||||
SoraClientEnabled: req.SoraClientEnabled,
|
||||
CustomMenuItems: customMenuJSON,
|
||||
CustomEndpoints: customEndpointsJSON,
|
||||
DefaultConcurrency: req.DefaultConcurrency,
|
||||
DefaultBalance: req.DefaultBalance,
|
||||
DefaultSubscriptions: defaultSubscriptions,
|
||||
@@ -592,6 +644,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
|
||||
SoraClientEnabled: updatedSettings.SoraClientEnabled,
|
||||
CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
|
||||
CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
|
||||
DefaultConcurrency: updatedSettings.DefaultConcurrency,
|
||||
DefaultBalance: updatedSettings.DefaultBalance,
|
||||
DefaultSubscriptions: updatedDefaultSubscriptions,
|
||||
|
||||
@@ -15,6 +15,13 @@ type CustomMenuItem struct {
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// CustomEndpoint represents an admin-configured API endpoint for quick copy.
|
||||
type CustomEndpoint struct {
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// SystemSettings represents the admin settings API response payload.
|
||||
type SystemSettings struct {
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
@@ -56,6 +63,7 @@ type SystemSettings struct {
|
||||
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||
SoraClientEnabled bool `json:"sora_client_enabled"`
|
||||
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
|
||||
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
|
||||
|
||||
DefaultConcurrency int `json:"default_concurrency"`
|
||||
DefaultBalance float64 `json:"default_balance"`
|
||||
@@ -114,6 +122,7 @@ type PublicSettings struct {
|
||||
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
|
||||
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
|
||||
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
|
||||
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
|
||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||
SoraClientEnabled bool `json:"sora_client_enabled"`
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
@@ -218,3 +227,17 @@ func ParseUserVisibleMenuItems(raw string) []CustomMenuItem {
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// ParseCustomEndpoints parses a JSON string into a slice of CustomEndpoint.
|
||||
// Returns empty slice on empty/invalid input.
|
||||
func ParseCustomEndpoints(raw string) []CustomEndpoint {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || raw == "[]" {
|
||||
return []CustomEndpoint{}
|
||||
}
|
||||
var items []CustomEndpoint
|
||||
if err := json.Unmarshal([]byte(raw), &items); err != nil {
|
||||
return []CustomEndpoint{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
||||
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
CustomMenuItems: dto.ParseUserVisibleMenuItems(settings.CustomMenuItems),
|
||||
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
|
||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||
SoraClientEnabled: settings.SoraClientEnabled,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
|
||||
@@ -540,7 +540,8 @@ func TestAPIContracts(t *testing.T) {
|
||||
"max_claude_code_version": "",
|
||||
"allow_ungrouped_key_scheduling": false,
|
||||
"backend_mode_enabled": false,
|
||||
"custom_menu_items": []
|
||||
"custom_menu_items": [],
|
||||
"custom_endpoints": []
|
||||
}
|
||||
}`,
|
||||
},
|
||||
|
||||
@@ -119,6 +119,7 @@ const (
|
||||
SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示"购买订阅"页面入口
|
||||
SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // "购买订阅"页面 URL(作为 iframe src)
|
||||
SettingKeyCustomMenuItems = "custom_menu_items" // 自定义菜单项(JSON 数组)
|
||||
SettingKeyCustomEndpoints = "custom_endpoints" // 自定义端点列表(JSON 数组)
|
||||
|
||||
// 默认配置
|
||||
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量
|
||||
|
||||
@@ -150,6 +150,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
SettingKeyPurchaseSubscriptionURL,
|
||||
SettingKeySoraClientEnabled,
|
||||
SettingKeyCustomMenuItems,
|
||||
SettingKeyCustomEndpoints,
|
||||
SettingKeyLinuxDoConnectEnabled,
|
||||
SettingKeyBackendModeEnabled,
|
||||
}
|
||||
@@ -195,6 +196,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
||||
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
|
||||
SoraClientEnabled: settings[SettingKeySoraClientEnabled] == "true",
|
||||
CustomMenuItems: settings[SettingKeyCustomMenuItems],
|
||||
CustomEndpoints: settings[SettingKeyCustomEndpoints],
|
||||
LinuxDoOAuthEnabled: linuxDoEnabled,
|
||||
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
|
||||
}, nil
|
||||
@@ -247,6 +249,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
||||
PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"`
|
||||
SoraClientEnabled bool `json:"sora_client_enabled"`
|
||||
CustomMenuItems json.RawMessage `json:"custom_menu_items"`
|
||||
CustomEndpoints json.RawMessage `json:"custom_endpoints"`
|
||||
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
|
||||
BackendModeEnabled bool `json:"backend_mode_enabled"`
|
||||
Version string `json:"version,omitempty"`
|
||||
@@ -272,6 +275,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
|
||||
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
|
||||
SoraClientEnabled: settings.SoraClientEnabled,
|
||||
CustomMenuItems: filterUserVisibleMenuItems(settings.CustomMenuItems),
|
||||
CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints),
|
||||
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
|
||||
BackendModeEnabled: settings.BackendModeEnabled,
|
||||
Version: s.version,
|
||||
@@ -314,6 +318,18 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
|
||||
return result
|
||||
}
|
||||
|
||||
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
|
||||
func safeRawJSONArray(raw string) json.RawMessage {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return json.RawMessage("[]")
|
||||
}
|
||||
if json.Valid([]byte(raw)) {
|
||||
return json.RawMessage(raw)
|
||||
}
|
||||
return json.RawMessage("[]")
|
||||
}
|
||||
|
||||
// GetFrameSrcOrigins returns deduplicated http(s) origins from purchase_subscription_url
|
||||
// and all custom_menu_items URLs. Used by the router layer for CSP frame-src injection.
|
||||
func (s *SettingService) GetFrameSrcOrigins(ctx context.Context) ([]string, error) {
|
||||
@@ -454,6 +470,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
|
||||
updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
|
||||
updates[SettingKeySoraClientEnabled] = strconv.FormatBool(settings.SoraClientEnabled)
|
||||
updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems
|
||||
updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints
|
||||
|
||||
// 默认配置
|
||||
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
|
||||
@@ -740,6 +757,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
||||
SettingKeyPurchaseSubscriptionURL: "",
|
||||
SettingKeySoraClientEnabled: "false",
|
||||
SettingKeyCustomMenuItems: "[]",
|
||||
SettingKeyCustomEndpoints: "[]",
|
||||
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
|
||||
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
|
||||
SettingKeyDefaultSubscriptions: "[]",
|
||||
@@ -805,6 +823,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
|
||||
SoraClientEnabled: settings[SettingKeySoraClientEnabled] == "true",
|
||||
CustomMenuItems: settings[SettingKeyCustomMenuItems],
|
||||
CustomEndpoints: settings[SettingKeyCustomEndpoints],
|
||||
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ type SystemSettings struct {
|
||||
PurchaseSubscriptionURL string
|
||||
SoraClientEnabled bool
|
||||
CustomMenuItems string // JSON array of custom menu items
|
||||
CustomEndpoints string // JSON array of custom endpoints
|
||||
|
||||
DefaultConcurrency int
|
||||
DefaultBalance float64
|
||||
@@ -104,6 +105,7 @@ type PublicSettings struct {
|
||||
PurchaseSubscriptionURL string
|
||||
SoraClientEnabled bool
|
||||
CustomMenuItems string // JSON array of custom menu items
|
||||
CustomEndpoints string // JSON array of custom endpoints
|
||||
|
||||
LinuxDoOAuthEnabled bool
|
||||
BackendModeEnabled bool
|
||||
|
||||
Reference in New Issue
Block a user