feat(api): 模型接入 - 统一出网 User-Agent 并支持添加时测试模型

Fixes #39
This commit is contained in:
ddCat
2026-07-29 18:31:01 +08:00
parent 09f5ff03cc
commit 55875207c9
19 changed files with 348 additions and 36 deletions

View File

@@ -694,6 +694,7 @@ func proxySystemRequest(c *gin.Context, svc *service.Service, user *model.User,
if accept := c.GetHeader("Accept"); accept != "" {
upstreamReq.Header.Set("Accept", accept)
}
service.ApplyDefaultOutboundHeaders(upstreamReq)
if protocol == model.ChannelInterfaceGeminiVeo {
upstreamReq.Header.Set("x-goog-api-key", channel.APIKey)
} else {

View File

@@ -97,7 +97,7 @@ func proxyCustomRelayRequest(c *gin.Context, policy service.RuntimeRequestPolicy
} else {
upstreamReq.Header.Set("Accept", "application/json")
}
upstreamReq.Header.Set("User-Agent", "InfiniteCanvas/custom-channel-relay")
service.ApplyDefaultOutboundHeaders(upstreamReq)
if apiFormat == "gemini" {
upstreamReq.Header.Set("x-goog-api-key", apiKey)
} else {

View File

@@ -22,6 +22,9 @@ func TestCustomRelayForwardsOpenAIRequestWithoutBrowserHeaders(t *testing.T) {
if r.Header.Get("Authorization") != "Bearer "+apiKey {
t.Errorf("Authorization = %q", r.Header.Get("Authorization"))
}
if r.Header.Get("User-Agent") != service.DefaultOutboundUserAgent {
t.Errorf("User-Agent = %q", r.Header.Get("User-Agent"))
}
for _, name := range []string{"Cookie", "Origin", "Referer", "X-Canvas-Upstream-URL", "X-Forwarded-For"} {
if value := r.Header.Get(name); value != "" {
t.Errorf("upstream received %s = %q", name, value)

View File

@@ -222,6 +222,28 @@ func RegisterFinanceRoutes(r *gin.RouterGroup, svc *service.Service) {
}
ok(c, result)
})
r.POST("/admin/channels/:id/models/test", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
if !enforceRateLimit(c, "admin-channel-model-test:"+user.ID+":"+c.Param("id"), 5, time.Minute) {
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 64<<10)
var req service.ChannelModelRequest
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
result, err := svc.TestAdminChannelModel(c.Request.Context(), user, c.Param("id"), req)
if err != nil {
failService(c, err)
return
}
ok(c, result)
})
r.POST("/admin/channels/:id/models", func(c *gin.Context) {
saveChannelModel(c, svc, "")
})

View File

@@ -80,7 +80,7 @@ func authorizeCustomRelay(method string, target *url.URL, apiFormat string, cont
return errors.New("自定义渠道不允许使用该请求方法")
}
if apiFormat == "openai" {
if len(query) != 0 || (!strings.HasSuffix(requestPath, "/responses") && !strings.HasSuffix(requestPath, "/chat/completions") && !strings.HasSuffix(requestPath, "/images/generations") && !strings.HasSuffix(requestPath, "/video/generations") && !strings.HasSuffix(requestPath, "/videos/generations") && !strings.HasSuffix(requestPath, "/contents/generations/tasks")) {
if len(query) != 0 || (!strings.HasSuffix(requestPath, "/responses") && !strings.HasSuffix(requestPath, "/chat/completions") && !strings.HasSuffix(requestPath, "/images/generations") && !strings.HasSuffix(requestPath, "/audio/speech") && !strings.HasSuffix(requestPath, "/video/generations") && !strings.HasSuffix(requestPath, "/videos/generations") && !strings.HasSuffix(requestPath, "/contents/generations/tasks")) {
return errors.New("自定义渠道不允许访问该上游接口")
}
return nil

View File

@@ -28,6 +28,7 @@ func TestAuthorizeCustomRelayAllowsModelsAndAgentEndpoints(t *testing.T) {
{method: http.MethodGet, target: "https://api.example.com/v1/models", apiFormat: "openai"},
{method: http.MethodPost, target: "https://api.example.com/v1/responses", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodPost, target: "https://api.example.com/v1/chat/completions", apiFormat: "openai", contentType: "application/json; charset=utf-8"},
{method: http.MethodPost, target: "https://api.example.com/v1/audio/speech", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodPost, target: "https://api.example.com/v1/video/generations", apiFormat: "openai", contentType: "application/json"},
{method: http.MethodGet, target: "https://api.example.com/v1/video/generations/task-1", apiFormat: "openai"},
{method: http.MethodPost, target: "https://api.x.ai/v1/videos/generations", apiFormat: "openai", contentType: "application/json"},