Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
995bee14
Commit
995bee14
authored
Mar 24, 2026
by
shaw
Browse files
feat: 支持自定义端点配置与展示
parent
bda7c39e
Changes
16
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/setting_handler.go
View file @
995bee14
...
@@ -110,6 +110,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -110,6 +110,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
CustomMenuItems
:
dto
.
ParseCustomMenuItems
(
settings
.
CustomMenuItems
),
CustomMenuItems
:
dto
.
ParseCustomMenuItems
(
settings
.
CustomMenuItems
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
DefaultConcurrency
:
settings
.
DefaultConcurrency
,
DefaultConcurrency
:
settings
.
DefaultConcurrency
,
DefaultBalance
:
settings
.
DefaultBalance
,
DefaultBalance
:
settings
.
DefaultBalance
,
DefaultSubscriptions
:
defaultSubscriptions
,
DefaultSubscriptions
:
defaultSubscriptions
,
...
@@ -176,6 +177,7 @@ type UpdateSettingsRequest struct {
...
@@ -176,6 +177,7 @@ type UpdateSettingsRequest struct {
PurchaseSubscriptionURL
*
string
`json:"purchase_subscription_url"`
PurchaseSubscriptionURL
*
string
`json:"purchase_subscription_url"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
CustomMenuItems
*
[]
dto
.
CustomMenuItem
`json:"custom_menu_items"`
CustomMenuItems
*
[]
dto
.
CustomMenuItem
`json:"custom_menu_items"`
CustomEndpoints
*
[]
dto
.
CustomEndpoint
`json:"custom_endpoints"`
// 默认配置
// 默认配置
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultConcurrency
int
`json:"default_concurrency"`
...
@@ -417,6 +419,55 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -417,6 +419,55 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
customMenuJSON
=
string
(
menuBytes
)
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).
// Ops metrics collector interval validation (seconds).
if
req
.
OpsMetricsIntervalSeconds
!=
nil
{
if
req
.
OpsMetricsIntervalSeconds
!=
nil
{
v
:=
*
req
.
OpsMetricsIntervalSeconds
v
:=
*
req
.
OpsMetricsIntervalSeconds
...
@@ -495,6 +546,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -495,6 +546,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
PurchaseSubscriptionURL
:
purchaseURL
,
PurchaseSubscriptionURL
:
purchaseURL
,
SoraClientEnabled
:
req
.
SoraClientEnabled
,
SoraClientEnabled
:
req
.
SoraClientEnabled
,
CustomMenuItems
:
customMenuJSON
,
CustomMenuItems
:
customMenuJSON
,
CustomEndpoints
:
customEndpointsJSON
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultBalance
:
req
.
DefaultBalance
,
DefaultBalance
:
req
.
DefaultBalance
,
DefaultSubscriptions
:
defaultSubscriptions
,
DefaultSubscriptions
:
defaultSubscriptions
,
...
@@ -592,6 +644,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -592,6 +644,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
PurchaseSubscriptionURL
:
updatedSettings
.
PurchaseSubscriptionURL
,
PurchaseSubscriptionURL
:
updatedSettings
.
PurchaseSubscriptionURL
,
SoraClientEnabled
:
updatedSettings
.
SoraClientEnabled
,
SoraClientEnabled
:
updatedSettings
.
SoraClientEnabled
,
CustomMenuItems
:
dto
.
ParseCustomMenuItems
(
updatedSettings
.
CustomMenuItems
),
CustomMenuItems
:
dto
.
ParseCustomMenuItems
(
updatedSettings
.
CustomMenuItems
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
updatedSettings
.
CustomEndpoints
),
DefaultConcurrency
:
updatedSettings
.
DefaultConcurrency
,
DefaultConcurrency
:
updatedSettings
.
DefaultConcurrency
,
DefaultBalance
:
updatedSettings
.
DefaultBalance
,
DefaultBalance
:
updatedSettings
.
DefaultBalance
,
DefaultSubscriptions
:
updatedDefaultSubscriptions
,
DefaultSubscriptions
:
updatedDefaultSubscriptions
,
...
...
backend/internal/handler/dto/settings.go
View file @
995bee14
...
@@ -15,6 +15,13 @@ type CustomMenuItem struct {
...
@@ -15,6 +15,13 @@ type CustomMenuItem struct {
SortOrder
int
`json:"sort_order"`
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.
// SystemSettings represents the admin settings API response payload.
type
SystemSettings
struct
{
type
SystemSettings
struct
{
RegistrationEnabled
bool
`json:"registration_enabled"`
RegistrationEnabled
bool
`json:"registration_enabled"`
...
@@ -56,6 +63,7 @@ type SystemSettings struct {
...
@@ -56,6 +63,7 @@ type SystemSettings struct {
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultBalance
float64
`json:"default_balance"`
DefaultBalance
float64
`json:"default_balance"`
...
@@ -114,6 +122,7 @@ type PublicSettings struct {
...
@@ -114,6 +122,7 @@ type PublicSettings struct {
PurchaseSubscriptionEnabled
bool
`json:"purchase_subscription_enabled"`
PurchaseSubscriptionEnabled
bool
`json:"purchase_subscription_enabled"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
...
@@ -218,3 +227,17 @@ func ParseUserVisibleMenuItems(raw string) []CustomMenuItem {
...
@@ -218,3 +227,17 @@ func ParseUserVisibleMenuItems(raw string) []CustomMenuItem {
}
}
return
filtered
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
}
backend/internal/handler/setting_handler.go
View file @
995bee14
...
@@ -52,6 +52,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
...
@@ -52,6 +52,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
PurchaseSubscriptionEnabled
:
settings
.
PurchaseSubscriptionEnabled
,
PurchaseSubscriptionEnabled
:
settings
.
PurchaseSubscriptionEnabled
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
CustomMenuItems
:
dto
.
ParseUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
CustomMenuItems
:
dto
.
ParseUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
...
...
backend/internal/server/api_contract_test.go
View file @
995bee14
...
@@ -540,7 +540,8 @@ func TestAPIContracts(t *testing.T) {
...
@@ -540,7 +540,8 @@ func TestAPIContracts(t *testing.T) {
"max_claude_code_version": "",
"max_claude_code_version": "",
"allow_ungrouped_key_scheduling": false,
"allow_ungrouped_key_scheduling": false,
"backend_mode_enabled": false,
"backend_mode_enabled": false,
"custom_menu_items": []
"custom_menu_items": [],
"custom_endpoints": []
}
}
}`
,
}`
,
},
},
...
...
backend/internal/service/domain_constants.go
View file @
995bee14
...
@@ -119,6 +119,7 @@ const (
...
@@ -119,6 +119,7 @@ const (
SettingKeyPurchaseSubscriptionEnabled
=
"purchase_subscription_enabled"
// 是否展示"购买订阅"页面入口
SettingKeyPurchaseSubscriptionEnabled
=
"purchase_subscription_enabled"
// 是否展示"购买订阅"页面入口
SettingKeyPurchaseSubscriptionURL
=
"purchase_subscription_url"
// "购买订阅"页面 URL(作为 iframe src)
SettingKeyPurchaseSubscriptionURL
=
"purchase_subscription_url"
// "购买订阅"页面 URL(作为 iframe src)
SettingKeyCustomMenuItems
=
"custom_menu_items"
// 自定义菜单项(JSON 数组)
SettingKeyCustomMenuItems
=
"custom_menu_items"
// 自定义菜单项(JSON 数组)
SettingKeyCustomEndpoints
=
"custom_endpoints"
// 自定义端点列表(JSON 数组)
// 默认配置
// 默认配置
SettingKeyDefaultConcurrency
=
"default_concurrency"
// 新用户默认并发量
SettingKeyDefaultConcurrency
=
"default_concurrency"
// 新用户默认并发量
...
...
backend/internal/service/setting_service.go
View file @
995bee14
...
@@ -150,6 +150,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -150,6 +150,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyPurchaseSubscriptionURL
,
SettingKeyPurchaseSubscriptionURL
,
SettingKeySoraClientEnabled
,
SettingKeySoraClientEnabled
,
SettingKeyCustomMenuItems
,
SettingKeyCustomMenuItems
,
SettingKeyCustomEndpoints
,
SettingKeyLinuxDoConnectEnabled
,
SettingKeyLinuxDoConnectEnabled
,
SettingKeyBackendModeEnabled
,
SettingKeyBackendModeEnabled
,
}
}
...
@@ -195,6 +196,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
...
@@ -195,6 +196,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
CustomEndpoints
:
settings
[
SettingKeyCustomEndpoints
],
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
LinuxDoOAuthEnabled
:
linuxDoEnabled
,
BackendModeEnabled
:
settings
[
SettingKeyBackendModeEnabled
]
==
"true"
,
BackendModeEnabled
:
settings
[
SettingKeyBackendModeEnabled
]
==
"true"
,
},
nil
},
nil
...
@@ -247,6 +249,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
...
@@ -247,6 +249,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url,omitempty"`
PurchaseSubscriptionURL
string
`json:"purchase_subscription_url,omitempty"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
SoraClientEnabled
bool
`json:"sora_client_enabled"`
CustomMenuItems
json
.
RawMessage
`json:"custom_menu_items"`
CustomMenuItems
json
.
RawMessage
`json:"custom_menu_items"`
CustomEndpoints
json
.
RawMessage
`json:"custom_endpoints"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
BackendModeEnabled
bool
`json:"backend_mode_enabled"`
Version
string
`json:"version,omitempty"`
Version
string
`json:"version,omitempty"`
...
@@ -272,6 +275,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
...
@@ -272,6 +275,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
PurchaseSubscriptionURL
:
settings
.
PurchaseSubscriptionURL
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
SoraClientEnabled
:
settings
.
SoraClientEnabled
,
CustomMenuItems
:
filterUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
CustomMenuItems
:
filterUserVisibleMenuItems
(
settings
.
CustomMenuItems
),
CustomEndpoints
:
safeRawJSONArray
(
settings
.
CustomEndpoints
),
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
LinuxDoOAuthEnabled
:
settings
.
LinuxDoOAuthEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
BackendModeEnabled
:
settings
.
BackendModeEnabled
,
Version
:
s
.
version
,
Version
:
s
.
version
,
...
@@ -314,6 +318,18 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
...
@@ -314,6 +318,18 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
return
result
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
// 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.
// 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
)
{
func
(
s
*
SettingService
)
GetFrameSrcOrigins
(
ctx
context
.
Context
)
([]
string
,
error
)
{
...
@@ -454,6 +470,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
...
@@ -454,6 +470,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
updates
[
SettingKeyPurchaseSubscriptionURL
]
=
strings
.
TrimSpace
(
settings
.
PurchaseSubscriptionURL
)
updates
[
SettingKeyPurchaseSubscriptionURL
]
=
strings
.
TrimSpace
(
settings
.
PurchaseSubscriptionURL
)
updates
[
SettingKeySoraClientEnabled
]
=
strconv
.
FormatBool
(
settings
.
SoraClientEnabled
)
updates
[
SettingKeySoraClientEnabled
]
=
strconv
.
FormatBool
(
settings
.
SoraClientEnabled
)
updates
[
SettingKeyCustomMenuItems
]
=
settings
.
CustomMenuItems
updates
[
SettingKeyCustomMenuItems
]
=
settings
.
CustomMenuItems
updates
[
SettingKeyCustomEndpoints
]
=
settings
.
CustomEndpoints
// 默认配置
// 默认配置
updates
[
SettingKeyDefaultConcurrency
]
=
strconv
.
Itoa
(
settings
.
DefaultConcurrency
)
updates
[
SettingKeyDefaultConcurrency
]
=
strconv
.
Itoa
(
settings
.
DefaultConcurrency
)
...
@@ -740,6 +757,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
...
@@ -740,6 +757,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyPurchaseSubscriptionURL
:
""
,
SettingKeyPurchaseSubscriptionURL
:
""
,
SettingKeySoraClientEnabled
:
"false"
,
SettingKeySoraClientEnabled
:
"false"
,
SettingKeyCustomMenuItems
:
"[]"
,
SettingKeyCustomMenuItems
:
"[]"
,
SettingKeyCustomEndpoints
:
"[]"
,
SettingKeyDefaultConcurrency
:
strconv
.
Itoa
(
s
.
cfg
.
Default
.
UserConcurrency
),
SettingKeyDefaultConcurrency
:
strconv
.
Itoa
(
s
.
cfg
.
Default
.
UserConcurrency
),
SettingKeyDefaultBalance
:
strconv
.
FormatFloat
(
s
.
cfg
.
Default
.
UserBalance
,
'f'
,
8
,
64
),
SettingKeyDefaultBalance
:
strconv
.
FormatFloat
(
s
.
cfg
.
Default
.
UserBalance
,
'f'
,
8
,
64
),
SettingKeyDefaultSubscriptions
:
"[]"
,
SettingKeyDefaultSubscriptions
:
"[]"
,
...
@@ -805,6 +823,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
...
@@ -805,6 +823,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
PurchaseSubscriptionURL
:
strings
.
TrimSpace
(
settings
[
SettingKeyPurchaseSubscriptionURL
]),
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
SoraClientEnabled
:
settings
[
SettingKeySoraClientEnabled
]
==
"true"
,
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
CustomMenuItems
:
settings
[
SettingKeyCustomMenuItems
],
CustomEndpoints
:
settings
[
SettingKeyCustomEndpoints
],
BackendModeEnabled
:
settings
[
SettingKeyBackendModeEnabled
]
==
"true"
,
BackendModeEnabled
:
settings
[
SettingKeyBackendModeEnabled
]
==
"true"
,
}
}
...
...
backend/internal/service/settings_view.go
View file @
995bee14
...
@@ -43,6 +43,7 @@ type SystemSettings struct {
...
@@ -43,6 +43,7 @@ type SystemSettings struct {
PurchaseSubscriptionURL
string
PurchaseSubscriptionURL
string
SoraClientEnabled
bool
SoraClientEnabled
bool
CustomMenuItems
string
// JSON array of custom menu items
CustomMenuItems
string
// JSON array of custom menu items
CustomEndpoints
string
// JSON array of custom endpoints
DefaultConcurrency
int
DefaultConcurrency
int
DefaultBalance
float64
DefaultBalance
float64
...
@@ -104,6 +105,7 @@ type PublicSettings struct {
...
@@ -104,6 +105,7 @@ type PublicSettings struct {
PurchaseSubscriptionURL
string
PurchaseSubscriptionURL
string
SoraClientEnabled
bool
SoraClientEnabled
bool
CustomMenuItems
string
// JSON array of custom menu items
CustomMenuItems
string
// JSON array of custom menu items
CustomEndpoints
string
// JSON array of custom endpoints
LinuxDoOAuthEnabled
bool
LinuxDoOAuthEnabled
bool
BackendModeEnabled
bool
BackendModeEnabled
bool
...
...
frontend/src/api/admin/settings.ts
View file @
995bee14
...
@@ -4,7 +4,7 @@
...
@@ -4,7 +4,7 @@
*/
*/
import
{
apiClient
}
from
'
../client
'
import
{
apiClient
}
from
'
../client
'
import
type
{
CustomMenuItem
}
from
'
@/types
'
import
type
{
CustomMenuItem
,
CustomEndpoint
}
from
'
@/types
'
export
interface
DefaultSubscriptionSetting
{
export
interface
DefaultSubscriptionSetting
{
group_id
:
number
group_id
:
number
...
@@ -43,6 +43,7 @@ export interface SystemSettings {
...
@@ -43,6 +43,7 @@ export interface SystemSettings {
sora_client_enabled
:
boolean
sora_client_enabled
:
boolean
backend_mode_enabled
:
boolean
backend_mode_enabled
:
boolean
custom_menu_items
:
CustomMenuItem
[]
custom_menu_items
:
CustomMenuItem
[]
custom_endpoints
:
CustomEndpoint
[]
// SMTP settings
// SMTP settings
smtp_host
:
string
smtp_host
:
string
smtp_port
:
number
smtp_port
:
number
...
@@ -112,6 +113,7 @@ export interface UpdateSettingsRequest {
...
@@ -112,6 +113,7 @@ export interface UpdateSettingsRequest {
sora_client_enabled
?:
boolean
sora_client_enabled
?:
boolean
backend_mode_enabled
?:
boolean
backend_mode_enabled
?:
boolean
custom_menu_items
?:
CustomMenuItem
[]
custom_menu_items
?:
CustomMenuItem
[]
custom_endpoints
?:
CustomEndpoint
[]
smtp_host
?:
string
smtp_host
?:
string
smtp_port
?:
number
smtp_port
?:
number
smtp_username
?:
string
smtp_username
?:
string
...
...
frontend/src/components/keys/EndpointPopover.vue
0 → 100644
View file @
995bee14
<
script
setup
lang=
"ts"
>
import
{
computed
,
onBeforeUnmount
,
ref
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
import
type
{
CustomEndpoint
}
from
'
@/types
'
const
props
=
defineProps
<
{
apiBaseUrl
:
string
customEndpoints
:
CustomEndpoint
[]
}
>
()
const
{
t
}
=
useI18n
()
const
{
copyToClipboard
}
=
useClipboard
()
const
copiedEndpoint
=
ref
<
string
|
null
>
(
null
)
let
copiedResetTimer
:
number
|
undefined
const
allEndpoints
=
computed
(()
=>
{
const
items
:
Array
<
{
name
:
string
;
endpoint
:
string
;
description
:
string
;
isDefault
:
boolean
}
>
=
[]
if
(
props
.
apiBaseUrl
)
{
items
.
push
({
name
:
t
(
'
keys.endpoints.title
'
),
endpoint
:
props
.
apiBaseUrl
,
description
:
''
,
isDefault
:
true
,
})
}
for
(
const
ep
of
props
.
customEndpoints
)
{
items
.
push
({
...
ep
,
isDefault
:
false
})
}
return
items
})
async
function
copy
(
url
:
string
)
{
const
success
=
await
copyToClipboard
(
url
,
t
(
'
keys.endpoints.copied
'
))
if
(
!
success
)
return
copiedEndpoint
.
value
=
url
if
(
copiedResetTimer
!==
undefined
)
{
window
.
clearTimeout
(
copiedResetTimer
)
}
copiedResetTimer
=
window
.
setTimeout
(()
=>
{
if
(
copiedEndpoint
.
value
===
url
)
{
copiedEndpoint
.
value
=
null
}
},
1800
)
}
function
tooltipHint
(
endpoint
:
string
):
string
{
return
copiedEndpoint
.
value
===
endpoint
?
t
(
'
keys.endpoints.copiedHint
'
)
:
t
(
'
keys.endpoints.clickToCopy
'
)
}
function
speedTestUrl
(
endpoint
:
string
):
string
{
return
`https://www.tcptest.cn/http/
${
encodeURIComponent
(
endpoint
)}
`
}
onBeforeUnmount
(()
=>
{
if
(
copiedResetTimer
!==
undefined
)
{
window
.
clearTimeout
(
copiedResetTimer
)
}
})
</
script
>
<
template
>
<div
v-if=
"allEndpoints.length > 0"
class=
"flex flex-wrap gap-2"
>
<div
v-for=
"(item, index) in allEndpoints"
:key=
"index"
class=
"flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs transition-colors hover:border-primary-200 dark:border-dark-600 dark:bg-dark-800 dark:hover:border-primary-700"
>
<span
class=
"font-medium text-gray-600 dark:text-gray-300"
>
{{
item
.
name
}}
</span>
<span
v-if=
"item.isDefault"
class=
"rounded bg-primary-50 px-1 py-px text-[10px] font-medium leading-tight text-primary-600 dark:bg-primary-900/30 dark:text-primary-400"
>
{{
t
(
'
keys.endpoints.default
'
)
}}
</span>
<span
class=
"text-gray-300 dark:text-dark-500"
>
|
</span>
<div
class=
"group/endpoint relative flex items-center gap-1.5"
>
<div
class=
"pointer-events-none absolute bottom-full left-1/2 z-20 mb-2 w-max max-w-[24rem] -translate-x-1/2 translate-y-1 rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-left opacity-0 shadow-[0_14px_36px_-20px_rgba(15,23,42,0.35)] ring-1 ring-slate-200/80 transition-all duration-150 group-hover/endpoint:translate-y-0 group-hover/endpoint:opacity-100 group-focus-within/endpoint:translate-y-0 group-focus-within/endpoint:opacity-100 dark:border-slate-700 dark:bg-slate-900 dark:ring-slate-700/70"
>
<p
v-if=
"item.description"
class=
"max-w-[24rem] break-words text-xs leading-5 text-slate-600 dark:text-slate-200"
>
{{
item
.
description
}}
</p>
<p
class=
"flex items-center gap-1.5 text-[11px] leading-4 text-primary-600 dark:text-primary-300"
:class=
"item.description ? 'mt-1.5' : ''"
>
<span
class=
"h-1.5 w-1.5 rounded-full bg-primary-500 dark:bg-primary-300"
></span>
{{
tooltipHint
(
item
.
endpoint
)
}}
</p>
<div
class=
"absolute left-1/2 top-full h-3 w-3 -translate-x-1/2 -translate-y-1/2 rotate-45 border-b border-r border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
></div>
</div>
<code
class=
"cursor-pointer font-mono text-gray-500 decoration-gray-400 decoration-dashed underline-offset-2 hover:text-primary-600 hover:underline focus:text-primary-600 focus:underline focus:outline-none dark:text-gray-400 dark:decoration-gray-500 dark:hover:text-primary-400 dark:focus:text-primary-400"
role=
"button"
tabindex=
"0"
@
click=
"copy(item.endpoint)"
@
keydown.enter.prevent=
"copy(item.endpoint)"
@
keydown.space.prevent=
"copy(item.endpoint)"
>
{{
item
.
endpoint
}}
</code>
<button
type=
"button"
class=
"rounded p-0.5 transition-colors"
:class=
"copiedEndpoint === item.endpoint
? 'text-emerald-500 dark:text-emerald-400'
: 'text-gray-400 hover:text-primary-500 dark:text-gray-500 dark:hover:text-primary-400'"
:aria-label=
"tooltipHint(item.endpoint)"
@
click=
"copy(item.endpoint)"
>
<svg
v-if=
"copiedEndpoint === item.endpoint"
class=
"h-3 w-3"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2.2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M5 13l4 4L19 7"
/>
</svg>
<svg
v-else
class=
"h-3 w-3"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
<a
:href=
"speedTestUrl(item.endpoint)"
target=
"_blank"
rel=
"noopener noreferrer"
class=
"rounded p-0.5 text-gray-400 transition-colors hover:text-amber-500 dark:text-gray-500 dark:hover:text-amber-400"
:title=
"t('keys.endpoints.speedTest')"
>
<svg
class=
"h-3 w-3"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</a>
</div>
</div>
</div>
</
template
>
frontend/src/components/keys/__tests__/EndpointPopover.spec.ts
0 → 100644
View file @
995bee14
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
{
flushPromises
,
mount
}
from
'
@vue/test-utils
'
const
copyToClipboard
=
vi
.
fn
().
mockResolvedValue
(
true
)
const
messages
:
Record
<
string
,
string
>
=
{
'
keys.endpoints.title
'
:
'
API 端点
'
,
'
keys.endpoints.default
'
:
'
默认
'
,
'
keys.endpoints.copied
'
:
'
已复制
'
,
'
keys.endpoints.copiedHint
'
:
'
已复制到剪贴板
'
,
'
keys.endpoints.clickToCopy
'
:
'
点击可复制此端点
'
,
'
keys.endpoints.speedTest
'
:
'
测速
'
,
}
vi
.
mock
(
'
vue-i18n
'
,
()
=>
({
useI18n
:
()
=>
({
t
:
(
key
:
string
)
=>
messages
[
key
]
??
key
,
}),
}))
vi
.
mock
(
'
@/composables/useClipboard
'
,
()
=>
({
useClipboard
:
()
=>
({
copyToClipboard
,
}),
}))
import
EndpointPopover
from
'
../EndpointPopover.vue
'
describe
(
'
EndpointPopover
'
,
()
=>
{
beforeEach
(()
=>
{
vi
.
clearAllMocks
()
})
it
(
'
将说明提示渲染到 URL 上方而不是旧的 title 图标上
'
,
()
=>
{
const
wrapper
=
mount
(
EndpointPopover
,
{
props
:
{
apiBaseUrl
:
'
https://default.example.com/v1
'
,
customEndpoints
:
[
{
name
:
'
备用线路
'
,
endpoint
:
'
https://backup.example.com/v1
'
,
description
:
'
自定义说明
'
,
},
],
},
})
expect
(
wrapper
.
text
()).
toContain
(
'
自定义说明
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
点击可复制此端点
'
)
expect
(
wrapper
.
find
(
'
[role="button"]
'
).
attributes
(
'
title
'
)).
toBeUndefined
()
expect
(
wrapper
.
find
(
'
[title="自定义说明"]
'
).
exists
()).
toBe
(
false
)
})
it
(
'
点击 URL 后会复制并切换为已复制提示
'
,
async
()
=>
{
const
wrapper
=
mount
(
EndpointPopover
,
{
props
:
{
apiBaseUrl
:
'
https://default.example.com/v1
'
,
customEndpoints
:
[],
},
})
await
wrapper
.
find
(
'
[role="button"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
expect
(
copyToClipboard
).
toHaveBeenCalledWith
(
'
https://default.example.com/v1
'
,
'
已复制
'
)
expect
(
wrapper
.
text
()).
toContain
(
'
已复制到剪贴板
'
)
expect
(
wrapper
.
find
(
'
button[aria-label="已复制到剪贴板"]
'
).
exists
()).
toBe
(
true
)
})
})
frontend/src/i18n/locales/en.ts
View file @
995bee14
...
@@ -533,6 +533,14 @@ export default {
...
@@ -533,6 +533,14 @@ export default {
title
:
'
API Keys
'
,
title
:
'
API Keys
'
,
description
:
'
Manage your API keys and access tokens
'
,
description
:
'
Manage your API keys and access tokens
'
,
searchPlaceholder
:
'
Search name or key...
'
,
searchPlaceholder
:
'
Search name or key...
'
,
endpoints
:
{
title
:
'
API Endpoints
'
,
default
:
'
Default
'
,
copied
:
'
Copied
'
,
copiedHint
:
'
Copied to clipboard
'
,
clickToCopy
:
'
Click to copy this endpoint
'
,
speedTest
:
'
Speed Test
'
,
},
allGroups
:
'
All Groups
'
,
allGroups
:
'
All Groups
'
,
allStatus
:
'
All Status
'
,
allStatus
:
'
All Status
'
,
createKey
:
'
Create API Key
'
,
createKey
:
'
Create API Key
'
,
...
@@ -4162,6 +4170,18 @@ export default {
...
@@ -4162,6 +4170,18 @@ export default {
apiBaseUrlPlaceholder
:
'
https://api.example.com
'
,
apiBaseUrlPlaceholder
:
'
https://api.example.com
'
,
apiBaseUrlHint
:
apiBaseUrlHint
:
'
Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.
'
,
'
Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.
'
,
customEndpoints
:
{
title
:
'
Custom Endpoints
'
,
description
:
'
Add additional API endpoint URLs for users to quickly copy on the API Keys page
'
,
itemLabel
:
'
Endpoint #{n}
'
,
name
:
'
Name
'
,
namePlaceholder
:
'
e.g., OpenAI Compatible
'
,
endpointUrl
:
'
Endpoint URL
'
,
endpointUrlPlaceholder
:
'
https://api2.example.com
'
,
descriptionLabel
:
'
Description
'
,
descriptionPlaceholder
:
'
e.g., Supports OpenAI format requests
'
,
add
:
'
Add Endpoint
'
,
},
contactInfo
:
'
Contact Info
'
,
contactInfo
:
'
Contact Info
'
,
contactInfoPlaceholder
:
'
e.g., QQ: 123456789
'
,
contactInfoPlaceholder
:
'
e.g., QQ: 123456789
'
,
contactInfoHint
:
'
Customer support contact info, displayed on redeem page, profile, etc.
'
,
contactInfoHint
:
'
Customer support contact info, displayed on redeem page, profile, etc.
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
995bee14
...
@@ -533,6 +533,14 @@ export default {
...
@@ -533,6 +533,14 @@ export default {
title
:
'
API 密钥
'
,
title
:
'
API 密钥
'
,
description
:
'
管理您的 API 密钥和访问令牌
'
,
description
:
'
管理您的 API 密钥和访问令牌
'
,
searchPlaceholder
:
'
搜索名称或Key...
'
,
searchPlaceholder
:
'
搜索名称或Key...
'
,
endpoints
:
{
title
:
'
API 端点
'
,
default
:
'
默认
'
,
copied
:
'
已复制
'
,
copiedHint
:
'
已复制到剪贴板
'
,
clickToCopy
:
'
点击可复制此端点
'
,
speedTest
:
'
测速
'
,
},
allGroups
:
'
全部分组
'
,
allGroups
:
'
全部分组
'
,
allStatus
:
'
全部状态
'
,
allStatus
:
'
全部状态
'
,
createKey
:
'
创建密钥
'
,
createKey
:
'
创建密钥
'
,
...
@@ -4324,6 +4332,18 @@ export default {
...
@@ -4324,6 +4332,18 @@ export default {
apiBaseUrl
:
'
API 端点地址
'
,
apiBaseUrl
:
'
API 端点地址
'
,
apiBaseUrlHint
:
'
用于"使用密钥"和"导入到 CC Switch"功能,留空则使用当前站点地址
'
,
apiBaseUrlHint
:
'
用于"使用密钥"和"导入到 CC Switch"功能,留空则使用当前站点地址
'
,
apiBaseUrlPlaceholder
:
'
https://api.example.com
'
,
apiBaseUrlPlaceholder
:
'
https://api.example.com
'
,
customEndpoints
:
{
title
:
'
自定义端点
'
,
description
:
'
添加额外的 API 端点地址,用户可在「API Keys」页面快速复制
'
,
itemLabel
:
'
端点 #{n}
'
,
name
:
'
名称
'
,
namePlaceholder
:
'
如:OpenAI Compatible
'
,
endpointUrl
:
'
端点地址
'
,
endpointUrlPlaceholder
:
'
https://api2.example.com
'
,
descriptionLabel
:
'
介绍
'
,
descriptionPlaceholder
:
'
如:支持 OpenAI 格式请求
'
,
add
:
'
添加端点
'
,
},
contactInfo
:
'
客服联系方式
'
,
contactInfo
:
'
客服联系方式
'
,
contactInfoPlaceholder
:
'
例如:QQ: 123456789
'
,
contactInfoPlaceholder
:
'
例如:QQ: 123456789
'
,
contactInfoHint
:
'
填写客服联系方式,将展示在兑换页面、个人资料等位置
'
,
contactInfoHint
:
'
填写客服联系方式,将展示在兑换页面、个人资料等位置
'
,
...
...
frontend/src/stores/app.ts
View file @
995bee14
...
@@ -330,6 +330,7 @@ export const useAppStore = defineStore('app', () => {
...
@@ -330,6 +330,7 @@ export const useAppStore = defineStore('app', () => {
purchase_subscription_enabled
:
false
,
purchase_subscription_enabled
:
false
,
purchase_subscription_url
:
''
,
purchase_subscription_url
:
''
,
custom_menu_items
:
[],
custom_menu_items
:
[],
custom_endpoints
:
[],
linuxdo_oauth_enabled
:
false
,
linuxdo_oauth_enabled
:
false
,
sora_client_enabled
:
false
,
sora_client_enabled
:
false
,
backend_mode_enabled
:
false
,
backend_mode_enabled
:
false
,
...
...
frontend/src/types/index.ts
View file @
995bee14
...
@@ -84,6 +84,12 @@ export interface CustomMenuItem {
...
@@ -84,6 +84,12 @@ export interface CustomMenuItem {
sort_order
:
number
sort_order
:
number
}
}
export
interface
CustomEndpoint
{
name
:
string
endpoint
:
string
description
:
string
}
export
interface
PublicSettings
{
export
interface
PublicSettings
{
registration_enabled
:
boolean
registration_enabled
:
boolean
email_verify_enabled
:
boolean
email_verify_enabled
:
boolean
...
@@ -104,6 +110,7 @@ export interface PublicSettings {
...
@@ -104,6 +110,7 @@ export interface PublicSettings {
purchase_subscription_enabled
:
boolean
purchase_subscription_enabled
:
boolean
purchase_subscription_url
:
string
purchase_subscription_url
:
string
custom_menu_items
:
CustomMenuItem
[]
custom_menu_items
:
CustomMenuItem
[]
custom_endpoints
:
CustomEndpoint
[]
linuxdo_oauth_enabled
:
boolean
linuxdo_oauth_enabled
:
boolean
sora_client_enabled
:
boolean
sora_client_enabled
:
boolean
backend_mode_enabled
:
boolean
backend_mode_enabled
:
boolean
...
...
frontend/src/views/admin/SettingsView.vue
View file @
995bee14
...
@@ -1248,6 +1248,81 @@
...
@@ -1248,6 +1248,81 @@
<
/p
>
<
/p
>
<
/div
>
<
/div
>
<!--
Custom
Endpoints
-->
<
div
>
<
label
class
=
"
mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300
"
>
{{
t
(
'
admin.settings.site.customEndpoints.title
'
)
}}
<
/label
>
<
p
class
=
"
mb-3 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.settings.site.customEndpoints.description
'
)
}}
<
/p
>
<
div
class
=
"
space-y-3
"
>
<
div
v
-
for
=
"
(ep, index) in form.custom_endpoints
"
:
key
=
"
index
"
class
=
"
rounded-lg border border-gray-200 p-4 dark:border-dark-600
"
>
<
div
class
=
"
mb-3 flex items-center justify-between
"
>
<
span
class
=
"
text-sm font-medium text-gray-700 dark:text-gray-300
"
>
{{
t
(
'
admin.settings.site.customEndpoints.itemLabel
'
,
{
n
:
index
+
1
}
)
}}
<
/span
>
<
button
type
=
"
button
"
class
=
"
rounded p-1 text-red-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20
"
@
click
=
"
removeEndpoint(index)
"
>
<
svg
class
=
"
h-4 w-4
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
2
"
><
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
d
=
"
M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16
"
/><
/svg
>
<
/button
>
<
/div
>
<
div
class
=
"
grid grid-cols-1 gap-3 sm:grid-cols-2
"
>
<
div
>
<
label
class
=
"
mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400
"
>
{{
t
(
'
admin.settings.site.customEndpoints.name
'
)
}}
<
/label
>
<
input
v
-
model
=
"
ep.name
"
type
=
"
text
"
class
=
"
input text-sm
"
:
placeholder
=
"
t('admin.settings.site.customEndpoints.namePlaceholder')
"
/>
<
/div
>
<
div
>
<
label
class
=
"
mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400
"
>
{{
t
(
'
admin.settings.site.customEndpoints.endpointUrl
'
)
}}
<
/label
>
<
input
v
-
model
=
"
ep.endpoint
"
type
=
"
url
"
class
=
"
input font-mono text-sm
"
:
placeholder
=
"
t('admin.settings.site.customEndpoints.endpointUrlPlaceholder')
"
/>
<
/div
>
<
div
class
=
"
sm:col-span-2
"
>
<
label
class
=
"
mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400
"
>
{{
t
(
'
admin.settings.site.customEndpoints.descriptionLabel
'
)
}}
<
/label
>
<
input
v
-
model
=
"
ep.description
"
type
=
"
text
"
class
=
"
input text-sm
"
:
placeholder
=
"
t('admin.settings.site.customEndpoints.descriptionPlaceholder')
"
/>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
button
type
=
"
button
"
class
=
"
mt-3 flex w-full items-center justify-center gap-2 rounded-lg border-2 border-dashed border-gray-300 px-4 py-2.5 text-sm text-gray-500 transition-colors hover:border-primary-400 hover:text-primary-600 dark:border-dark-600 dark:text-gray-400 dark:hover:border-primary-500 dark:hover:text-primary-400
"
@
click
=
"
addEndpoint
"
>
<
svg
class
=
"
h-4 w-4
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
2
"
><
path
stroke
-
linecap
=
"
round
"
stroke
-
linejoin
=
"
round
"
d
=
"
M12 4v16m8-8H4
"
/><
/svg
>
{{
t
(
'
admin.settings.site.customEndpoints.add
'
)
}}
<
/button
>
<
/div
>
<!--
Contact
Info
-->
<!--
Contact
Info
-->
<
div
>
<
div
>
<
label
class
=
"
mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300
"
>
<
label
class
=
"
mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300
"
>
...
@@ -1945,6 +2020,7 @@ const form = reactive<SettingsForm>({
...
@@ -1945,6 +2020,7 @@ const form = reactive<SettingsForm>({
purchase_subscription_url
:
''
,
purchase_subscription_url
:
''
,
sora_client_enabled
:
false
,
sora_client_enabled
:
false
,
custom_menu_items
:
[]
as
Array
<
{
id
:
string
;
label
:
string
;
icon_svg
:
string
;
url
:
string
;
visibility
:
'
user
'
|
'
admin
'
;
sort_order
:
number
}
>
,
custom_menu_items
:
[]
as
Array
<
{
id
:
string
;
label
:
string
;
icon_svg
:
string
;
url
:
string
;
visibility
:
'
user
'
|
'
admin
'
;
sort_order
:
number
}
>
,
custom_endpoints
:
[]
as
Array
<
{
name
:
string
;
endpoint
:
string
;
description
:
string
}
>
,
frontend_url
:
''
,
frontend_url
:
''
,
smtp_host
:
''
,
smtp_host
:
''
,
smtp_port
:
587
,
smtp_port
:
587
,
...
@@ -2114,6 +2190,15 @@ function moveMenuItem(index: number, direction: -1 | 1) {
...
@@ -2114,6 +2190,15 @@ function moveMenuItem(index: number, direction: -1 | 1) {
}
)
}
)
}
}
// Custom endpoint management
function
addEndpoint
()
{
form
.
custom_endpoints
.
push
({
name
:
''
,
endpoint
:
''
,
description
:
''
}
)
}
function
removeEndpoint
(
index
:
number
)
{
form
.
custom_endpoints
.
splice
(
index
,
1
)
}
async
function
loadSettings
()
{
async
function
loadSettings
()
{
loading
.
value
=
true
loading
.
value
=
true
try
{
try
{
...
@@ -2253,6 +2338,7 @@ async function saveSettings() {
...
@@ -2253,6 +2338,7 @@ async function saveSettings() {
purchase_subscription_url
:
form
.
purchase_subscription_url
,
purchase_subscription_url
:
form
.
purchase_subscription_url
,
sora_client_enabled
:
form
.
sora_client_enabled
,
sora_client_enabled
:
form
.
sora_client_enabled
,
custom_menu_items
:
form
.
custom_menu_items
,
custom_menu_items
:
form
.
custom_menu_items
,
custom_endpoints
:
form
.
custom_endpoints
,
frontend_url
:
form
.
frontend_url
,
frontend_url
:
form
.
frontend_url
,
smtp_host
:
form
.
smtp_host
,
smtp_host
:
form
.
smtp_host
,
smtp_port
:
form
.
smtp_port
,
smtp_port
:
form
.
smtp_port
,
...
...
frontend/src/views/user/KeysView.vue
View file @
995bee14
...
@@ -2,24 +2,31 @@
...
@@ -2,24 +2,31 @@
<AppLayout>
<AppLayout>
<TablePageLayout>
<TablePageLayout>
<template
#filters
>
<template
#filters
>
<div
class=
"flex flex-wrap items-center gap-3"
>
<div
class=
"flex flex-col gap-3"
>
<SearchInput
<div
class=
"flex flex-wrap items-center gap-3"
>
v-model=
"filterSearch"
<SearchInput
:placeholder=
"t('keys.searchPlaceholder')"
v-model=
"filterSearch"
class=
"w-full sm:w-64"
:placeholder=
"t('keys.searchPlaceholder')"
@
search=
"onFilterChange"
class=
"w-full sm:w-64"
/>
@
search=
"onFilterChange"
<Select
/>
:model-value=
"filterGroupId"
<Select
class=
"w-40"
:model-value=
"filterGroupId"
:options=
"groupFilterOptions"
class=
"w-40"
@
update:model-value=
"onGroupFilterChange"
:options=
"groupFilterOptions"
/>
@
update:model-value=
"onGroupFilterChange"
<Select
/>
:model-value=
"filterStatus"
<Select
class=
"w-40"
:model-value=
"filterStatus"
:options=
"statusFilterOptions"
class=
"w-40"
@
update:model-value=
"onStatusFilterChange"
:options=
"statusFilterOptions"
@
update:model-value=
"onStatusFilterChange"
/>
</div>
<EndpointPopover
v-if=
"publicSettings?.api_base_url || (publicSettings?.custom_endpoints?.length ?? 0) > 0"
:api-base-url=
"publicSettings?.api_base_url || ''"
:custom-endpoints=
"publicSettings?.custom_endpoints || []"
/>
/>
</div>
</div>
</
template
>
</
template
>
...
@@ -1050,6 +1057,7 @@ import TablePageLayout from '@/components/layout/TablePageLayout.vue'
...
@@ -1050,6 +1057,7 @@ import TablePageLayout from '@/components/layout/TablePageLayout.vue'
import
SearchInput
from
'
@/components/common/SearchInput.vue
'
import
SearchInput
from
'
@/components/common/SearchInput.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
UseKeyModal
from
'
@/components/keys/UseKeyModal.vue
'
import
UseKeyModal
from
'
@/components/keys/UseKeyModal.vue
'
import
EndpointPopover
from
'
@/components/keys/EndpointPopover.vue
'
import
GroupBadge
from
'
@/components/common/GroupBadge.vue
'
import
GroupBadge
from
'
@/components/common/GroupBadge.vue
'
import
GroupOptionItem
from
'
@/components/common/GroupOptionItem.vue
'
import
GroupOptionItem
from
'
@/components/common/GroupOptionItem.vue
'
import
type
{
ApiKey
,
Group
,
PublicSettings
,
SubscriptionType
,
GroupPlatform
}
from
'
@/types
'
import
type
{
ApiKey
,
Group
,
PublicSettings
,
SubscriptionType
,
GroupPlatform
}
from
'
@/types
'
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment