Skip to content

Commit 995bee1

Browse files
committed
feat: 支持自定义端点配置与展示
1 parent bda7c39 commit 995bee1

16 files changed

Lines changed: 474 additions & 20 deletions

File tree

backend/internal/handler/admin/setting_handler.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
110110
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
111111
SoraClientEnabled: settings.SoraClientEnabled,
112112
CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems),
113+
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
113114
DefaultConcurrency: settings.DefaultConcurrency,
114115
DefaultBalance: settings.DefaultBalance,
115116
DefaultSubscriptions: defaultSubscriptions,
@@ -176,6 +177,7 @@ type UpdateSettingsRequest struct {
176177
PurchaseSubscriptionURL *string `json:"purchase_subscription_url"`
177178
SoraClientEnabled bool `json:"sora_client_enabled"`
178179
CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"`
180+
CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"`
179181

180182
// 默认配置
181183
DefaultConcurrency int `json:"default_concurrency"`
@@ -417,6 +419,55 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
417419
customMenuJSON = string(menuBytes)
418420
}
419421

422+
// 自定义端点验证
423+
const (
424+
maxCustomEndpoints = 10
425+
maxEndpointNameLen = 50
426+
maxEndpointURLLen = 2048
427+
maxEndpointDescriptionLen = 200
428+
)
429+
430+
customEndpointsJSON := previousSettings.CustomEndpoints
431+
if req.CustomEndpoints != nil {
432+
endpoints := *req.CustomEndpoints
433+
if len(endpoints) > maxCustomEndpoints {
434+
response.BadRequest(c, "Too many custom endpoints (max 10)")
435+
return
436+
}
437+
for _, ep := range endpoints {
438+
if strings.TrimSpace(ep.Name) == "" {
439+
response.BadRequest(c, "Custom endpoint name is required")
440+
return
441+
}
442+
if len(ep.Name) > maxEndpointNameLen {
443+
response.BadRequest(c, "Custom endpoint name is too long (max 50 characters)")
444+
return
445+
}
446+
if strings.TrimSpace(ep.Endpoint) == "" {
447+
response.BadRequest(c, "Custom endpoint URL is required")
448+
return
449+
}
450+
if len(ep.Endpoint) > maxEndpointURLLen {
451+
response.BadRequest(c, "Custom endpoint URL is too long (max 2048 characters)")
452+
return
453+
}
454+
if err := config.ValidateAbsoluteHTTPURL(strings.TrimSpace(ep.Endpoint)); err != nil {
455+
response.BadRequest(c, "Custom endpoint URL must be an absolute http(s) URL")
456+
return
457+
}
458+
if len(ep.Description) > maxEndpointDescriptionLen {
459+
response.BadRequest(c, "Custom endpoint description is too long (max 200 characters)")
460+
return
461+
}
462+
}
463+
endpointBytes, err := json.Marshal(endpoints)
464+
if err != nil {
465+
response.BadRequest(c, "Failed to serialize custom endpoints")
466+
return
467+
}
468+
customEndpointsJSON = string(endpointBytes)
469+
}
470+
420471
// Ops metrics collector interval validation (seconds).
421472
if req.OpsMetricsIntervalSeconds != nil {
422473
v := *req.OpsMetricsIntervalSeconds
@@ -495,6 +546,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
495546
PurchaseSubscriptionURL: purchaseURL,
496547
SoraClientEnabled: req.SoraClientEnabled,
497548
CustomMenuItems: customMenuJSON,
549+
CustomEndpoints: customEndpointsJSON,
498550
DefaultConcurrency: req.DefaultConcurrency,
499551
DefaultBalance: req.DefaultBalance,
500552
DefaultSubscriptions: defaultSubscriptions,
@@ -592,6 +644,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
592644
PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL,
593645
SoraClientEnabled: updatedSettings.SoraClientEnabled,
594646
CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems),
647+
CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints),
595648
DefaultConcurrency: updatedSettings.DefaultConcurrency,
596649
DefaultBalance: updatedSettings.DefaultBalance,
597650
DefaultSubscriptions: updatedDefaultSubscriptions,

