package controller import ( "context" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" ) const defaultDoubaoAssetBaseURL = "https://ark.cn-beijing.volcengineapi.com" var blockedAssetActions = map[string]struct{}{ "createassetgroup": {}, "getassetgroup": {}, "listassetgroups": {}, "updateassetgroup": {}, "deleteassetgroup": {}, } func assetProxyError(c *gin.Context, status int, errType, message string) { c.JSON(status, gin.H{ "error": gin.H{ "message": message, "type": errType, }, }) } func buildDoubaoAssetURL(channel *model.Channel, action string, version string) (string, error) { baseURL := defaultDoubaoAssetBaseURL if channel != nil && channel.BaseURL != nil { if configuredBaseURL := strings.TrimSpace(*channel.BaseURL); configuredBaseURL != "" { baseURL = configuredBaseURL } } u, err := url.Parse(baseURL) if err != nil { return "", err } if u.Scheme == "" || u.Host == "" { return "", fmt.Errorf("invalid Doubao asset base URL: %s", baseURL) } u.Path = strings.TrimRight(u.Path, "/") + "/api/v1/multimodal/sd/assets" u.RawQuery = "" u.Fragment = "" query := u.Query() query.Set("Action", action) if strings.TrimSpace(version) == "" { version = "2024-01-01" } query.Set("Version", version) u.RawQuery = query.Encode() return u.String(), nil } func effectiveDoubaoAssetGroup(c *gin.Context) string { if group := strings.TrimSpace(common.GetContextKeyString(c, constant.ContextKeyUsingGroup)); group != "" { return group } return strings.TrimSpace(common.GetContextKeyString(c, constant.ContextKeyTokenGroup)) } func concreteDoubaoAssetGroupsForRequest(c *gin.Context, autoGroups func(string) []string) []string { group := effectiveDoubaoAssetGroup(c) if group != "auto" { if group == "" { return nil } return []string{group} } userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup) groups := autoGroups(userGroup) concreteGroups := make([]string, 0, len(groups)) for _, candidate := range groups { candidate = strings.TrimSpace(candidate) if candidate == "" || candidate == "auto" { continue } concreteGroups = append(concreteGroups, candidate) } return concreteGroups } func resolveDoubaoAssetChannelForRequest(c *gin.Context) (*model.Channel, string, error) { userId := c.GetInt("id") var lastErr error for _, group := range concreteDoubaoAssetGroupsForRequest(c, service.GetUserAutoGroup) { channel, err := service.ResolveDoubaoAssetChannel(userId, group) if err != nil { lastErr = err logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to resolve Doubao asset channel for group %s: %v", group, err)) continue } if channel != nil { return channel, group, nil } } if lastErr != nil { return nil, "", lastErr } return nil, "", nil } func DoubaoAssetProxy(c *gin.Context) { action := strings.TrimSpace(c.Query("Action")) if action == "" { assetProxyError(c, http.StatusBadRequest, "invalid_request_error", "Action query parameter is required") return } if _, ok := blockedAssetActions[strings.ToLower(action)]; ok { assetProxyError(c, http.StatusBadRequest, "invalid_request_error", fmt.Sprintf("Asset group API (%s) is not supported", action)) return } version := strings.TrimSpace(c.Query("Version")) if version == "" { version = "2024-01-01" } channel, _, err := resolveDoubaoAssetChannelForRequest(c) if err != nil { assetProxyError(c, http.StatusBadGateway, "server_error", fmt.Sprintf("Failed to resolve Doubao asset channel: %v", err)) return } if channel == nil { assetProxyError(c, http.StatusBadGateway, "server_error", "Failed to resolve Doubao asset channel") return } if strings.TrimSpace(channel.Key) == "" { assetProxyError(c, http.StatusBadGateway, "server_error", "Doubao asset channel key is missing") return } upstreamURL, err := buildDoubaoAssetURL(channel, action, version) if err != nil { assetProxyError(c, http.StatusBadGateway, "server_error", fmt.Sprintf("Failed to build upstream URL: %v", err)) return } fetchSetting := system_setting.GetFetchSetting() if err := common.ValidateURLWithFetchSetting(upstreamURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("Doubao asset URL blocked: %v", err)) assetProxyError(c, http.StatusForbidden, "server_error", fmt.Sprintf("request blocked: %v", err)) return } client, err := service.GetHttpClientWithProxy(channel.GetSetting().Proxy) if err != nil { assetProxyError(c, http.StatusBadGateway, "server_error", fmt.Sprintf("Failed to create proxy client: %v", err)) return } if client == nil { assetProxyError(c, http.StatusBadGateway, "server_error", "Failed to create proxy client") return } ctx, cancel := context.WithTimeout(c.Request.Context(), 60*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, c.Request.Body) if err != nil { assetProxyError(c, http.StatusBadGateway, "server_error", fmt.Sprintf("Failed to create upstream request: %v", err)) return } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(channel.Key)) resp, err := client.Do(req) if err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to proxy Doubao asset request to %s: %s", upstreamURL, err.Error())) assetProxyError(c, http.StatusBadGateway, "server_error", fmt.Sprintf("Failed to proxy Doubao asset request: %v", err)) return } defer resp.Body.Close() for key, values := range resp.Header { for _, value := range values { c.Writer.Header().Add(key, value) } } c.Writer.WriteHeader(resp.StatusCode) if _, err = io.Copy(c.Writer, resp.Body); err != nil { logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to copy Doubao asset upstream response: %s", err.Error())) } }