@@ -33,7 +33,7 @@ func (h *Handler) AdminChannels(c *gin.Context) {
masked = "****"
}
out = append ( out , gin . H {
"id" : ch . ID , "name" : ch . Name , "provider" : ch . Provider , "formats" : ch . FormatsEffective ( ) , "homepage" : ch . Homepage , " base_url": ch . BaseURL ,
"id" : ch . ID , "name" : ch . Name , "provider" : ch . Provider , "formats" : ch . FormatsEffective ( ) , "base_url" : ch . BaseURL ,
"api_key_masked" : masked , "weight" : ch . Weight , "priority" : ch . Priority ,
"timeout_ms" : ch . TimeoutMS , "max_concurrency" : ch . MaxConcurrency ,
"health_status" : ch . HealthStatus , "enabled" : ch . Enabled ,
@@ -47,8 +47,7 @@ type channelBody struct {
Name string ` json:"name" binding:"required,min=1,max=64" `
Provider string ` json:"provider" ` // 可选:为空时按 formats 推断(兼容旧数据)
Formats [ ] string ` json:"formats" ` // 原生支持的协议 chat|responses|messages(主配置)
Homepage string ` json:"homepage" `
BaseURL string ` json:"base_url" binding:"required" `
BaseURL string ` json:"base_url" ` // 可选:为空时按供应商默认(openai/anthropic)
APIKey string ` json:"api_key" `
Weight * int ` json:"weight" `
Priority * int ` json:"priority" `
@@ -57,6 +56,23 @@ type channelBody struct {
Enabled * bool ` json:"enabled" `
}
// resolveBaseURL 渠道 base_url:留空按供应商默认;兼容用户填完整地址(含 /v1)。
func resolveBaseURL ( provider , raw string ) ( string , error ) {
base := strings . TrimRight ( raw , "/" )
if base == "" {
switch provider {
case store . ChannelProviderOpenAI :
base = "https://api.openai.com"
case store . ChannelProviderAnthropic :
base = "https://api.anthropic.com"
}
}
if base == "" {
return "" , errors . New ( "base_url required for compatible channels" )
}
return strings . TrimSuffix ( base , "/v1" ) , nil
}
func validateProvider ( p string ) bool {
return p == store . ChannelProviderOpenAI || p == store . ChannelProviderAnthropic || p == store . ChannelProviderCompatible
}
@@ -137,13 +153,18 @@ func (h *Handler) AdminCreateChannel(c *gin.Context) {
resp . Fail ( c , http . StatusBadRequest , err . Error ( ) )
return
}
baseURL , err := resolveBaseURL ( req . Provider , req . BaseURL )
if err != nil {
resp . Fail ( c , http . StatusBadRequest , err . Error ( ) )
return
}
enc , err := h . a . Enc . Encrypt ( req . APIKey )
if err != nil {
resp . Fail ( c , http . StatusInternalServerError , "failed to encrypt api key" )
return
}
ch := store . Channel {
Name : req . Name , Provider : req . Provider , Formats : formats , Homepage : req . Homepage , BaseURL : strings . TrimRight ( req . BaseURL , "/" ) ,
Name : req . Name , Provider : req . Provider , Formats : formats , BaseURL : baseURL ,
APIKeyEnc : enc , Weight : intOr ( req . Weight , 1 ) , Priority : intOr ( req . Priority , 0 ) ,
TimeoutMS : intOr ( req . TimeoutMS , 120000 ) , MaxConcurrency : intOr ( req . MaxConcurrency , 16 ) ,
HealthStatus : store . ChannelHealthHealthy , Enabled : boolOr ( req . Enabled , true ) ,
@@ -163,18 +184,17 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
return
}
var body struct {
Name * string ` json:"name" `
Provider * string ` json:"provider" `
Name * string ` json:"name" `
Provider * string ` json:"provider" `
Formats * [ ] string ` json:"formats" `
Homepage * string ` json:"homepage " `
BaseURL * string ` json:"base_url " `
APIKey * string ` json:"api_key " `
Weight * int ` json:"weight " `
Priority * int ` json:"priority " `
TimeoutMS * int ` json:"timeout_ms " `
MaxConcurrency * int ` json:"max_concurrency " `
HealthStatus * string ` json:"health_status " `
Enabled * bool ` json:"enabled" `
BaseURL * string ` json:"base_url " `
APIKey * string ` json:"api_key " `
Weight * int ` json:"weight " `
Priority * int ` json:"priority " `
TimeoutMS * int ` json:"timeout_ms " `
MaxConcurrency * int ` json:"max_concurrency " `
HealthStatus * string ` json:"health_status " `
Enabled * bool ` json:"enabled " `
}
if err := c . ShouldBindJSON ( & body ) ; err != nil {
resp . Fail ( c , http . StatusBadRequest , "invalid input" )
@@ -197,7 +217,16 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
updates [ "provider" ] = * body . Provider
}
if body . BaseURL != nil {
updates [ "base_url" ] = strings . TrimRight ( * body . BaseURL , "/" )
prov := ch . Provider
if body . Provider != nil {
prov = * body . Provider
}
b , berr := resolveBaseURL ( prov , * body . BaseURL )
if berr != nil {
resp . Fail ( c , http . StatusBadRequest , berr . Error ( ) )
return
}
updates [ "base_url" ] = b
}
if body . APIKey != nil && * body . APIKey != "" {
enc , err := h . a . Enc . Encrypt ( * body . APIKey )
@@ -219,9 +248,6 @@ func (h *Handler) AdminUpdateChannel(c *gin.Context) {
if body . MaxConcurrency != nil {
updates [ "max_concurrency" ] = * body . MaxConcurrency
}
if body . Homepage != nil {
updates [ "homepage" ] = * body . Homepage
}
if body . HealthStatus != nil {
updates [ "health_status" ] = * body . HealthStatus
}
@@ -403,53 +429,6 @@ func (h *Handler) AdminImportChannelModels(c *gin.Context) {
resp . OK ( c , gin . H { "imported" : imported } )
}
// AdminChannelFavicon GET /api/v1/admin/channels/:id/favicon — 代理获取渠道主页 favicon(内存缓存 1h)。
func ( h * Handler ) AdminChannelFavicon ( c * gin . Context ) {
id , err := strconv . ParseUint ( c . Param ( "id" ) , 10 , 64 )
if err != nil {
c . Status ( http . StatusBadRequest )
return
}
var ch store . Channel
if err := h . a . DB . First ( & ch , id ) . Error ; err != nil || ch . Homepage == "" {
c . Status ( http . StatusNotFound )
return
}
h . favMu . Lock ( )
if f , ok := h . favCache [ id ] ; ok && time . Since ( f . at ) < time . Hour {
h . favMu . Unlock ( )
c . Data ( http . StatusOK , f . ct , f . data )
return
}
h . favMu . Unlock ( )
url := strings . TrimRight ( ch . Homepage , "/" ) + "/favicon.ico"
client := & http . Client { Timeout : 5 * time . Second }
resp , err := client . Get ( url )
if err != nil || resp . StatusCode != http . StatusOK {
if resp != nil {
resp . Body . Close ( )
}
c . Status ( http . StatusNotFound )
return
}
defer resp . Body . Close ( )
data , err := io . ReadAll ( io . LimitReader ( resp . Body , 256 * 1024 ) )
if err != nil || len ( data ) == 0 {
c . Status ( http . StatusNotFound )
return
}
ct := resp . Header . Get ( "Content-Type" )
if ct == "" {
ct = "image/x-icon"
}
h . favMu . Lock ( )
h . favCache [ id ] = favEntry { data : data , ct : ct , at : time . Now ( ) }
h . favMu . Unlock ( )
c . Data ( http . StatusOK , ct , data )
}
var _ = clause . Assignments // 保留 gorm/clause 引用(后续定价批处理用)
func intOr ( p * int , def int ) int {