backend/internal/handler/dto/settings.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ type CustomMenuItem struct {
1515
SortOrder int `json:"sort_order"`
1616
}
1717

18+
// CustomEndpoint represents an admin-configured API endpoint for quick copy.
19+
type CustomEndpoint struct {
20+
Name string `json:"name"`
21+
Endpoint string `json:"endpoint"`
22+
Description string `json:"description"`
23+
}
24+
1825
// SystemSettings represents the admin settings API response payload.
1926
type SystemSettings struct {
2027
RegistrationEnabled bool `json:"registration_enabled"`
@@ -56,6 +63,7 @@ type SystemSettings struct {
5663
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
5764
SoraClientEnabled bool `json:"sora_client_enabled"`
5865
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
66+
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
5967

6068
DefaultConcurrency int `json:"default_concurrency"`
6169
DefaultBalance float64 `json:"default_balance"`
@@ -114,6 +122,7 @@ type PublicSettings struct {
114122
PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"`
115123
PurchaseSubscriptionURL string `json:"purchase_subscription_url"`
116124
CustomMenuItems []CustomMenuItem `json:"custom_menu_items"`
125+
CustomEndpoints []CustomEndpoint `json:"custom_endpoints"`
117126
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
118127
SoraClientEnabled bool `json:"sora_client_enabled"`
119128
BackendModeEnabled bool `json:"backend_mode_enabled"`
@@ -218,3 +227,17 @@ func ParseUserVisibleMenuItems(raw string) []CustomMenuItem {
218227
}
219228
return filtered
220229
}
230+
231+
// ParseCustomEndpoints parses a JSON string into a slice of CustomEndpoint.
232+
// Returns empty slice on empty/invalid input.
233+
func ParseCustomEndpoints(raw string) []CustomEndpoint {
234+
raw = strings.TrimSpace(raw)
235+
if raw == "" || raw == "[]" {
236+
return []CustomEndpoint{}
237+
}
238+
var items []CustomEndpoint
239+
if err := json.Unmarshal([]byte(raw), &items); err != nil {
240+
return []CustomEndpoint{}
241+
}
242+
return items
243+
}

backend/internal/handler/setting_handler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
5252
PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled,
5353
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
5454
CustomMenuItems: dto.ParseUserVisibleMenuItems(settings.CustomMenuItems),
55+
CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints),
5556
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
5657
SoraClientEnabled: settings.SoraClientEnabled,
5758
BackendModeEnabled: settings.BackendModeEnabled,

backend/internal/server/api_contract_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,8 @@ func TestAPIContracts(t *testing.T) {
540540
"max_claude_code_version": "",
541541
"allow_ungrouped_key_scheduling": false,
542542
"backend_mode_enabled": false,
543-
"custom_menu_items": []
543+
"custom_menu_items": [],
544+
"custom_endpoints": []
544545
}
545546
}`,
546547
},

backend/internal/service/domain_constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const (
119119
SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示"购买订阅"页面入口
120120
SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // "购买订阅"页面 URL(作为 iframe src)
121121
SettingKeyCustomMenuItems = "custom_menu_items" // 自定义菜单项(JSON 数组)
122+
SettingKeyCustomEndpoints = "custom_endpoints" // 自定义端点列表(JSON 数组)
122123

123124
// 默认配置
124125
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量

backend/internal/service/setting_service.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
150150
SettingKeyPurchaseSubscriptionURL,
151151
SettingKeySoraClientEnabled,
152152
SettingKeyCustomMenuItems,
153+
SettingKeyCustomEndpoints,
153154
SettingKeyLinuxDoConnectEnabled,
154155
SettingKeyBackendModeEnabled,
155156
}
@@ -195,6 +196,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
195196
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
196197
SoraClientEnabled: settings[SettingKeySoraClientEnabled] == "true",
197198
CustomMenuItems: settings[SettingKeyCustomMenuItems],
199+
CustomEndpoints: settings[SettingKeyCustomEndpoints],
198200
LinuxDoOAuthEnabled: linuxDoEnabled,
199201
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
200202
}, nil
@@ -247,6 +249,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
247249
PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"`
248250
SoraClientEnabled bool `json:"sora_client_enabled"`
249251
CustomMenuItems json.RawMessage `json:"custom_menu_items"`
252+
CustomEndpoints json.RawMessage `json:"custom_endpoints"`
250253
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
251254
BackendModeEnabled bool `json:"backend_mode_enabled"`
252255
Version string `json:"version,omitempty"`
@@ -272,6 +275,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
272275
PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL,
273276
SoraClientEnabled: settings.SoraClientEnabled,
274277
CustomMenuItems: filterUserVisibleMenuItems(settings.CustomMenuItems),
278+
CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints),
275279
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
276280
BackendModeEnabled: settings.BackendModeEnabled,
277281
Version: s.version,
@@ -314,6 +318,18 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
314318
return result
315319
}
316320

