Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

164 řádky
6.3 KiB

  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "github.com/QuantumNous/new-api/common"
  11. "github.com/QuantumNous/new-api/model"
  12. "github.com/QuantumNous/new-api/setting/system_setting"
  13. )
  14. type TianyiYunAssetAdapter struct{}
  15. func NewTianyiYunAssetAdapter() AssetAdapter {
  16. return &TianyiYunAssetAdapter{}
  17. }
  18. func (a *TianyiYunAssetAdapter) Name() string {
  19. return "tianyiyun_asset"
  20. }
  21. func (a *TianyiYunAssetAdapter) Supports(operation AssetOperation) bool {
  22. return operation == AssetOperationAssetCreate || operation == AssetOperationAssetGet
  23. }
  24. func (a *TianyiYunAssetAdapter) DoAssetRequest(ctx context.Context, channel *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError) {
  25. if !a.Supports(req.Action.Operation) {
  26. return nil, newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported", req.Action.Operation), http.StatusBadRequest)
  27. }
  28. if channel == nil {
  29. return nil, newAssetError(AssetErrorInvalidRequest, "channel is required", http.StatusBadRequest)
  30. }
  31. upstreamURL, body, method, assetErr := buildTianyiYunAssetRequest(channel, req)
  32. if assetErr != nil {
  33. return nil, assetErr
  34. }
  35. fetchSetting := system_setting.GetFetchSetting()
  36. if err := common.ValidateURLWithFetchSetting(upstreamURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
  37. return nil, newAssetError(AssetErrorServer, fmt.Sprintf("request blocked: %v", err), http.StatusForbidden)
  38. }
  39. httpReq, err := http.NewRequestWithContext(ctx, method, upstreamURL, bytes.NewReader(body))
  40. if err != nil {
  41. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  42. }
  43. httpReq.Header.Set("Accept", "application/json")
  44. httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(channel.Key))
  45. if method == http.MethodPost {
  46. httpReq.Header.Set("Content-Type", "application/json")
  47. }
  48. client, err := GetHttpClientWithProxy(channel.GetSetting().Proxy)
  49. if err != nil {
  50. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  51. }
  52. if client == nil {
  53. client = http.DefaultClient
  54. }
  55. resp, err := client.Do(httpReq)
  56. if err != nil {
  57. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  58. }
  59. defer resp.Body.Close()
  60. data, err := io.ReadAll(resp.Body)
  61. if err != nil {
  62. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  63. }
  64. if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
  65. return nil, newAssetError(AssetErrorUpstream, string(data), http.StatusBadGateway)
  66. }
  67. return &AssetUpstreamResponse{StatusCode: resp.StatusCode, Header: resp.Header, Body: data}, nil
  68. }
  69. func buildTianyiYunAssetRequest(channel *model.Channel, req AssetRequest) (string, []byte, string, *AssetError) {
  70. baseURL, err := tianyiYunAssetBaseURL(channel)
  71. if err != nil {
  72. return "", nil, "", newAssetError(AssetErrorInvalidRequest, err.Error(), http.StatusBadRequest)
  73. }
  74. switch req.Action.Operation {
  75. case AssetOperationAssetCreate:
  76. payload, assetErr := buildTianyiYunAssetCreatePayload(req.Body)
  77. if assetErr != nil {
  78. return "", nil, "", assetErr
  79. }
  80. data, err := common.Marshal(payload)
  81. if err != nil {
  82. return "", nil, "", newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  83. }
  84. return baseURL + "/api/assets/upload", data, http.MethodPost, nil
  85. case AssetOperationAssetGet:
  86. id := tianyiYunAssetField(req.Body, "Id", "id")
  87. if id == "" {
  88. return "", nil, "", newAssetError(AssetErrorInvalidRequest, "asset id is required", http.StatusBadRequest)
  89. }
  90. return baseURL + "/api/assets/" + url.PathEscape(id), nil, http.MethodGet, nil
  91. default:
  92. return "", nil, "", newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported", req.Action.Operation), http.StatusBadRequest)
  93. }
  94. }
  95. func tianyiYunAssetBaseURL(channel *model.Channel) (string, error) {
  96. baseURL := strings.TrimSpace(channel.GetBaseURL())
  97. if baseURL == "" {
  98. baseURL = "https://ai.ctaigw.cn/v1"
  99. }
  100. u, err := url.Parse(baseURL)
  101. if err != nil || u.Scheme == "" || u.Host == "" {
  102. return "", fmt.Errorf("invalid TianyiYun asset base URL")
  103. }
  104. u.Path = strings.TrimRight(u.Path, "/")
  105. if !strings.HasSuffix(u.Path, "/v1") {
  106. u.Path += "/v1"
  107. }
  108. u.RawQuery = ""
  109. u.Fragment = ""
  110. return strings.TrimRight(u.String(), "/"), nil
  111. }
  112. func buildTianyiYunAssetCreatePayload(body map[string]any) (map[string]string, *AssetError) {
  113. sourceURL := tianyiYunAssetField(body, "URL", "url")
  114. if sourceURL == "" {
  115. return nil, newAssetError(AssetErrorInvalidRequest, "asset URL is required", http.StatusBadRequest)
  116. }
  117. parsedURL, err := url.Parse(sourceURL)
  118. if err != nil || (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") || parsedURL.Host == "" {
  119. return nil, newAssetError(AssetErrorInvalidRequest, "asset URL must use http or https", http.StatusBadRequest)
  120. }
  121. assetType := tianyiYunAssetField(body, "AssetType", "asset_type")
  122. if assetType != "Image" && assetType != "Video" && assetType != "Audio" {
  123. return nil, newAssetError(AssetErrorInvalidRequest, "asset type must be Image, Video, or Audio", http.StatusBadRequest)
  124. }
  125. fetchSetting := system_setting.GetFetchSetting()
  126. if err := common.ValidateURLWithFetchSetting(sourceURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
  127. return nil, newAssetError(AssetErrorInvalidRequest, fmt.Sprintf("asset URL blocked: %v", err), http.StatusBadRequest)
  128. }
  129. payload := map[string]string{"url": sourceURL, "asset_type": assetType}
  130. if name := tianyiYunAssetField(body, "Name", "name"); name != "" {
  131. payload["name"] = name
  132. }
  133. return payload, nil
  134. }
  135. func tianyiYunAssetField(body map[string]any, upperKey string, lowerKey string) string {
  136. if body == nil {
  137. return ""
  138. }
  139. if value, ok := body[upperKey]; ok {
  140. if text, ok := value.(string); ok {
  141. return strings.TrimSpace(text)
  142. }
  143. }
  144. if value, ok := body[lowerKey]; ok {
  145. if text, ok := value.(string); ok {
  146. return strings.TrimSpace(text)
  147. }
  148. }
  149. return ""
  150. }