321+
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
322+
func safeRawJSONArray(raw string) json.RawMessage {
323+
raw = strings.TrimSpace(raw)
324+
if raw == "" {
325+
return json.RawMessage("[]")
326+
}
327+
if json.Valid([]byte(raw)) {
328+
return json.RawMessage(raw)
329+
}
330+
return json.RawMessage("[]")
331+
}
332+
317333
// GetFrameSrcOrigins returns deduplicated http(s) origins from purchase_subscription_url
318334
// and all custom_menu_items URLs. Used by the router layer for CSP frame-src injection.
319335
func (s *SettingService) GetFrameSrcOrigins(ctx context.Context) ([]string, error) {
@@ -454,6 +470,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
454470
updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
455471
updates[SettingKeySoraClientEnabled] = strconv.FormatBool(settings.SoraClientEnabled)
456472
updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems
473+
updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints
457474

458475
// 默认配置
459476
updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
@@ -740,6 +757,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
740757
SettingKeyPurchaseSubscriptionURL: "",
741758
SettingKeySoraClientEnabled: "false",
742759
SettingKeyCustomMenuItems: "[]",
760+
SettingKeyCustomEndpoints: "[]",
743761
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
744762
SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
745763
SettingKeyDefaultSubscriptions: "[]",
@@ -805,6 +823,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
805823
PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
806824
SoraClientEnabled: settings[SettingKeySoraClientEnabled] == "true",
807825
CustomMenuItems: settings[SettingKeyCustomMenuItems],
826+
CustomEndpoints: settings[SettingKeyCustomEndpoints],
808827
BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
809828
}
810829

backend/internal/service/settings_view.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type SystemSettings struct {
4343
PurchaseSubscriptionURL string
4444
SoraClientEnabled bool
4545
CustomMenuItems string // JSON array of custom menu items
46+
CustomEndpoints string // JSON array of custom endpoints
4647

4748
DefaultConcurrency int
4849
DefaultBalance float64
@@ -104,6 +105,7 @@ type PublicSettings struct {
104105
PurchaseSubscriptionURL string
105106
SoraClientEnabled bool
106107
CustomMenuItems string // JSON array of custom menu items
108+
CustomEndpoints string // JSON array of custom endpoints
107109

108110
LinuxDoOAuthEnabled bool
109111
BackendModeEnabled bool

frontend/src/api/admin/settings.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { apiClient } from '../client'
7-
import type { CustomMenuItem } from '@/types'
7+
import type { CustomMenuItem, CustomEndpoint } from '@/types'
88

99
export interface DefaultSubscriptionSetting {
1010
group_id: number
@@ -43,6 +43,7 @@ export interface SystemSettings {
4343
sora_client_enabled: boolean
4444
backend_mode_enabled: boolean
4545
custom_menu_items: CustomMenuItem[]
46+
custom_endpoints: CustomEndpoint[]
4647
// SMTP settings
4748
smtp_host: string
4849
smtp_port: number
@@ -112,6 +113,7 @@ export interface UpdateSettingsRequest {
112113
sora_client_enabled?: boolean
113114
backend_mode_enabled?: boolean
114115
custom_menu_items?: CustomMenuItem[]
116+
custom_endpoints?: CustomEndpoint[]
115117
smtp_host?: string
116118
smtp_port?: number
117119
smtp_username?: string

0 commit comments

Comments
 (0)