Просмотр исходного кода

feat(chinamobile): add Seedance channel and isolated asset library

Co-Authored-By: Codex <noreply@anthropic.com>
master
fengsilin 1 месяц назад
Родитель
Сommit
c67d8c409e
100 измененных файлов: 10108 добавлений и 138 удалений
  1. +3
    -0
      .gitignore
  2. +3
    -0
      Dockerfile
  3. +2
    -0
      common/api_type.go
  4. +17
    -0
      common/chinamobile_seedance_channel_test.go
  5. +1
    -1
      common/endpoint_type.go
  6. +3
    -0
      constant/channel.go
  7. +1
    -0
      controller/channel-test.go
  8. +74
    -120
      controller/doubao_asset.go
  9. +183
    -0
      controller/doubao_asset_real_e2e_test.go
  10. +330
    -14
      controller/doubao_asset_test.go
  11. +89
    -0
      docs/superpowers/specs/2026-07-17-chinamobile-asset-isolation-design.md
  12. +13
    -1
      go.mod
  13. +80
    -1
      go.sum
  14. +3
    -1
      model/main.go
  15. +8
    -0
      model/user_asset_channel.go
  16. +20
    -0
      model/user_asset_channel_test.go
  17. +45
    -0
      model/user_asset_group.go
  18. +63
    -0
      model/user_asset_group_test.go
  19. +494
    -0
      relay/channel/task/chinamobile_seedance/adaptor.go
  20. +349
    -0
      relay/channel/task/chinamobile_seedance/adaptor_test.go
  21. +9
    -0
      relay/channel/task/chinamobile_seedance/constants.go
  22. +81
    -0
      relay/channel/task/chinamobile_seedance/sdk_client.go
  23. +5
    -0
      relay/helper/matrix_usage_capability.go
  24. +9
    -0
      relay/helper/matrix_usage_capability_test.go
  25. +3
    -0
      relay/relay_adaptor.go
  26. +15
    -0
      relay/relay_adaptor_chinamobile_test.go
  27. +5
    -0
      relay/relay_task.go
  28. +107
    -0
      service/asset_adapter.go
  29. +28
    -0
      service/asset_adapter_test.go
  30. +739
    -0
      service/asset_chinamobile.go
  31. +41
    -0
      service/asset_chinamobile_sdk.go
  32. +19
    -0
      service/asset_chinamobile_sdk_test.go
  33. +391
    -0
      service/asset_chinamobile_test.go
  34. +98
    -0
      service/asset_compatible.go
  35. +27
    -0
      service/asset_compatible_test.go
  36. +77
    -0
      service/asset_operation.go
  37. +45
    -0
      service/asset_operation_test.go
  38. +79
    -0
      service/asset_resolver.go
  39. +106
    -0
      service/asset_resolver_test.go
  40. +20
    -0
      service/channel_select_test.go
  41. +128
    -0
      service/chinamobile_user_asset_group.go
  42. +159
    -0
      service/chinamobile_user_asset_group_test.go
  43. +1
    -0
      service/video_asset_channel.go
  44. +4
    -0
      service/video_asset_channel_test.go
  45. +17
    -0
      setup_test.sql
  46. +24
    -0
      setup_test_env.ps1
  47. +154
    -0
      third_party/ecloudsdkcore/api_client.go
  48. +14
    -0
      third_party/ecloudsdkcore/auth/constants.go
  49. +117
    -0
      third_party/ecloudsdkcore/auth/credential.go
  50. +284
    -0
      third_party/ecloudsdkcore/auth/credential_impl.go
  51. +175
    -0
      third_party/ecloudsdkcore/auth/provider/providers.go
  52. +215
    -0
      third_party/ecloudsdkcore/config/config.go
  53. +131
    -0
      third_party/ecloudsdkcore/config/runtime_config.go
  54. +26
    -0
      third_party/ecloudsdkcore/consts/constants.go
  55. +106
    -0
      third_party/ecloudsdkcore/errs/errors.go
  56. +5
    -0
      third_party/ecloudsdkcore/go.mod
  57. +280
    -0
      third_party/ecloudsdkcore/http/http_client.go
  58. +186
    -0
      third_party/ecloudsdkcore/http/http_client_builder.go
  59. +80
    -0
      third_party/ecloudsdkcore/param/api_params.go
  60. +13
    -0
      third_party/ecloudsdkcore/position/http_position.go
  61. +7
    -0
      third_party/ecloudsdkcore/region/region.go
  62. +320
    -0
      third_party/ecloudsdkcore/request/http_request.go
  63. +7
    -0
      third_party/ecloudsdkcore/response/http_response.go
  64. +70
    -0
      third_party/ecloudsdkcore/retry/retry_context.go
  65. +85
    -0
      third_party/ecloudsdkcore/retry/retry_policy.go
  66. +78
    -0
      third_party/ecloudsdkcore/retry/retry_template.go
  67. +47
    -0
      third_party/ecloudsdkcore/retry/retry_template_builder.go
  68. +81
    -0
      third_party/ecloudsdkcore/utils/algorithm_util.go
  69. +49
    -0
      third_party/ecloudsdkcore/utils/convert_util.go
  70. +82
    -0
      third_party/ecloudsdkcore/utils/http_util.go
  71. +150
    -0
      third_party/ecloudsdkcore/utils/other_util.go
  72. +33
    -0
      third_party/ecloudsdkcore/utils/string_util.go
  73. +84
    -0
      third_party/ecloudsdkcore/utils/struct_map_util.go
  74. +73
    -0
      third_party/ecloudsdkcore/utils/trans_util.go
  75. +1228
    -0
      third_party/ecloudsdkmaas/client.go
  76. +5
    -0
      third_party/ecloudsdkmaas/go.mod
  77. +55
    -0
      third_party/ecloudsdkmaas/model/auto_renew_order_body.go
  78. +54
    -0
      third_party/ecloudsdkmaas/model/auto_renew_order_request.go
  79. +66
    -0
      third_party/ecloudsdkmaas/model/auto_renew_order_request_auto_renew_list.go
  80. +102
    -0
      third_party/ecloudsdkmaas/model/auto_renew_order_response.go
  81. +55
    -0
      third_party/ecloudsdkmaas/model/batch_delete_doc_body.go
  82. +55
    -0
      third_party/ecloudsdkmaas/model/batch_delete_doc_path.go
  83. +66
    -0
      third_party/ecloudsdkmaas/model/batch_delete_doc_request.go
  84. +102
    -0
      third_party/ecloudsdkmaas/model/batch_delete_doc_response.go
  85. +54
    -0
      third_party/ecloudsdkmaas/model/batch_delete_doc_response_body.go
  86. +55
    -0
      third_party/ecloudsdkmaas/model/cancel_order_body.go
  87. +54
    -0
      third_party/ecloudsdkmaas/model/cancel_order_request.go
  88. +102
    -0
      third_party/ecloudsdkmaas/model/cancel_order_response.go
  89. +99
    -0
      third_party/ecloudsdkmaas/model/create_asset_body.go
  90. +79
    -0
      third_party/ecloudsdkmaas/model/create_asset_group_body.go
  91. +54
    -0
      third_party/ecloudsdkmaas/model/create_asset_group_request.go
  92. +102
    -0
      third_party/ecloudsdkmaas/model/create_asset_group_response.go
  93. +133
    -0
      third_party/ecloudsdkmaas/model/create_asset_group_response_body.go
  94. +54
    -0
      third_party/ecloudsdkmaas/model/create_asset_request.go
  95. +102
    -0
      third_party/ecloudsdkmaas/model/create_asset_response.go
  96. +139
    -0
      third_party/ecloudsdkmaas/model/create_body.go
  97. +79
    -0
      third_party/ecloudsdkmaas/model/create_dataset_body.go
  98. +55
    -0
      third_party/ecloudsdkmaas/model/create_dataset_query.go
  99. +66
    -0
      third_party/ecloudsdkmaas/model/create_dataset_request.go
  100. +123
    -0
      third_party/ecloudsdkmaas/model/create_dataset_request_retrieval_config.go

+ 3
- 0
.gitignore Просмотреть файл

@@ -34,6 +34,9 @@ docs/superpowers

# Local dev server logs
web/vite-*.log
/server_*.log
/server_*_err.log
/asset_test_log.md

# E2E / Playwright artifacts
test-artifacts/


+ 3
- 0
Dockerfile Просмотреть файл

@@ -23,6 +23,9 @@ ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
WORKDIR /build

ADD go.mod go.sum ./
COPY ./third_party/ecloudsdkcore ./third_party/ecloudsdkcore
COPY ./third_party/ecloudsdkmaas ./third_party/ecloudsdkmaas
COPY ./third_party/maas_seedance_sdk_1.0.0_go ./third_party/maas_seedance_sdk_1.0.0_go
RUN --mount=type=cache,target=/go/pkg/mod go mod download

COPY . .


+ 2
- 0
common/api_type.go Просмотреть файл

@@ -57,6 +57,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeVolcEngine
case constant.ChannelTypeDoubaoVideoCompatibleTianyiYun:
apiType = constant.APITypeVolcEngine
case constant.ChannelTypeChinaMobileSeedance:
apiType = constant.APITypeVolcEngine
case constant.ChannelTypeBaiduV2:
apiType = constant.APITypeBaiduV2
case constant.ChannelTypeOpenRouter:


+ 17
- 0
common/chinamobile_seedance_channel_test.go Просмотреть файл

@@ -0,0 +1,17 @@
package common

import (
"testing"

"github.com/QuantumNous/new-api/constant"
"github.com/stretchr/testify/require"
)

func TestChinaMobileSeedanceChannelMappings(t *testing.T) {
apiType, ok := ChannelType2APIType(constant.ChannelTypeChinaMobileSeedance)
require.True(t, ok)
require.Equal(t, constant.APITypeVolcEngine, apiType)

endpoints := GetEndpointTypesByChannelType(constant.ChannelTypeChinaMobileSeedance, "doubao-seedance-2.0")
require.Contains(t, endpoints, constant.EndpointTypeDoubaoVideo)
}

+ 1
- 1
common/endpoint_type.go Просмотреть файл

@@ -30,7 +30,7 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse}
case constant.ChannelTypeSora:
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIVideo}
case constant.ChannelTypeDoubaoVideo, constant.ChannelTypeDoubaoVideoCompatibleAiping, constant.ChannelTypeDoubaoVideoCompatibleTianyiYun:
case constant.ChannelTypeDoubaoVideo, constant.ChannelTypeDoubaoVideoCompatibleAiping, constant.ChannelTypeDoubaoVideoCompatibleTianyiYun, constant.ChannelTypeChinaMobileSeedance:
endpointTypes = []constant.EndpointType{constant.EndpointTypeDoubaoVideo}
default:
if IsOpenAIResponseOnlyModel(modelName) {


+ 3
- 0
constant/channel.go Просмотреть файл

@@ -58,6 +58,7 @@ const (
ChannelTypeDoubaoVideoCompatibleAiping = 58
ChannelTypeKlingAiping = 59
ChannelTypeDoubaoVideoCompatibleTianyiYun = 60
ChannelTypeChinaMobileSeedance = 61
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
@@ -124,6 +125,7 @@ var ChannelBaseURLs = []string{
"", //58
"https://aiping.cn/api", //59
"https://ai.ctaigw.cn", //60
"https://zhenze-huhehaote.cmecloud.cn/api/v3", //61
}

var ChannelTypeNames = map[int]string{
@@ -184,6 +186,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeDoubaoVideoCompatibleAiping: "DoubaoVideoCompatibleAiping",
ChannelTypeKlingAiping: "KlingAiping",
ChannelTypeDoubaoVideoCompatibleTianyiYun: "DoubaoVideoCompatibleTianyiYun",
ChannelTypeChinaMobileSeedance: "ChinaMobileSeedance",
}

func GetChannelTypeName(channelType int) string {


+ 1
- 0
controller/channel-test.go Просмотреть файл

@@ -67,6 +67,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
constant.ChannelTypeDoubaoVideo,
constant.ChannelTypeDoubaoVideoCompatibleAiping,
constant.ChannelTypeDoubaoVideoCompatibleTianyiYun,
constant.ChannelTypeChinaMobileSeedance,
constant.ChannelTypeVidu,
}
if lo.Contains(unsupportedTestChannelTypes, channel.Type) {


+ 74
- 120
controller/doubao_asset.go Просмотреть файл

@@ -5,30 +5,17 @@ import (
"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{
@@ -38,36 +25,7 @@ func assetProxyError(c *gin.Context, status int, errType, message string) {
})
}

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
}
var doubaoAssetAutoGroups = service.GetUserAutoGroup

func effectiveDoubaoAssetGroup(c *gin.Context) string {
if group := strings.TrimSpace(common.GetContextKeyString(c, constant.ContextKeyUsingGroup)); group != "" {
@@ -98,34 +56,15 @@ func concreteDoubaoAssetGroupsForRequest(c *gin.Context, autoGroups func(string)
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")
actionRaw := strings.TrimSpace(c.Query("Action"))
if actionRaw == "" {
assetProxyError(c, http.StatusBadRequest, service.AssetErrorInvalidRequest, "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))
action, ok := service.ParseAssetAction(actionRaw)
if !ok {
assetProxyError(c, http.StatusBadRequest, service.AssetErrorInvalidRequest, fmt.Sprintf("unsupported asset Action: %s", actionRaw))
return
}

@@ -134,70 +73,85 @@ func DoubaoAssetProxy(c *gin.Context) {
version = "2024-01-01"
}

channel, _, err := resolveDoubaoAssetChannelForRequest(c)
rawBody, err := io.ReadAll(c.Request.Body)
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")
assetProxyError(c, http.StatusBadRequest, service.AssetErrorInvalidRequest, fmt.Sprintf("failed to read request body: %v", err))
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
body := map[string]any{}
if len(strings.TrimSpace(string(rawBody))) > 0 {
if err := common.Unmarshal(rawBody, &body); err != nil {
assetProxyError(c, http.StatusBadRequest, service.AssetErrorInvalidRequest, fmt.Sprintf("invalid JSON body: %v", err))
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))
userID := c.GetInt("id")
var lastAssetErr *service.AssetError
for _, group := range concreteDoubaoAssetGroupsForRequest(c, doubaoAssetAutoGroups) {
channel, adapter, assetErr := service.ResolveAssetChannelForOperation(userID, group, action.Operation)
if assetErr != nil {
lastAssetErr = assetErr
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to resolve asset channel for group %s action %s: %s", group, action.Action, assetErr.Message))
if assetErr.Type == service.AssetErrorChannelNotFound {
continue
}
break
}
if channel == nil || adapter == nil {
continue
}
assetRequest := service.AssetRequest{
Action: action,
Version: version,
Body: body,
RawBody: rawBody,
}
if service.IsChinaMobileAssetChannel(channel) {
if service.IsAssetGroupOperation(action.Operation) {
assetProxyError(c, http.StatusForbidden, service.AssetErrorOperationNotSupported, "China Mobile asset group APIs are managed by the platform")
return
}
groupID, assetErr := service.GetOrCreateChinaMobileUserAssetGroup(c.Request.Context(), userID, channel, func(ctx context.Context, userID int, channel *model.Channel) (string, *service.AssetError) {
return service.CreateChinaMobileUserAssetGroup(ctx, userID, adapter, channel)
})
if assetErr != nil {
assetProxyError(c, assetErr.HTTPStatus, assetErr.Type, assetErr.Message)
return
}
service.ScopeChinaMobileAssetRequest(&assetRequest, groupID)
if action.Operation == service.AssetOperationAssetGet || action.Operation == service.AssetOperationAssetUpdate || action.Operation == service.AssetOperationAssetDelete {
if assetErr := service.RequireChinaMobileAssetOwnership(c.Request.Context(), adapter, channel, assetRequest, groupID); assetErr != nil {
assetProxyError(c, assetErr.HTTPStatus, assetErr.Type, assetErr.Message)
return
}
}
}
resp, assetErr := adapter.DoAssetRequest(c.Request.Context(), channel, assetRequest)
if assetErr != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to proxy asset action %s via channel %d type %d: %s", action.Action, channel.Id, channel.Type, assetErr.Message))
assetProxyError(c, assetErr.HTTPStatus, assetErr.Type, assetErr.Message)
return
}
copyDoubaoAssetResponseHeaders(c, resp.Header)
c.Data(resp.StatusCode, "application/json", resp.Body)
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))
if lastAssetErr != nil {
assetProxyError(c, lastAssetErr.HTTPStatus, lastAssetErr.Type, lastAssetErr.Message)
return
}
defer resp.Body.Close()
assetProxyError(c, http.StatusBadGateway, service.AssetErrorChannelNotFound, "no available asset channel supports requested operation")
}

for key, values := range resp.Header {
func copyDoubaoAssetResponseHeaders(c *gin.Context, headers http.Header) {
for key, values := range headers {
switch http.CanonicalHeaderKey(key) {
case "Connection", "Content-Length", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Te", "Trailer", "Transfer-Encoding", "Upgrade":
continue
}
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()))
}
}

+ 183
- 0
controller/doubao_asset_real_e2e_test.go Просмотреть файл

@@ -0,0 +1,183 @@
package controller

import (
"bytes"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

const chinaMobileAssetE2EImageURL = "https://bkimg.cdn.bcebos.com/pic/caef76094b36acaf2edd2133a78e9a1001e9380136fe?x-bce-process=image/format,f_auto/watermark,image_d2F0ZXIvYmFpa2UyNzI,g_7,xp_5,yp_5,P_20/resize,m_lfit,limit_1,h_1080"

func TestChinaMobileAssetControllerWithRealUpstream(t *testing.T) {
ak := strings.TrimSpace(os.Getenv("CHINAMOBILE_ASSET_AK"))
sk := strings.TrimSpace(os.Getenv("CHINAMOBILE_ASSET_SK"))
poolID := strings.TrimSpace(os.Getenv("CHINAMOBILE_ASSET_POOL_ID"))
if ak == "" || sk == "" {
t.Skip("CHINAMOBILE_ASSET_AK and CHINAMOBILE_ASSET_SK are required")
}
if poolID == "" {
poolID = "CIDC-CORE-00"
}
db := setupRealAssetE2EDB(t)
createDoubaoAssetProxyChannel(t, db, 7101, constant.ChannelTypeChinaMobileSeedance, "default", "video-generation-key", common.ChannelStatusEnabled)
router := setupRealAssetE2ERouter()
suffix := time.Now().Format("20060102150405")
groupName := "new-api-e2e-" + suffix
assetName := "new-api-img-" + suffix

var groupId string
var assetId string
defer func() {
if assetId != "" {
code, _ := performRealAssetE2EAction(t, router, "DeleteAsset", map[string]any{"Id": assetId})
require.Equal(t, http.StatusOK, code, "cleanup DeleteAsset failed")
}
if groupId != "" {
code, _ := performRealAssetE2EAction(t, router, "DeleteAssetGroup", map[string]any{"Id": groupId})
require.Equal(t, http.StatusOK, code, "cleanup DeleteAssetGroup failed")
}
}()

code, body := performRealAssetE2EAction(t, router, "CreateAssetGroup", map[string]any{
"Name": groupName,
"GroupType": "AIGC",
"Description": "new-api real e2e test",
})
require.Equal(t, http.StatusOK, code)
groupId = realAssetE2EString(t, body, "Result.GroupId")
require.NotEmpty(t, groupId)

code, body = performRealAssetE2EAction(t, router, "CreateAsset", map[string]any{
"GroupId": groupId,
"Name": assetName,
"URL": chinaMobileAssetE2EImageURL,
"AssetType": "Image",
})
require.Equal(t, http.StatusOK, code)
assetId = realAssetE2EString(t, body, "Result")
require.NotEmpty(t, assetId)

var status string
for i := 0; i < 6; i++ {
code, body = performRealAssetE2EAction(t, router, "GetAsset", map[string]any{"Id": assetId})
require.Equal(t, http.StatusOK, code)
require.Equal(t, assetId, realAssetE2EString(t, body, "Result.Id"))
status = realAssetE2EString(t, body, "Result.Status")
if status != "Processing" {
break
}
time.Sleep(3 * time.Second)
}
require.Equal(t, "Active", status)

code, body = performRealAssetE2EAction(t, router, "ListAssets", map[string]any{
"PageNumber": 1,
"PageSize": 10,
"Filter": map[string]any{
"GroupType": "AIGC",
"GroupIds": []string{groupId},
},
})
require.Equal(t, http.StatusOK, code)
require.Equal(t, float64(1), realAssetE2EValue(t, body, "Result.TotalCount"))
}

func setupRealAssetE2EDB(t *testing.T) *gorm.DB {
t.Helper()
oldDB := model.DB
oldSQLitePath := common.SQLitePath
oldMemoryCacheEnabled := common.MemoryCacheEnabled
oldIsMasterNode := common.IsMasterNode
oldUsingSQLite := common.UsingSQLite
oldUsingMySQL := common.UsingMySQL
oldUsingPostgreSQL := common.UsingPostgreSQL
oldSQLDSN, hadSQLDSN := os.LookupEnv("SQL_DSN")

common.SQLitePath = "file:" + strings.NewReplacer("/", "_", " ", "_").Replace(t.Name()) + "?mode=memory&cache=shared"
common.MemoryCacheEnabled = false
common.IsMasterNode = false
common.UsingSQLite = false
common.UsingMySQL = false
common.UsingPostgreSQL = false
require.NoError(t, os.Setenv("SQL_DSN", "local"))
require.NoError(t, model.InitDB())

model.DB = model.DB.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)})
db := model.DB
sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)
require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.Ability{}, &model.UserAssetChannel{}))

t.Cleanup(func() {
_ = sqlDB.Close()
model.DB = oldDB
common.SQLitePath = oldSQLitePath
common.MemoryCacheEnabled = oldMemoryCacheEnabled
common.IsMasterNode = oldIsMasterNode
common.UsingSQLite = oldUsingSQLite
common.UsingMySQL = oldUsingMySQL
common.UsingPostgreSQL = oldUsingPostgreSQL
if hadSQLDSN {
_ = os.Setenv("SQL_DSN", oldSQLDSN)
} else {
_ = os.Unsetenv("SQL_DSN")
}
})
return db
}

func setupRealAssetE2ERouter() *gin.Engine {
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set("id", 10)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
c.Next()
})
router.POST("/api/v1/volcengine/asset", DoubaoAssetProxy)
return router
}

func performRealAssetE2EAction(t *testing.T, router *gin.Engine, action string, payload map[string]any) (int, string) {
t.Helper()
data, err := common.Marshal(payload)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action="+action, bytes.NewReader(data))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
return w.Code, w.Body.String()
}

func realAssetE2EString(t *testing.T, data string, path string) string {
t.Helper()
value := realAssetE2EValue(t, data, path)
s, ok := value.(string)
require.Truef(t, ok, "%s is not a string: %#v", path, value)
return s
}

func realAssetE2EValue(t *testing.T, data string, path string) any {
t.Helper()
var payload map[string]any
require.NoError(t, common.Unmarshal([]byte(data), &payload))
current := any(payload)
for _, part := range strings.Split(path, ".") {
m, ok := current.(map[string]any)
require.Truef(t, ok, "%s is not an object at %s", path, part)
current = m[part]
}
return current
}

+ 330
- 14
controller/doubao_asset_test.go Просмотреть файл

@@ -1,16 +1,23 @@
package controller

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

func setupDoubaoAssetProxyRouter(t *testing.T) *gin.Engine {
@@ -40,6 +47,74 @@ func decodeDoubaoAssetErrorMessage(t *testing.T, body string) string {
return payload.Error.Message
}

func setupDoubaoAssetProxyDB(t *testing.T) *gorm.DB {
t.Helper()

oldDB := model.DB
oldSQLitePath := common.SQLitePath
oldMemoryCacheEnabled := common.MemoryCacheEnabled
oldIsMasterNode := common.IsMasterNode
oldUsingSQLite := common.UsingSQLite
oldUsingMySQL := common.UsingMySQL
oldUsingPostgreSQL := common.UsingPostgreSQL
oldSQLDSN, hadSQLDSN := os.LookupEnv("SQL_DSN")

common.SQLitePath = "file:" + strings.NewReplacer("/", "_", " ", "_").Replace(t.Name()) + "?mode=memory&cache=shared"
common.MemoryCacheEnabled = false
common.IsMasterNode = false
common.UsingSQLite = false
common.UsingMySQL = false
common.UsingPostgreSQL = false
require.NoError(t, os.Setenv("SQL_DSN", "local"))
require.NoError(t, model.InitDB())

model.DB = model.DB.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)})
db := model.DB
sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)
require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.Ability{}, &model.UserAssetChannel{}, &model.UserAssetGroup{}))

t.Cleanup(func() {
_ = sqlDB.Close()
model.DB = oldDB
common.SQLitePath = oldSQLitePath
common.MemoryCacheEnabled = oldMemoryCacheEnabled
common.IsMasterNode = oldIsMasterNode
common.UsingSQLite = oldUsingSQLite
common.UsingMySQL = oldUsingMySQL
common.UsingPostgreSQL = oldUsingPostgreSQL
if hadSQLDSN {
_ = os.Setenv("SQL_DSN", oldSQLDSN)
} else {
_ = os.Unsetenv("SQL_DSN")
}
})

return db
}

func createDoubaoAssetProxyChannel(t *testing.T, db *gorm.DB, id int, channelType int, group string, key string, status int) {
t.Helper()

priority := int64(id)
weight := uint(10)
autoBan := 1
require.NoError(t, db.Create(&model.Channel{
Id: id,
Type: channelType,
Key: key,
Status: status,
Name: fmt.Sprintf("channel-%d", id),
Group: group,
Models: "seedance-2",
Priority: &priority,
Weight: &weight,
AutoBan: &autoBan,
CreatedTime: int64(id),
}).Error)
}

func TestDoubaoAssetProxyMissingActionReturns400(t *testing.T) {
router := setupDoubaoAssetProxyRouter(t)

@@ -51,35 +126,230 @@ func TestDoubaoAssetProxyMissingActionReturns400(t *testing.T) {
assert.Equal(t, "Action query parameter is required", decodeDoubaoAssetErrorMessage(t, w.Body.String()))
}

func TestDoubaoAssetProxyBlocksAssetGroupActionsCaseInsensitively(t *testing.T) {
func TestDoubaoAssetProxyUnknownActionReturns400(t *testing.T) {
router := setupDoubaoAssetProxyRouter(t)

req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action=CreateRealPersonAuthSession", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, decodeDoubaoAssetErrorMessage(t, w.Body.String()), "unsupported asset Action")
}

func TestDoubaoAssetProxyInvalidJSONReturns400(t *testing.T) {
router := setupDoubaoAssetProxyRouter(t)

for _, action := range []string{"CreateAssetGroup", "getassetgroup", "LISTASSETGROUPS", "UpdateAssetGroup", "deleteassetgroup"} {
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action=ListAssets", strings.NewReader(`{"PageNumber":`))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, decodeDoubaoAssetErrorMessage(t, w.Body.String()), "invalid JSON body")
}

func TestDoubaoAssetProxyRejectsChinaMobileAssetGroupActions(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
fake := &fakeDoubaoAssetAdapter{}
t.Cleanup(service.OverrideAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fake))
createDoubaoAssetProxyChannel(t, db, 61, constant.ChannelTypeChinaMobileSeedance, "default", "video-generation-key", common.ChannelStatusEnabled)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set("id", 10)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
c.Next()
})
router.POST("/api/v1/volcengine/asset", DoubaoAssetProxy)

for _, action := range []string{"CreateAssetGroup", "ListAssetGroups", "GetAssetGroup", "UpdateAssetGroup", "DeleteAssetGroup"} {
t.Run(action, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action="+action, nil)
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action="+action, strings.NewReader(`{"Id":"group-1","GroupType":"AIGC","Name":"g"}`))
w := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, decodeDoubaoAssetErrorMessage(t, w.Body.String()), "Asset group API ("+action+") is not supported")
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Equal(t, 0, fake.calls)
})
}
}

func TestBuildDoubaoAssetURLDefaultBaseAndEscapedQuery(t *testing.T) {
got, err := buildDoubaoAssetURL(&model.Channel{}, "ApplyUploadInner&Space", "")
func TestDoubaoAssetProxyDoesNotBlockNonChinaMobileAssetGroupAction(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
fake := &fakeDoubaoAssetAdapter{}
t.Cleanup(service.OverrideAssetAdapterForTest(constant.ChannelTypeDoubaoVideoCompatibleAiping, fake))
createDoubaoAssetProxyChannel(t, db, 63, constant.ChannelTypeDoubaoVideoCompatibleAiping, "default", "video-generation-key", common.ChannelStatusEnabled)
router := newDoubaoAssetProxyTestRouter()

require.NoError(t, err)
assert.Equal(t, defaultDoubaoAssetBaseURL+"/api/v1/multimodal/sd/assets?Action=ApplyUploadInner%26Space&Version=2024-01-01", got)
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action=ListAssetGroups", strings.NewReader(`{"PageNumber":1,"PageSize":10}`))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, service.AssetOperationAssetGroupList, fake.operation)
}

func TestBuildDoubaoAssetURLExplicitBaseURLOverridesDefault(t *testing.T) {
baseURL := "https://example.com/custom/"
func TestDoubaoAssetProxyAutoGroupContinuesAfterMissingGroup(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
fake := &fakeDoubaoAssetAdapter{}
t.Cleanup(service.OverrideAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fake))
createDoubaoAssetProxyChannel(t, db, 62, constant.ChannelTypeChinaMobileSeedance, "vip", "video-generation-key", common.ChannelStatusEnabled)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set("id", 10)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "auto")
common.SetContextKey(c, constant.ContextKeyUserGroup, "default")
c.Next()
})
router.POST("/api/v1/volcengine/asset", DoubaoAssetProxy)
oldAutoGroups := doubaoAssetAutoGroups
doubaoAssetAutoGroups = func(string) []string {
return []string{"default", "vip"}
}
t.Cleanup(func() {
doubaoAssetAutoGroups = oldAutoGroups
})

got, err := buildDoubaoAssetURL(&model.Channel{BaseURL: &baseURL}, "CommitUploadInner", "2025-02-03")
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action=ListAssets", strings.NewReader(`{"Filter":{"GroupType":"AIGC"},"PageNumber":1,"PageSize":10}`))
w := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)

require.NoError(t, err)
assert.Equal(t, "https://example.com/custom/api/v1/multimodal/sd/assets?Action=CommitUploadInner&Version=2025-02-03", got)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, service.AssetOperationAssetList, fake.operation)
}

func TestDoubaoAssetProxyForwardsAdapterResponseHeaders(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
fake := &fakeDoubaoAssetAdapter{responseHeader: http.Header{"X-Upstream-Request-Id": []string{"upstream-1"}, "Content-Length": []string{"999"}}}
t.Cleanup(service.OverrideAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fake))
createDoubaoAssetProxyChannel(t, db, 62, constant.ChannelTypeChinaMobileSeedance, "default", "video-generation-key", common.ChannelStatusEnabled)
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set("id", 10)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
c.Next()
})
router.POST("/api/v1/volcengine/asset", DoubaoAssetProxy)

req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action=ListAssets", strings.NewReader(`{"Filter":{"GroupType":"AIGC"},"PageNumber":1,"PageSize":10}`))
w := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "upstream-1", w.Header().Get("X-Upstream-Request-Id"))
assert.NotEqual(t, "999", w.Header().Get("Content-Length"))
}

func TestDoubaoAssetProxyScopesChinaMobileCreateAndListRequests(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
fake := &fakeDoubaoAssetAdapter{}
t.Cleanup(service.OverrideAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fake))
createDoubaoAssetProxyChannel(t, db, 71, constant.ChannelTypeChinaMobileSeedance, "default", "video-generation-key", common.ChannelStatusEnabled)
require.NoError(t, model.CreateUserAssetGroup(10, 71, "owned"))
router := newDoubaoAssetProxyTestRouter()

for _, tc := range []struct {
action string
body string
}{
{"CreateAsset", `{"GroupId":"forged","Name":"n","URL":"https://example.com/a.png","AssetType":"Image"}`},
{"ListAssets", `{"Filter":{"GroupType":"AIGC","GroupIds":["forged"]},"PageNumber":1,"PageSize":10}`},
} {
t.Run(tc.action, func(t *testing.T) {
fake.requests = nil
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action="+tc.action, strings.NewReader(tc.body))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
require.Len(t, fake.requests, 1)
if tc.action == "CreateAsset" {
assert.Equal(t, "owned", fake.requests[0].Body["GroupId"])
} else {
assert.Equal(t, []string{"owned"}, fake.requests[0].Body["Filter"].(map[string]any)["GroupIds"])
}
})
}
}

func TestDoubaoAssetProxyRejectsChinaMobileListAssetsWithoutFilter(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
createDoubaoAssetProxyChannel(t, db, 73, constant.ChannelTypeChinaMobileSeedance, "default", "video-generation-key", common.ChannelStatusEnabled)
require.NoError(t, model.CreateUserAssetGroup(10, 73, "owned"))
router := newDoubaoAssetProxyTestRouter()

for _, body := range []string{`{"PageNumber":1,"PageSize":10}`, `{"Filter":null,"PageNumber":1,"PageSize":10}`} {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action=ListAssets", strings.NewReader(body))
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, decodeDoubaoAssetErrorMessage(t, w.Body.String()), "Filter.GroupType is required")
}
}

func TestDoubaoAssetProxyHidesChinaMobileAssetsOwnedByAnotherUser(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
fake := &fakeDoubaoAssetAdapter{getAssetGroupID: "other"}
t.Cleanup(service.OverrideAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fake))
createDoubaoAssetProxyChannel(t, db, 72, constant.ChannelTypeChinaMobileSeedance, "default", "video-generation-key", common.ChannelStatusEnabled)
require.NoError(t, model.CreateUserAssetGroup(10, 72, "owned"))
router := newDoubaoAssetProxyTestRouter()

for _, action := range []string{"GetAsset", "UpdateAsset", "DeleteAsset"} {
t.Run(action, func(t *testing.T) {
fake.requests = nil
req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action="+action, strings.NewReader(`{"Id":"asset-other","Name":"n"}`))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusNotFound, w.Code)
require.Len(t, fake.requests, 1)
assert.Equal(t, service.AssetOperationAssetGet, fake.requests[0].Action.Operation)
})
}
}

func newDoubaoAssetProxyTestRouter() *gin.Engine {
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set("id", 10)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
c.Next()
})
router.POST("/api/v1/volcengine/asset", DoubaoAssetProxy)
return router
}

func TestDoubaoAssetProxyAutoGroupDoesNotBypassInvalidBinding(t *testing.T) {
db := setupDoubaoAssetProxyDB(t)
createDoubaoAssetProxyChannel(t, db, 1, constant.ChannelTypeChinaMobileSeedance, "other", "bad-key", common.ChannelStatusEnabled)
createDoubaoAssetProxyChannel(t, db, 2, constant.ChannelTypeChinaMobileSeedance, "vip", "video-generation-key", common.ChannelStatusEnabled)
require.NoError(t, model.BindUserAssetChannel(10, constant.ChannelTypeChinaMobileSeedance, "default", 1))
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set("id", 10)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "auto")
common.SetContextKey(c, constant.ContextKeyUserGroup, "default")
c.Next()
})
router.POST("/api/v1/volcengine/asset", DoubaoAssetProxy)
oldAutoGroups := doubaoAssetAutoGroups
doubaoAssetAutoGroups = func(string) []string {
return []string{"default", "vip"}
}
t.Cleanup(func() {
doubaoAssetAutoGroups = oldAutoGroups
})

req := httptest.NewRequest(http.MethodPost, "/api/v1/volcengine/asset?Action=ListAssets", strings.NewReader(`{"Filter":{"GroupType":"AIGC"},"PageNumber":1,"PageSize":10}`))
w := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusBadGateway, w.Code)
assert.Contains(t, w.Body.String(), service.AssetErrorBindingInvalid)
}

func TestEffectiveDoubaoAssetGroupUsesUsingGroupBeforeBlankTokenGroup(t *testing.T) {
@@ -101,3 +371,49 @@ func TestConcreteDoubaoAssetGroupsForAutoUsesUserAutoGroups(t *testing.T) {

assert.Equal(t, []string{"default", "vip"}, groups)
}

type fakeDoubaoAssetAdapter struct {
operation service.AssetOperation
responseHeader http.Header
requests []service.AssetRequest
getAssetGroupID string
calls int
}

func (a *fakeDoubaoAssetAdapter) Name() string {
return "fake_asset"
}

func (a *fakeDoubaoAssetAdapter) Supports(operation service.AssetOperation) bool {
return true
}

func (a *fakeDoubaoAssetAdapter) DoAssetRequest(_ context.Context, _ *model.Channel, req service.AssetRequest) (*service.AssetUpstreamResponse, *service.AssetError) {
a.operation = req.Action.Operation
a.requests = append(a.requests, req)
a.calls++
if req.Action.Operation == service.AssetOperationAssetGroupCreate {
body, err := service.BuildAssetSuccessResponse(req.Action.Action, req.Version, map[string]any{"GroupId": "generated-group"})
if err != nil {
return nil, &service.AssetError{Type: service.AssetErrorServer, Message: err.Error(), HTTPStatus: http.StatusInternalServerError}
}
return &service.AssetUpstreamResponse{StatusCode: http.StatusOK, Header: a.responseHeader, Body: body}, nil
}
if req.Action.Operation == service.AssetOperationAssetGet {
body, err := service.BuildAssetSuccessResponse(req.Action.Action, req.Version, map[string]any{"Id": "asset-1", "GroupId": a.getAssetGroupID})
if err != nil {
return nil, &service.AssetError{Type: service.AssetErrorServer, Message: err.Error(), HTTPStatus: http.StatusInternalServerError}
}
return &service.AssetUpstreamResponse{StatusCode: http.StatusOK, Header: a.responseHeader, Body: body}, nil
}
body, err := service.BuildAssetSuccessResponse(req.Action.Action, req.Version, map[string]any{
"Items": []map[string]any{
{"Id": "group-1", "Name": "g", "GroupType": "AIGC"},
},
"TotalCount": 1,
})
if err != nil {
return nil, &service.AssetError{Type: service.AssetErrorServer, Message: err.Error(), HTTPStatus: http.StatusInternalServerError}
}
return &service.AssetUpstreamResponse{StatusCode: http.StatusOK, Header: a.responseHeader, Body: body}, nil
}

+ 89
- 0
docs/superpowers/specs/2026-07-17-chinamobile-asset-isolation-design.md Просмотреть файл

@@ -0,0 +1,89 @@
# China Mobile Asset Library Per-User Isolation

## Goal

Give every platform user an isolated China Mobile AIGC asset group for each
resolved China Mobile channel. The platform creates and manages the group
internally. A user can only create, list, read, update, and delete assets in
that group.

## Scope

The change applies only after the asset proxy resolves a China Mobile asset
channel. Other asset adapters retain their existing action support and request
behavior.

Historical China Mobile groups and assets are not migrated. A user receives a
new group on their first asset request after rollout.

## Data Model

Add a persistent binding table with these fields:

- `user_id`: platform user ID.
- `channel_id`: resolved China Mobile channel ID.
- `group_id`: upstream China Mobile asset group ID.
- timestamps managed through the existing GORM conventions.

`(user_id, channel_id)` is unique. The group identity is deliberately scoped
to a channel because channels can represent different China Mobile credentials
or resource pools.

## Group Lifecycle

The asset proxy resolves the user and channel before dispatching a China Mobile
asset request. It calls a dedicated service to obtain the user group binding.

If no binding exists, the service creates an `AIGC` group through the internal
China Mobile adapter path, persists the returned group ID, and returns it. A
database uniqueness constraint handles concurrent first requests: after a
unique-conflict result, the service reads and returns the winning binding.

If upstream creation or binding persistence fails, the asset request fails. It
must never fall back to the shared upstream library. An orphaned upstream group
may remain when persistence fails after creation; it is inaccessible through
the platform and does not weaken isolation.

## Request Authorization

Clients are forbidden from calling all group actions:

- `CreateAssetGroup`
- `ListAssetGroups`
- `GetAssetGroup`
- `UpdateAssetGroup`
- `DeleteAssetGroup`

The proxy returns HTTP 403 before dispatching these actions for a resolved
China Mobile channel. Internal group creation bypasses this public-action gate.

For permitted asset actions, the proxy injects the bound group ID and does not
trust user-supplied group restrictions:

- `CreateAsset`: overwrite `GroupId`.
- `ListAssets`: overwrite `Filter.GroupIds` with the bound group ID.
- `GetAsset`, `UpdateAsset`, and `DeleteAsset`: retrieve the asset first and
verify its upstream `GroupId` equals the bound group ID. Treat a mismatch or
missing asset as not found, without exposing another user's asset details.

## Error Behavior

Group provisioning failures and binding-store failures stop the request and
return an explicit service or upstream error. Authorization failures for group
actions return 403. Cross-user asset IDs return the same not-found outcome as
an absent asset ID.

## Testing

Add focused controller and service tests for:

- initial group creation and reuse for the same user and channel;
- separate bindings for distinct users or channels;
- concurrent initial requests converging on one persisted binding;
- client-supplied `GroupId` and `Filter.GroupIds` being overwritten;
- cross-user get, update, and delete attempts returning not found;
- all five public group actions returning 403 only for China Mobile;
- regression coverage confirming non-China-Mobile adapters preserve current
group-action behavior.

Run focused controller/service tests, then the relevant package test suite.

+ 13
- 1
go.mod Просмотреть файл

@@ -50,6 +50,8 @@ require (
github.com/tidwall/sjson v1.2.5
github.com/tiktoken-go/tokenizer v0.6.2
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.6
gitlab.ecloud.com/ecloud/ecloudsdkmaas v1.0.2
golang.org/x/crypto v0.48.0
golang.org/x/image v0.23.0
golang.org/x/net v0.49.0
@@ -60,6 +62,7 @@ require (
gorm.io/driver/mysql v1.4.3
gorm.io/driver/postgres v1.5.2
gorm.io/gorm v1.25.2
maas_seedance_sdk_1.0.0_go v1.0.0
)

require (
@@ -110,6 +113,7 @@ require (
github.com/jfreymuth/vorbis v1.0.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
@@ -123,6 +127,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/openai/openai-go/v3 v3.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
@@ -131,18 +136,25 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/samber/go-singleflightx v0.3.2 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/volcengine/volc-sdk-golang v1.0.23 // indirect
github.com/volcengine/volcengine-go-sdk v1.2.10 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.40.1 // indirect
)

replace maas_seedance_sdk_1.0.0_go => ./third_party/maas_seedance_sdk_1.0.0_go
replace gitlab.ecloud.com/ecloud/ecloudsdkcore => ./third_party/ecloudsdkcore
replace gitlab.ecloud.com/ecloud/ecloudsdkmaas => ./third_party/ecloudsdkmaas

+ 80
- 1
go.sum Просмотреть файл

@@ -1,3 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
@@ -12,6 +14,7 @@ github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+Kc
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg=
@@ -37,8 +40,10 @@ github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
@@ -53,6 +58,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
@@ -133,10 +140,26 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -147,6 +170,7 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
@@ -184,6 +208,10 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -194,6 +222,7 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -242,6 +271,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/openai/openai-go/v3 v3.1.0 h1:sBf6OYL6Pj1qMAkQEmkz8r8z+EBes+iI7gCuCgr8e/A=
github.com/openai/openai-go/v3 v3.1.0/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
@@ -257,6 +288,7 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
@@ -286,6 +318,7 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -307,8 +340,9 @@ github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tiktoken-go/tokenizer v0.6.2 h1:t0GN2DvcUZSFWT/62YOgoqb10y7gSXBGs0A+4VCQK+g=
@@ -325,6 +359,10 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
github.com/volcengine/volcengine-go-sdk v1.2.10 h1:C227zcVFwQaM0yOpCANP1vGWZbW5ndpnugah6e8czC8=
github.com/volcengine/volcengine-go-sdk v1.2.10/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -334,6 +372,10 @@ github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c/go.mod h1:WSZ59b
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.6 h1:b7SFKIjaJBmkXEnih7ArHz3jCff/k56/vOHgP4KeZAI=
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.6/go.mod h1:Reukq+A2PUdVHnAQv5oXCJT8nm0Ig1ZO60VfR6bNawM=
gitlab.ecloud.com/ecloud/ecloudsdkmaas v1.0.2 h1:JPmskyGB98qDeWKLrca7ngT7FO6yTiL220KRES5k2/4=
gitlab.ecloud.com/ecloud/ecloudsdkmaas v1.0.2/go.mod h1:UbEEVYgaAUPypTX4Ug721k0scLXZXG8HaaFnDgh2NLI=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -348,10 +390,14 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -359,6 +405,10 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -370,6 +420,9 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -379,6 +432,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -421,6 +475,10 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
@@ -430,12 +488,31 @@ golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -458,6 +535,8 @@ gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBp
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=


+ 3
- 1
model/main.go Просмотреть файл

@@ -207,7 +207,7 @@ func InitDB() (err error) {
return err
}
LoadEmailQuotaCache()
return nil
return nil
} else {
common.FatalLog(err)
}
@@ -287,6 +287,7 @@ func migrateDB() error {
&EmailQuotaRule{},
&UserModelRateLimit{},
&UserAssetChannel{},
&UserAssetGroup{},
&UserMigrationBatch{},
&UserMigrationItem{},
&MigrationQuotaGrant{},
@@ -355,6 +356,7 @@ func migrateDBFast() error {
{&QuotaSyncLog{}, "QuotaSyncLog"},
{&EmailQuotaRule{}, "EmailQuotaRule"},
{&UserAssetChannel{}, "UserAssetChannel"},
{&UserAssetGroup{}, "UserAssetGroup"},
{&UserMigrationBatch{}, "UserMigrationBatch"},
{&UserMigrationItem{}, "UserMigrationItem"},
{&MigrationQuotaGrant{}, "MigrationQuotaGrant"},


+ 8
- 0
model/user_asset_channel.go Просмотреть файл

@@ -73,6 +73,14 @@ func GetUserAssetChannelsByTypes(userId int, channelTypes []int, group string) (
return bindings, err
}

func GetUserAssetChannels(userId int, group string) ([]UserAssetChannel, error) {
var bindings []UserAssetChannel
err := DB.Where("user_id = ? AND "+commonGroupCol+" = ?", userId, group).
Order("updated_at DESC, id DESC").
Find(&bindings).Error
return bindings, err
}

func DeleteUserAssetChannelsByTypesWithTx(tx *gorm.DB, userId int, channelTypes []int, group string) error {
if len(channelTypes) == 0 {
return nil


+ 20
- 0
model/user_asset_channel_test.go Просмотреть файл

@@ -92,6 +92,26 @@ func TestGetUserAssetChannelsByTypesSortsLatestFirst(t *testing.T) {
assert.Equal(t, 2, bindings[1].ChannelType)
}

func TestGetUserAssetChannelsReturnsAllTypesSortedLatestFirst(t *testing.T) {
db := setupUserAssetChannelDB(t)
require.NoError(t, BindUserAssetChannel(1, 2, "default", 100))
require.NoError(t, BindUserAssetChannel(1, 3, "default", 200))
require.NoError(t, BindUserAssetChannel(1, 4, "vip", 300))
require.NoError(t, db.Model(&UserAssetChannel{}).
Where("user_id = ? AND channel_type = ?", 1, 2).
Update("updated_at", int64(100)).Error)
require.NoError(t, db.Model(&UserAssetChannel{}).
Where("user_id = ? AND channel_type = ?", 1, 3).
Update("updated_at", int64(200)).Error)

bindings, err := GetUserAssetChannels(1, "default")

require.NoError(t, err)
require.Len(t, bindings, 2)
assert.Equal(t, 3, bindings[0].ChannelType)
assert.Equal(t, 2, bindings[1].ChannelType)
}

func TestBindUserAssetChannelWithTxUpserts(t *testing.T) {
db := setupUserAssetChannelDB(t)



+ 45
- 0
model/user_asset_group.go Просмотреть файл

@@ -0,0 +1,45 @@
package model

import (
"errors"

"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
)

// UserAssetGroup binds a platform user to an upstream asset group per channel.
type UserAssetGroup struct {
Id int `json:"id" gorm:"primaryKey"`
UserId int `json:"user_id" gorm:"not null;uniqueIndex:idx_user_asset_group,priority:1"`
ChannelId int `json:"channel_id" gorm:"not null;uniqueIndex:idx_user_asset_group,priority:2"`
GroupId string `json:"group_id" gorm:"type:varchar(255);not null"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}

func (UserAssetGroup) TableName() string {
return "user_asset_groups"
}

func GetUserAssetGroup(userId int, channelId int) (*UserAssetGroup, error) {
var binding UserAssetGroup
err := DB.Where("user_id = ? AND channel_id = ?", userId, channelId).First(&binding).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &binding, nil
}

func CreateUserAssetGroup(userId int, channelId int, groupId string) error {
now := common.GetTimestamp()
return DB.Create(&UserAssetGroup{
UserId: userId,
ChannelId: channelId,
GroupId: groupId,
CreatedAt: now,
UpdatedAt: now,
}).Error
}

+ 63
- 0
model/user_asset_group_test.go Просмотреть файл

@@ -0,0 +1,63 @@
package model

import (
"testing"

"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

func setupUserAssetGroupDB(t *testing.T) *gorm.DB {
t.Helper()

db, err := gorm.Open(sqlite.Open("file:user_asset_groups?mode=memory&cache=shared"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
require.NoError(t, err)

sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)

origDB := DB
DB = db
require.NoError(t, db.AutoMigrate(&UserAssetGroup{}))

t.Cleanup(func() {
DB = origDB
require.NoError(t, sqlDB.Close())
})

return db
}

func TestUserAssetGroupScopesByUserAndChannel(t *testing.T) {
setupUserAssetGroupDB(t)

require.NoError(t, CreateUserAssetGroup(10, 101, "group-a"))
require.NoError(t, CreateUserAssetGroup(11, 101, "group-b"))
require.NoError(t, CreateUserAssetGroup(10, 102, "group-c"))

binding, err := GetUserAssetGroup(10, 101)
require.NoError(t, err)
require.NotNil(t, binding)
assert.Equal(t, "group-a", binding.GroupId)
}

func TestUserAssetGroupRejectsDuplicateUserChannel(t *testing.T) {
setupUserAssetGroupDB(t)

require.NoError(t, CreateUserAssetGroup(10, 101, "first"))
assert.Error(t, CreateUserAssetGroup(10, 101, "second"))
}

func TestGetUserAssetGroupReturnsNilWhenNotFound(t *testing.T) {
setupUserAssetGroupDB(t)

binding, err := GetUserAssetGroup(10, 101)
require.NoError(t, err)
assert.Nil(t, binding)
}

+ 494
- 0
relay/channel/task/chinamobile_seedance/adaptor.go Просмотреть файл

@@ -0,0 +1,494 @@
package chinamobile_seedance

import (
"bytes"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
taskcommon "github.com/QuantumNous/new-api/relay/channel/task/taskcommon"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/service"
relaytypes "github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)

type responsePayload struct {
ID string `json:"id"`
}

type upstreamError struct {
Code string `json:"code"`
Message string `json:"message"`
}

func (e *upstreamError) UnmarshalJSON(data []byte) error {
if len(bytes.TrimSpace(data)) == 0 || string(bytes.TrimSpace(data)) == "null" {
return nil
}
var message string
if err := common.Unmarshal(data, &message); err == nil {
e.Message = message
return nil
}
type alias upstreamError
var parsed alias
if err := common.Unmarshal(data, &parsed); err != nil {
return err
}
*e = upstreamError(parsed)
return nil
}

type responseTask struct {
ID string `json:"id"`
Model string `json:"model"`
Status string `json:"status"`
Content struct {
VideoURL string `json:"video_url"`
} `json:"content"`
Usage struct {
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
Error upstreamError `json:"error"`
Message string `json:"message"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}

type TaskAdaptor struct {
taskcommon.BaseBilling
ChannelType int
apiKey string
baseURL string
newSDK seedanceSDKFactory
submitTimeout time.Duration
queryTimeout time.Duration
}

func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
if info != nil && info.ChannelMeta != nil {
a.ChannelType = info.ChannelType
a.baseURL = strings.TrimRight(info.ChannelBaseUrl, "/")
a.apiKey = info.ApiKey
}
if a.newSDK == nil {
a.newSDK = newRealSDK
}
if a.submitTimeout == 0 {
a.submitTimeout = defaultSubmitTimeout
}
if a.queryTimeout == 0 {
a.queryTimeout = defaultQueryTimeout
}
}

func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) {
if _, err := relaycommon.GetTaskRequest(c); err == nil {
info.Action = constant.TaskActionGenerate
return nil
}
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
}

func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
baseURL := a.baseURL
if strings.TrimSpace(baseURL) == "" && info != nil {
baseURL = strings.TrimRight(info.ChannelBaseUrl, "/")
}
if strings.TrimSpace(baseURL) == "" {
baseURL = defaultBaseURL
}
return fmt.Sprintf("%s/contents/generations/tasks", strings.TrimRight(baseURL, "/")), nil
}

func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
return nil
}

func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
req, err := relaycommon.GetTaskRequest(c)
if err != nil {
return nil, err
}
body, err := a.convertToRequestPayload(&req)
if err != nil {
return nil, errors.Wrap(err, "convert request payload failed")
}
if info != nil {
if info.IsModelMapped {
body["model"] = info.UpstreamModelName
} else if modelName, _ := body["model"].(string); modelName != "" {
info.UpstreamModelName = modelName
}
}
data, err := common.Marshal(body)
if err != nil {
return nil, err
}
return bytes.NewReader(data), nil
}

func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
data, err := io.ReadAll(requestBody)
if err != nil {
return nil, err
}
payload := map[string]interface{}{}
if err := common.Unmarshal(data, &payload); err != nil {
return nil, err
}
modelName := chinaMobileSeedanceModel(modelFromPayloadOrInfo(payload, info))
payload["model"] = modelName
if info != nil {
info.UpstreamModelName = modelName
}
client, err := a.createSDK(info, modelName)
if err != nil {
return nil, err
}
submitTimeout := a.submitTimeout
if submitTimeout == 0 {
submitTimeout = defaultSubmitTimeout
}
taskID, err := runSDKCall[string](submitTimeout, func() (string, error) {
return client.CreateVideoGenerationTask(payload)
})
if err != nil {
return chinaMobileSeedanceErrorResponse(err), nil
}
respBody, err := common.Marshal(map[string]any{"id": taskID})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader(respBody)),
}, nil
}

func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) {
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
}
_ = resp.Body.Close()

var dResp responsePayload
if err := common.Unmarshal(responseBody, &dResp); err != nil {
return "", nil, service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError)
}
if strings.TrimSpace(dResp.ID) == "" {
return "", nil, service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError)
}

clientPayload := map[string]any{}
if err := common.Unmarshal(responseBody, &clientPayload); err != nil {
return "", nil, service.TaskErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
}
clientPayload = relaytypes.CloneMapAny(clientPayload)
if info.PublicTaskID != "" {
clientPayload["id"] = info.PublicTaskID
}
if _, ok := clientPayload["created_at"]; !ok {
clientPayload["created_at"] = time.Now().Unix()
}
if _, ok := clientPayload["model"]; !ok {
clientPayload["model"] = info.OriginModelName
}
c.JSON(http.StatusOK, clientPayload)
return dResp.ID, responseBody, nil
}

func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any, proxy string) (*http.Response, error) {
taskID, _ := body["task_id"].(string)
if strings.TrimSpace(taskID) == "" {
return nil, fmt.Errorf("invalid task_id")
}
modelName, _ := body["model"].(string)
client, err := a.createSDKFromValues(baseUrl, key, modelName)
if err != nil {
return nil, err
}
queryTimeout := a.queryTimeout
if queryTimeout == 0 {
queryTimeout = defaultQueryTimeout
}
result, err := runSDKCall[map[string]interface{}](queryTimeout, func() (map[string]interface{}, error) {
return client.QueryVideoGenerationTask(taskID)
})
if err != nil {
return nil, err
}
respBody, err := common.Marshal(result)
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader(respBody)),
}, nil
}

func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) {
resTask := responseTask{}
if err := common.Unmarshal(respBody, &resTask); err != nil {
return nil, errors.Wrap(err, "unmarshal task result failed")
}

taskResult := relaycommon.TaskInfo{Code: 0}
switch strings.ToLower(resTask.Status) {
case "pending", "queued":
taskResult.Status = model.TaskStatusQueued
taskResult.Progress = "10%"
case "processing", "running":
taskResult.Status = model.TaskStatusInProgress
taskResult.Progress = "50%"
case "succeeded", "success":
taskResult.Status = model.TaskStatusSuccess
taskResult.Progress = "100%"
taskResult.Url = resTask.Content.VideoURL
taskResult.CompletionTokens = resTask.Usage.CompletionTokens
taskResult.TotalTokens = resTask.Usage.TotalTokens
case "failed", "expired", "cancelled":
taskResult.Status = model.TaskStatusFailure
taskResult.Progress = "100%"
taskResult.Reason = upstreamReason(resTask)
default:
if resTask.Error.Message != "" || resTask.Message != "" {
taskResult.Status = model.TaskStatusFailure
taskResult.Progress = "100%"
taskResult.Reason = upstreamReason(resTask)
} else {
taskResult.Status = model.TaskStatusInProgress
taskResult.Progress = "30%"
}
}
return &taskResult, nil
}

func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) {
var dResp responseTask
if err := common.Unmarshal(originTask.Data, &dResp); err != nil {
return nil, errors.Wrap(err, "unmarshal chinamobile seedance task data failed")
}

openAIVideo := dto.NewOpenAIVideo()
openAIVideo.ID = originTask.TaskID
openAIVideo.TaskID = originTask.TaskID
openAIVideo.Status = originTask.Status.ToVideoStatus()
openAIVideo.SetProgressStr(originTask.Progress)
openAIVideo.SetMetadata("url", dResp.Content.VideoURL)
openAIVideo.CreatedAt = originTask.CreatedAt
openAIVideo.CompletedAt = originTask.UpdatedAt
openAIVideo.Model = originTask.Properties.OriginModelName

if originTask.Status == model.TaskStatusFailure || dResp.Status == "failed" {
message := upstreamReason(dResp)
if message == "" {
message = "task failed"
}
code := dResp.Error.Code
if code == "" {
code = "failed"
}
openAIVideo.Error = &dto.OpenAIVideoError{Message: message, Code: code}
}
return common.Marshal(openAIVideo)
}

func (a *TaskAdaptor) GetModelList() []string {
return ModelList
}

func (a *TaskAdaptor) GetChannelName() string {
return ChannelName
}

func (a *TaskAdaptor) createSDK(info *relaycommon.RelayInfo, modelName string) (seedanceSDK, error) {
baseURL := a.baseURL
apiKey := a.apiKey
if info != nil && info.ChannelMeta != nil {
baseURL = info.ChannelBaseUrl
apiKey = info.ApiKey
}
return a.createSDKFromValues(baseURL, apiKey, modelName)
}

func (a *TaskAdaptor) createSDKFromValues(baseURL, apiKey, modelName string) (seedanceSDK, error) {
factory := a.newSDK
if factory == nil {
factory = newRealSDK
}
return factory(strings.TrimRight(baseURL, "/"), apiKey, modelName)
}

func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (map[string]interface{}, error) {
payload := map[string]interface{}{
"model": req.Model,
}
for k, v := range req.Metadata {
payload[k] = v
}
if sec, _ := strconv.Atoi(req.Seconds); sec > 0 {
if _, ok := payload["duration"]; !ok {
payload["duration"] = sec
}
}
content, err := normalizeContent(payload["content"])
if err != nil {
return nil, err
}
if len(content) == 0 && req.HasImage() {
for _, imgURL := range req.Images {
content = append(content, map[string]interface{}{
"type": "image_url",
"image_url": map[string]interface{}{"url": imgURL},
})
}
}
if strings.TrimSpace(req.Prompt) != "" && !hasTextContent(content) {
content = append(content, map[string]interface{}{
"type": "text",
"text": req.Prompt,
})
}
if len(content) > 0 {
payload["content"] = content
}
return payload, nil
}

func normalizeContent(input any) ([]interface{}, error) {
if input == nil {
return nil, nil
}
data, err := common.Marshal(input)
if err != nil {
return nil, err
}
var content []interface{}
if err := common.Unmarshal(data, &content); err != nil {
return nil, err
}
return content, nil
}

func hasTextContent(content []interface{}) bool {
for _, item := range content {
m, ok := item.(map[string]interface{})
if !ok {
continue
}
if m["type"] == "text" && strings.TrimSpace(fmt.Sprint(m["text"])) != "" {
return true
}
}
return false
}

func modelFromPayloadOrInfo(payload map[string]interface{}, info *relaycommon.RelayInfo) string {
if modelName, _ := payload["model"].(string); strings.TrimSpace(modelName) != "" {
return modelName
}
if info != nil {
if strings.TrimSpace(info.UpstreamModelName) != "" {
return info.UpstreamModelName
}
return info.OriginModelName
}
return defaultModel
}

func chinaMobileSeedanceModel(modelName string) string {
switch strings.TrimSpace(modelName) {
case "", "doubao-seedance-2-0-260128", "doubao-seedance-2-0-fast-260128":
return defaultModel
default:
return modelName
}
}

type chinaMobileSeedanceUpstreamError struct {
ErrorCode string `json:"ErrorCode"`
ErrorMessage string `json:"ErrorMessage"`
}

func chinaMobileSeedanceErrorResponse(err error) *http.Response {
statusCode := http.StatusBadGateway
body := map[string]any{
"code": "upstream_error",
"message": err.Error(),
}
if upstreamErr, ok := parseChinaMobileSeedanceError(err); ok {
statusCode = chinaMobileSeedanceStatusCode(upstreamErr.ErrorCode)
body["code"] = upstreamErr.ErrorCode
body["message"] = upstreamErr.ErrorMessage
}
respBody, marshalErr := common.Marshal(body)
if marshalErr != nil {
respBody = []byte(`{"code":"upstream_error","message":"upstream error"}`)
}
return &http.Response{
StatusCode: statusCode,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader(respBody)),
}
}

func parseChinaMobileSeedanceError(err error) (chinaMobileSeedanceUpstreamError, bool) {
if err == nil {
return chinaMobileSeedanceUpstreamError{}, false
}
text := err.Error()
start := strings.Index(text, "{")
end := strings.LastIndex(text, "}")
if start < 0 || end < start {
return chinaMobileSeedanceUpstreamError{}, false
}
var upstreamErr chinaMobileSeedanceUpstreamError
if err := common.Unmarshal([]byte(text[start:end+1]), &upstreamErr); err != nil {
return chinaMobileSeedanceUpstreamError{}, false
}
return upstreamErr, upstreamErr.ErrorCode != "" || upstreamErr.ErrorMessage != ""
}

func chinaMobileSeedanceStatusCode(code string) int {
upperCode := strings.ToUpper(strings.TrimSpace(code))
switch {
case strings.Contains(upperCode, "PERMISSION") || strings.Contains(upperCode, "UNAUTHORIZED"):
return http.StatusForbidden
case strings.Contains(upperCode, "SENSITIVE") || strings.Contains(upperCode, "INVALID") || strings.Contains(upperCode, "BAD"):
return http.StatusBadRequest
case strings.Contains(upperCode, "RATE") || strings.Contains(upperCode, "LIMIT"):
return http.StatusTooManyRequests
default:
return http.StatusBadGateway
}
}

func upstreamReason(resTask responseTask) string {
if resTask.Error.Message != "" {
return resTask.Error.Message
}
if resTask.Message != "" {
return resTask.Message
}
if resTask.Status != "" {
return "task " + resTask.Status
}
return "upstream error"
}

+ 349
- 0
relay/channel/task/chinamobile_seedance/adaptor_test.go Просмотреть файл

@@ -0,0 +1,349 @@
package chinamobile_seedance

import (
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func TestRunSDKCallRecoversPanic(t *testing.T) {
_, err := runSDKCall[string](time.Second, func() (string, error) {
panic("attestation failed")
})
require.Error(t, err)
require.Contains(t, err.Error(), "upstream_sdk_panic")
}

func TestRunSDKCallTimesOut(t *testing.T) {
start := time.Now()
_, err := runSDKCall[string](20*time.Millisecond, func() (string, error) {
time.Sleep(200 * time.Millisecond)
return "late", nil
})
require.Error(t, err)
require.Contains(t, err.Error(), "upstream_timeout")
require.Less(t, time.Since(start), 150*time.Millisecond)
}

func TestRunSDKCallTimeoutDoesNotPermanentlyOccupySemaphore(t *testing.T) {
original := sdkSemaphore
sdkSemaphore = make(chan struct{}, 1)
t.Cleanup(func() {
sdkSemaphore = original
})

block := make(chan struct{})
_, err := runSDKCall[string](20*time.Millisecond, func() (string, error) {
<-block
return "late", nil
})
require.ErrorContains(t, err, "upstream_timeout")

_, err = runSDKCall[string](time.Second, func() (string, error) {
return "ok", nil
})
require.NoError(t, err)

close(block)
}

func TestRunSDKCallReturnsError(t *testing.T) {
_, err := runSDKCall[string](time.Second, func() (string, error) {
return "", errors.New("upstream rejected")
})
require.ErrorContains(t, err, "upstream rejected")
}

type fakeSDK struct {
createInput map[string]interface{}
createID string
queryTaskID string
queryResult map[string]interface{}
err error
}

func (f *fakeSDK) CreateVideoGenerationTask(data map[string]interface{}) (string, error) {
f.createInput = data
if f.err != nil {
return "", f.err
}
return f.createID, nil
}

func (f *fakeSDK) QueryVideoGenerationTask(taskID string) (map[string]interface{}, error) {
f.queryTaskID = taskID
if f.err != nil {
return nil, f.err
}
return f.queryResult, nil
}

func TestBuildRequestBodyNormalizesContentToInterfaceSlice(t *testing.T) {
adaptor := &TaskAdaptor{}
adaptor.Init(&relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: constant.ChannelTypeChinaMobileSeedance},
})
c := newTaskRequestContext(t, `{
"model":"doubao-seedance-2.0",
"prompt":"current prompt",
"metadata":{
"content":[
{"type":"video_url","video_url":{"url":"https://example.test/input.mp4"},"role":"reference_video"}
],
"duration":5
}
}`)
info := &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{UpstreamModelName: "doubao-seedance-2.0"},
}

body, err := adaptor.BuildRequestBody(c, info)
require.NoError(t, err)
data, err := io.ReadAll(body)
require.NoError(t, err)

var payload map[string]interface{}
require.NoError(t, common.Unmarshal(data, &payload))
content, ok := payload["content"].([]interface{})
require.True(t, ok)
require.Len(t, content, 2)
require.Equal(t, "video_url", content[0].(map[string]interface{})["type"])
require.Equal(t, "text", content[1].(map[string]interface{})["type"])
require.Equal(t, "current prompt", content[1].(map[string]interface{})["text"])
require.Equal(t, float64(5), payload["duration"])
}

func TestDoRequestCallsSDKAndReturnsSyntheticResponse(t *testing.T) {
fake := &fakeSDK{createID: "upstream-task-1"}
adaptor := &TaskAdaptor{
newSDK: func(baseURL, apiKey, model string) (seedanceSDK, error) {
require.Equal(t, "https://cm.example.com/api/v3", baseURL)
require.Equal(t, "sk-test", apiKey)
require.Equal(t, "doubao-seedance-2.0", model)
return fake, nil
},
}
adaptor.Init(&relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{
ChannelType: constant.ChannelTypeChinaMobileSeedance,
ChannelBaseUrl: "https://cm.example.com/api/v3",
ApiKey: "sk-test",
},
OriginModelName: "doubao-seedance-2.0",
})
payload := strings.NewReader(`{"model":"doubao-seedance-2.0","content":[{"type":"text","text":"hello"}]}`)

resp, err := adaptor.DoRequest(&gin.Context{}, &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{
ChannelType: constant.ChannelTypeChinaMobileSeedance,
ChannelBaseUrl: "https://cm.example.com/api/v3",
ApiKey: "sk-test",
},
OriginModelName: "doubao-seedance-2.0",
}, payload)

require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
data, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.JSONEq(t, `{"id":"upstream-task-1"}`, string(data))
require.Equal(t, "doubao-seedance-2.0", fake.createInput["model"])
}

func TestDoRequestMapsOfficialSeedanceModelToChinaMobileDefault(t *testing.T) {
fake := &fakeSDK{createID: "upstream-task-1"}
adaptor := &TaskAdaptor{
newSDK: func(baseURL, apiKey, model string) (seedanceSDK, error) {
require.Equal(t, "doubao-seedance-2.0", model)
return fake, nil
},
}
payload := strings.NewReader(`{"model":"doubao-seedance-2-0-260128","content":[{"type":"text","text":"hello"}]}`)

resp, err := adaptor.DoRequest(&gin.Context{}, &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: constant.ChannelTypeChinaMobileSeedance},
OriginModelName: "doubao-seedance-2-0-260128",
}, payload)

require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "doubao-seedance-2.0", fake.createInput["model"])
}

func TestDoRequestMapsPermissionErrorToForbiddenResponse(t *testing.T) {
adaptor := &TaskAdaptor{
newSDK: func(baseURL, apiKey, model string) (seedanceSDK, error) {
return &fakeSDK{err: errors.New(`Failed to create video generation task: {"ErrorCode":"PERMISSION_ERROR","ErrorMessage":"Endpoint is not authorized"}`)}, nil
},
}
payload := strings.NewReader(`{"model":"doubao-seedance-2.0","content":[{"type":"text","text":"hello"}]}`)

resp, err := adaptor.DoRequest(&gin.Context{}, &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: constant.ChannelTypeChinaMobileSeedance},
OriginModelName: "doubao-seedance-2.0",
}, payload)

require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode)
data, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(data), "PERMISSION_ERROR")
require.Contains(t, string(data), "Endpoint is not authorized")
}

func TestDoRequestMapsSensitiveContentErrorToBadRequestResponse(t *testing.T) {
adaptor := &TaskAdaptor{
newSDK: func(baseURL, apiKey, model string) (seedanceSDK, error) {
return &fakeSDK{err: errors.New(`Failed to create video generation task: {"ErrorCode":"InputVideoSensitiveContentDetected.PrivacyInformation","ErrorMessage":"input video may contain real person"}`)}, nil
},
}
payload := strings.NewReader(`{"model":"doubao-seedance-2.0","content":[{"type":"text","text":"hello"}]}`)

resp, err := adaptor.DoRequest(&gin.Context{}, &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: constant.ChannelTypeChinaMobileSeedance},
OriginModelName: "doubao-seedance-2.0",
}, payload)

require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
data, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(data), "InputVideoSensitiveContentDetected.PrivacyInformation")
}

func TestFetchTaskCallsSDKAndReturnsQueryMap(t *testing.T) {
fake := &fakeSDK{queryResult: map[string]interface{}{
"id": "upstream-task-1",
"status": "succeeded",
"content": map[string]interface{}{
"video_url": "https://example.test/video.mp4",
},
}}
adaptor := &TaskAdaptor{
newSDK: func(baseURL, apiKey, model string) (seedanceSDK, error) {
require.Equal(t, "https://cm.example.com/api/v3", baseURL)
require.Equal(t, "sk-test", apiKey)
require.Equal(t, "doubao-seedance-2.0", model)
return fake, nil
},
}
adaptor.Init(&relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ChannelType: constant.ChannelTypeChinaMobileSeedance}})

resp, err := adaptor.FetchTask("https://cm.example.com/api/v3", "sk-test", map[string]any{
"task_id": "upstream-task-1",
"model": "doubao-seedance-2.0",
}, "")

require.NoError(t, err)
require.Equal(t, "upstream-task-1", fake.queryTaskID)
data, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(data), `"video_url":"https://example.test/video.mp4"`)
}

func TestFetchTaskWithoutInitUsesDefaultTimeout(t *testing.T) {
fake := &fakeSDK{queryResult: map[string]interface{}{
"id": "upstream-task-1",
"status": "queued",
}}
adaptor := &TaskAdaptor{
newSDK: func(baseURL, apiKey, model string) (seedanceSDK, error) {
return fake, nil
},
}

resp, err := adaptor.FetchTask("https://cm.example.com/api/v3", "sk-test", map[string]any{
"task_id": "upstream-task-1",
"model": "doubao-seedance-2.0",
}, "")

require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
}

func TestDoResponseKeepsUpstreamIDWhenPublicTaskIDMissing(t *testing.T) {
adaptor := &TaskAdaptor{}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
resp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"id":"upstream-task-1"}`)),
}

upstreamID, _, taskErr := adaptor.DoResponse(c, resp, &relaycommon.RelayInfo{
OriginModelName: "doubao-seedance-2.0",
TaskRelayInfo: &relaycommon.TaskRelayInfo{},
})

require.Nil(t, taskErr)
require.Equal(t, "upstream-task-1", upstreamID)
require.Contains(t, w.Body.String(), `"id":"upstream-task-1"`)
}

func TestParseTaskResultSuccessWithVideoURLAndUsage(t *testing.T) {
taskInfo, err := (&TaskAdaptor{}).ParseTaskResult([]byte(`{
"id":"cm-task",
"status":"succeeded",
"content":{"video_url":"https://example.com/video.mp4"},
"usage":{"completion_tokens":12,"total_tokens":34}
}`))
require.NoError(t, err)
require.Equal(t, model.TaskStatusSuccess, taskInfo.Status)
require.Equal(t, "https://example.com/video.mp4", taskInfo.Url)
require.Equal(t, 12, taskInfo.CompletionTokens)
require.Equal(t, 34, taskInfo.TotalTokens)
}

func TestParseTaskResultSuccessWithoutUsage(t *testing.T) {
taskInfo, err := (&TaskAdaptor{}).ParseTaskResult([]byte(`{
"id":"cm-task",
"status":"success",
"content":{"video_url":"https://example.com/video.mp4"}
}`))
require.NoError(t, err)
require.Equal(t, model.TaskStatusSuccess, taskInfo.Status)
require.Equal(t, 0, taskInfo.CompletionTokens)
require.Equal(t, 0, taskInfo.TotalTokens)
}

func TestParseTaskResultFailureFromErrorMap(t *testing.T) {
taskInfo, err := (&TaskAdaptor{}).ParseTaskResult([]byte(`{
"error":{"code":"bad_request","message":"invalid content"}
}`))
require.NoError(t, err)
require.Equal(t, model.TaskStatusFailure, taskInfo.Status)
require.Equal(t, "invalid content", taskInfo.Reason)
}

func TestParseTaskResultFailureFromErrorString(t *testing.T) {
taskInfo, err := (&TaskAdaptor{}).ParseTaskResult([]byte(`{
"status":"failed",
"error":"quota exceeded"
}`))
require.NoError(t, err)
require.Equal(t, model.TaskStatusFailure, taskInfo.Status)
require.Equal(t, "quota exceeded", taskInfo.Reason)
}

func newTaskRequestContext(t *testing.T, body string) *gin.Context {
t.Helper()
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v3/contents/generations/tasks", strings.NewReader(body))
c.Request.Header.Set("Content-Type", "application/json")
var req relaycommon.TaskSubmitReq
require.NoError(t, common.Unmarshal([]byte(body), &req))
relaycommon.StoreTaskRequest(c, &relaycommon.RelayInfo{}, constant.TaskActionGenerate, req)
return c
}

+ 9
- 0
relay/channel/task/chinamobile_seedance/constants.go Просмотреть файл

@@ -0,0 +1,9 @@
package chinamobile_seedance

var ModelList = []string{
"doubao-seedance-2.0",
"doubao-seedance-2-0-260128",
"doubao-seedance-2-0-fast-260128",
}

var ChannelName = "ChinaMobileSeedance"

+ 81
- 0
relay/channel/task/chinamobile_seedance/sdk_client.go Просмотреть файл

@@ -0,0 +1,81 @@
package chinamobile_seedance

import (
"fmt"
"strings"
"sync"
"time"

maas "maas_seedance_sdk_1.0.0_go"
)

const (
defaultBaseURL = "https://zhenze-huhehaote.cmecloud.cn/api/v3"
defaultModel = "doubao-seedance-2.0"
defaultSubmitTimeout = 180 * time.Second
defaultQueryTimeout = 60 * time.Second
sdkAcquireTimeout = 2 * time.Second
)

var sdkSemaphore = make(chan struct{}, 4)

type seedanceSDK interface {
CreateVideoGenerationTask(data map[string]interface{}) (string, error)
QueryVideoGenerationTask(taskID string) (map[string]interface{}, error)
}

type seedanceSDKFactory func(baseURL, apiKey, model string) (seedanceSDK, error)

func newRealSDK(baseURL, apiKey, model string) (seedanceSDK, error) {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
if baseURL == "" {
baseURL = defaultBaseURL
}
model = strings.TrimSpace(model)
if model == "" {
model = defaultModel
}
return runSDKCall[seedanceSDK](defaultQueryTimeout, func() (seedanceSDK, error) {
return maas.NewMaasSeedanceClient(baseURL, apiKey, model, false)
})
}

func runSDKCall[T any](timeout time.Duration, fn func() (T, error)) (T, error) {
var zero T
select {
case sdkSemaphore <- struct{}{}:
case <-time.After(sdkAcquireTimeout):
return zero, fmt.Errorf("upstream_busy: sdk concurrency limit reached")
}

type result struct {
value T
err error
}
var releaseOnce sync.Once
release := func() {
releaseOnce.Do(func() {
<-sdkSemaphore
})
}
done := make(chan result, 1)
go func() {
res := result{}
defer func() {
if r := recover(); r != nil {
res.err = fmt.Errorf("upstream_sdk_panic: %v", r)
}
release()
done <- res
}()
res.value, res.err = fn()
}()

select {
case res := <-done:
return res.value, res.err
case <-time.After(timeout):
release()
return zero, fmt.Errorf("upstream_timeout: sdk call exceeded %s", timeout)
}
}

+ 5
- 0
relay/helper/matrix_usage_capability.go Просмотреть файл

@@ -23,6 +23,11 @@ var matrixUsageCapabilities = []MatrixUsageCapability{
{BillingModelName: "doubao-seedance-2-0-fast-260128", UpstreamModelName: "doubao-seedance-2-0-fast-260128", ChannelType: constant.ChannelTypeDoubaoVideoCompatibleAiping},
{BillingModelName: "cdance2.0-0611", UpstreamModelName: "cdance2.0-0611", ChannelType: constant.ChannelTypeDoubaoVideoCompatibleTianyiYun},
{BillingModelName: "cdance2.0-fast-0611", UpstreamModelName: "cdance2.0-fast-0611", ChannelType: constant.ChannelTypeDoubaoVideoCompatibleTianyiYun},
{BillingModelName: "doubao-seedance-2-0-260128", UpstreamModelName: "doubao-seedance-2-0-260128", ChannelType: constant.ChannelTypeChinaMobileSeedance},
{BillingModelName: "doubao-seedance-2-0-260128", UpstreamModelName: "doubao-seedance-2.0", ChannelType: constant.ChannelTypeChinaMobileSeedance},
{BillingModelName: "doubao-seedance-2-0-fast-260128", UpstreamModelName: "doubao-seedance-2-0-fast-260128", ChannelType: constant.ChannelTypeChinaMobileSeedance},
{BillingModelName: "doubao-seedance-2-0-fast-260128", UpstreamModelName: "doubao-seedance-2.0", ChannelType: constant.ChannelTypeChinaMobileSeedance},
{BillingModelName: "doubao-seedance-2.0", UpstreamModelName: "doubao-seedance-2.0", ChannelType: constant.ChannelTypeChinaMobileSeedance},
// Keep in sync with SupportsAnyMatrixUsageBillingModel in setting/ratio_setting/model_pricing.go
{BillingModelName: "kling-v1", UpstreamModelName: "kling-v1", ChannelType: constant.ChannelTypeKlingAiping},
{BillingModelName: "kling-v1", UpstreamModelName: "Kling-V1", ChannelType: constant.ChannelTypeKlingAiping},


+ 9
- 0
relay/helper/matrix_usage_capability_test.go Просмотреть файл

@@ -29,6 +29,15 @@ func TestSupportsMatrixUsageBilling_TianyiYunSeedanceChannel(t *testing.T) {
require.False(t, SupportsMatrixUsageBilling("cdance2.0-0611", constant.ChannelTypeDoubaoVideoCompatibleAiping, "cdance2.0-0611"))
}

func TestSupportsMatrixUsageBilling_ChinaMobileSeedanceChannel(t *testing.T) {
require.True(t, SupportsMatrixUsageBilling("doubao-seedance-2-0-260128", constant.ChannelTypeChinaMobileSeedance, "doubao-seedance-2-0-260128"))
require.True(t, SupportsMatrixUsageBilling("doubao-seedance-2-0-260128", constant.ChannelTypeChinaMobileSeedance, "doubao-seedance-2.0"))
require.True(t, SupportsMatrixUsageBilling("doubao-seedance-2-0-fast-260128", constant.ChannelTypeChinaMobileSeedance, "doubao-seedance-2-0-fast-260128"))
require.True(t, SupportsMatrixUsageBilling("doubao-seedance-2-0-fast-260128", constant.ChannelTypeChinaMobileSeedance, "doubao-seedance-2.0"))
require.True(t, SupportsMatrixUsageBilling("doubao-seedance-2.0", constant.ChannelTypeChinaMobileSeedance, "doubao-seedance-2.0"))
require.False(t, SupportsMatrixUsageBilling("doubao-seedance-2-0-260128", constant.ChannelTypeDoubaoVideoCompatibleTianyiYun, "doubao-seedance-2.0"))
}

func TestSupportsMatrixUsageBilling_KlingAipingVideoModels(t *testing.T) {
cases := []struct {
billingModel string


+ 3
- 0
relay/relay_adaptor.go Просмотреть файл

@@ -31,6 +31,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/siliconflow"
"github.com/QuantumNous/new-api/relay/channel/submodel"
taskali "github.com/QuantumNous/new-api/relay/channel/task/ali"
taskchinamobileseedance "github.com/QuantumNous/new-api/relay/channel/task/chinamobile_seedance"
taskdoubao "github.com/QuantumNous/new-api/relay/channel/task/doubao"
taskdoubaoaiping "github.com/QuantumNous/new-api/relay/channel/task/doubao_aiping"
taskdoubaotianyiyun "github.com/QuantumNous/new-api/relay/channel/task/doubao_tianyiyun"
@@ -160,6 +161,8 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor {
return &taskdoubaoaiping.TaskAdaptor{}
case constant.ChannelTypeDoubaoVideoCompatibleTianyiYun:
return &taskdoubaotianyiyun.TaskAdaptor{}
case constant.ChannelTypeChinaMobileSeedance:
return &taskchinamobileseedance.TaskAdaptor{}
case constant.ChannelTypeKlingAiping:
return &klingaiping.TaskAdaptor{}
case constant.ChannelTypeSora, constant.ChannelTypeOpenAI:


+ 15
- 0
relay/relay_adaptor_chinamobile_test.go Просмотреть файл

@@ -0,0 +1,15 @@
package relay

import (
"strconv"
"testing"

"github.com/QuantumNous/new-api/constant"
"github.com/stretchr/testify/require"
)

func TestGetTaskAdaptorChinaMobileSeedance(t *testing.T) {
adaptor := GetTaskAdaptor(constant.TaskPlatform(strconv.Itoa(constant.ChannelTypeChinaMobileSeedance)))
require.NotNil(t, adaptor)
require.Equal(t, "ChinaMobileSeedance", adaptor.GetChannelName())
}

+ 5
- 0
relay/relay_task.go Просмотреть файл

@@ -406,9 +406,14 @@ func tryRealtimeFetch(task *model.Task, isOpenAIVideoAPI bool) []byte {
return nil
}

fetchModel := task.Properties.UpstreamModelName
if strings.TrimSpace(fetchModel) == "" {
fetchModel = task.Properties.OriginModelName
}
resp, err := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{
"task_id": task.GetUpstreamTaskID(),
"action": task.Action,
"model": fetchModel,
}, proxy)
if err != nil || resp == nil {
return nil


+ 107
- 0
service/asset_adapter.go Просмотреть файл

@@ -0,0 +1,107 @@
package service

import (
"context"
"net/http"
"sort"
"strings"

"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
)

const (
AssetErrorInvalidRequest = "invalid_request_error"
AssetErrorChannelNotFound = "asset_channel_not_found"
AssetErrorBindingInvalid = "asset_channel_binding_invalid"
AssetErrorOperationNotSupported = "asset_operation_not_supported"
AssetErrorNotFound = "asset_not_found"
AssetErrorUpstream = "upstream_error"
AssetErrorServer = "server_error"
)

type AssetError struct {
Type string
Message string
HTTPStatus int
}

func newAssetError(errType string, message string, status int) *AssetError {
if status == 0 {
status = http.StatusBadGateway
}
return &AssetError{Type: errType, Message: message, HTTPStatus: status}
}

type AssetRequest struct {
Action AssetActionSpec
Version string
Body map[string]any
RawBody []byte
}

type AssetUpstreamResponse struct {
StatusCode int
Header http.Header
Body []byte
}

type AssetAdapter interface {
Name() string
Supports(operation AssetOperation) bool
DoAssetRequest(ctx context.Context, channel *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError)
}

var assetAdapters = map[int]AssetAdapter{}

func init() {
registerDefaultAssetAdapters()
}

func registerDefaultAssetAdapters() {
assetAdapters[constant.ChannelTypeChinaMobileSeedance] = NewChinaMobileAssetAdapter()
assetAdapters[constant.ChannelTypeDoubaoVideoCompatibleAiping] = NewCompatibleAssetAdapter("aiping_asset", []AssetOperation{
AssetOperationAssetCreate,
AssetOperationAssetList,
AssetOperationAssetGet,
AssetOperationAssetUpdate,
AssetOperationAssetDelete,
})
assetAdapters[constant.ChannelTypeDoubaoVideoCompatibleTianyiYun] = NewCompatibleAssetAdapter("tianyiyun_asset", []AssetOperation{
AssetOperationAssetCreate,
AssetOperationAssetList,
AssetOperationAssetGet,
AssetOperationAssetUpdate,
AssetOperationAssetDelete,
})
}

func GetAssetAdapter(channelType int) (AssetAdapter, bool) {
adapter, ok := assetAdapters[channelType]
return adapter, ok
}

func OverrideAssetAdapterForTest(channelType int, adapter AssetAdapter) func() {
oldAdapter, hadOldAdapter := assetAdapters[channelType]
assetAdapters[channelType] = adapter
return func() {
if hadOldAdapter {
assetAdapters[channelType] = oldAdapter
} else {
delete(assetAdapters, channelType)
}
}
}

func RegisteredAssetChannelTypes() []int {
types := make([]int, 0, len(assetAdapters))
for channelType := range assetAdapters {
types = append(types, channelType)
}
sort.Ints(types)
return types
}

func assetChannelHasKey(channel *model.Channel) bool {
return channel != nil && strings.TrimSpace(channel.Key) != "" && len(channel.GetKeys()) > 0
}

+ 28
- 0
service/asset_adapter_test.go Просмотреть файл

@@ -0,0 +1,28 @@
package service

import (
"testing"

"github.com/stretchr/testify/assert"
)

func resetAssetAdapterRegistryForTest(t interface{ Cleanup(func()) }) {
oldRegistry := assetAdapters
assetAdapters = map[int]AssetAdapter{}
t.Cleanup(func() {
assetAdapters = oldRegistry
})
}

func RegisterAssetAdapterForTest(channelType int, adapter AssetAdapter) {
assetAdapters[channelType] = adapter
}

func TestRegisteredAssetChannelTypesAreSorted(t *testing.T) {
resetAssetAdapterRegistryForTest(t)
RegisterAssetAdapterForTest(61, fakeAssetAdapter{})
RegisterAssetAdapterForTest(7, fakeAssetAdapter{})
RegisterAssetAdapterForTest(16, fakeAssetAdapter{})

assert.Equal(t, []int{7, 16, 61}, RegisteredAssetChannelTypes())
}

+ 739
- 0
service/asset_chinamobile.go Просмотреть файл

@@ -0,0 +1,739 @@
package service

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
cmerrs "gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
cmmodel "gitlab.ecloud.com/ecloud/ecloudsdkmaas/model"
)

const defaultChinaMobileAssetBaseURL = "https://ecloud.10086.cn"
const defaultChinaMobileAssetPoolID = "CIDC-CORE-00"
const chinaMobileAssetAKEnv = "CHINAMOBILE_ASSET_AK"
const chinaMobileAssetSKEnv = "CHINAMOBILE_ASSET_SK"
const chinaMobileAssetPoolIDEnv = "CHINAMOBILE_ASSET_POOL_ID"

type ChinaMobileAssetAdapter struct {
newClient func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient
}

func NewChinaMobileAssetAdapter() AssetAdapter {
return &ChinaMobileAssetAdapter{newClient: newChinaMobileAssetSDKClient}
}

func (a *ChinaMobileAssetAdapter) Name() string {
return "chinamobile_asset"
}

func (a *ChinaMobileAssetAdapter) Supports(operation AssetOperation) bool {
switch operation {
case AssetOperationAssetCreate,
AssetOperationAssetList,
AssetOperationAssetGet,
AssetOperationAssetUpdate,
AssetOperationAssetDelete,
AssetOperationAssetGroupCreate,
AssetOperationAssetGroupList,
AssetOperationAssetGroupGet,
AssetOperationAssetGroupUpdate,
AssetOperationAssetGroupDelete:
return true
default:
return false
}
}

func (a *ChinaMobileAssetAdapter) DoAssetRequest(ctx context.Context, channel *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError) {
if !a.Supports(req.Action.Operation) {
return nil, newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported", req.Action.Operation), http.StatusBadRequest)
}
if err := validateChinaMobileCompatibility(req.Body); err != nil {
return nil, err
}
if err := validateChinaMobileAssetRequest(req); err != nil {
return nil, err
}

credential, err := chinaMobileAssetCredentialFromEnv()
if err != nil {
return nil, newAssetError(AssetErrorInvalidRequest, err.Error(), http.StatusBadRequest)
}
newClient := a.newClient
if newClient == nil {
newClient = newChinaMobileAssetSDKClient
}
result, err := callChinaMobileAssetSDK(ctx, newClient(credential), req)
if err != nil {
if assetErr := classifyChinaMobileAssetSDKError(err); assetErr != nil {
return nil, assetErr
}
return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
}
normalized, assetErr := normalizeChinaMobileAssetSDKResponse(req.Action, req.Version, result)
if assetErr != nil {
return nil, assetErr
}
return &AssetUpstreamResponse{StatusCode: http.StatusOK, Header: http.Header{}, Body: normalized}, nil
}

func joinChinaMobileAssetURL(baseURL string, path string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
u.Path = strings.TrimRight(u.Path, "/") + path
u.RawQuery = ""
u.Fragment = ""
return u.String(), nil
}

type chinaMobileAssetCredential struct {
AK string `json:"ak"`
SK string `json:"sk"`
PoolID string `json:"pool_id"`
}

func chinaMobileAssetCredentialFromEnv() (chinaMobileAssetCredential, error) {
return normalizeChinaMobileAssetCredential(chinaMobileAssetCredential{
AK: os.Getenv(chinaMobileAssetAKEnv),
SK: os.Getenv(chinaMobileAssetSKEnv),
PoolID: os.Getenv(chinaMobileAssetPoolIDEnv),
})
}

func normalizeChinaMobileAssetCredential(credential chinaMobileAssetCredential) (chinaMobileAssetCredential, error) {
credential.AK = strings.TrimSpace(credential.AK)
credential.SK = strings.TrimSpace(credential.SK)
credential.PoolID = strings.TrimSpace(credential.PoolID)
if credential.AK == "" || credential.SK == "" {
return chinaMobileAssetCredential{}, fmt.Errorf("%s and %s are required for China Mobile asset library", chinaMobileAssetAKEnv, chinaMobileAssetSKEnv)
}
if credential.PoolID == "" {
credential.PoolID = defaultChinaMobileAssetPoolID
}
return credential, nil
}

func callChinaMobileAssetSDK(ctx context.Context, client chinaMobileAssetSDKClient, req AssetRequest) (any, error) {
type sdkResult struct {
value any
err error
}
resultCh := make(chan sdkResult, 1)
go func() {
value, err := executeChinaMobileAssetSDK(client, req)
resultCh <- sdkResult{value: value, err: err}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-resultCh:
return result.value, result.err
}
}

func executeChinaMobileAssetSDK(client chinaMobileAssetSDKClient, req AssetRequest) (any, error) {
switch req.Action.Operation {
case AssetOperationAssetCreate:
return client.CreateAsset(&cmmodel.CreateAssetRequest{CreateAssetBody: newChinaMobileCreateAssetBody(req.Body)})
case AssetOperationAssetList:
return client.ListAssets(&cmmodel.ListAssetsRequest{ListAssetsBody: newChinaMobileListAssetsBody(req.Body)})
case AssetOperationAssetGet:
id, err := requiredAssetString(req.Body, "Id")
if err != nil {
return nil, err
}
return client.GetAsset(&cmmodel.GetAssetRequest{GetAssetPath: (&cmmodel.GetAssetPath{}).SetAssetId(id)})
case AssetOperationAssetUpdate:
id, err := requiredAssetString(req.Body, "Id")
if err != nil {
return nil, err
}
return client.UpdateAsset(&cmmodel.UpdateAssetRequest{
UpdateAssetPath: (&cmmodel.UpdateAssetPath{}).SetAssetId(id),
UpdateAssetBody: newChinaMobileUpdateAssetBody(req.Body),
})
case AssetOperationAssetDelete:
id, err := requiredAssetString(req.Body, "Id")
if err != nil {
return nil, err
}
return client.DeleteAsset(&cmmodel.DeleteAssetRequest{DeleteAssetPath: (&cmmodel.DeleteAssetPath{}).SetAssetId(id)})
case AssetOperationAssetGroupCreate:
return client.CreateAssetGroup(&cmmodel.CreateAssetGroupRequest{CreateAssetGroupBody: newChinaMobileCreateAssetGroupBody(req.Body)})
case AssetOperationAssetGroupList:
return client.ListAssetGroups(&cmmodel.ListAssetGroupsRequest{ListAssetGroupsBody: newChinaMobileListAssetGroupsBody(req.Body)})
case AssetOperationAssetGroupGet:
id, err := requiredAssetString(req.Body, "Id")
if err != nil {
return nil, err
}
return client.GetAssetGroup(&cmmodel.GetAssetGroupRequest{GetAssetGroupPath: (&cmmodel.GetAssetGroupPath{}).SetGroupId(id)})
case AssetOperationAssetGroupUpdate:
id, err := requiredAssetString(req.Body, "Id")
if err != nil {
return nil, err
}
return client.UpdateAssetGroup(&cmmodel.UpdateAssetGroupRequest{
UpdateAssetGroupPath: (&cmmodel.UpdateAssetGroupPath{}).SetGroupId(id),
UpdateAssetGroupBody: newChinaMobileUpdateAssetGroupBody(req.Body),
})
case AssetOperationAssetGroupDelete:
id, err := requiredAssetString(req.Body, "Id")
if err != nil {
return nil, err
}
return client.DeleteAssetGroup(&cmmodel.DeleteAssetGroupRequest{DeleteAssetGroupPath: (&cmmodel.DeleteAssetGroupPath{}).SetGroupId(id)})
default:
return nil, fmt.Errorf("unsupported asset operation %s", req.Action.Operation)
}
}

func isChinaMobileAssetLocalValidationError(err error) bool {
return err != nil && strings.Contains(err.Error(), " is required")
}

func classifyChinaMobileAssetSDKError(err error) *AssetError {
if err == nil {
return nil
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return newAssetError(AssetErrorUpstream, err.Error(), http.StatusGatewayTimeout)
}
if isChinaMobileAssetLocalValidationError(err) {
return newAssetError(AssetErrorInvalidRequest, err.Error(), http.StatusBadRequest)
}
var responseErr *cmerrs.ServerResponseError
if errors.As(err, &responseErr) && responseErr.Code == http.StatusBadRequest {
message := strings.TrimSpace(responseErr.Body)
if message == "" {
message = responseErr.Error()
}
return newAssetError(AssetErrorInvalidRequest, message, http.StatusBadRequest)
}
return nil
}

func validateChinaMobileAssetRequest(req AssetRequest) *AssetError {
invalid := func(message string) *AssetError {
return newAssetError(AssetErrorInvalidRequest, message, http.StatusBadRequest)
}
requireString := func(key string) *AssetError {
if strings.TrimSpace(stringValue(req.Body, key)) == "" {
return invalid(key + " is required")
}
return nil
}
validatePage := func() *AssetError {
pageNumber := intValue(req.Body, "PageNumber", 1)
pageSize := intValue(req.Body, "PageSize", 10)
if pageNumber < 1 {
return invalid("PageNumber must be greater than or equal to 1")
}
if pageSize < 1 || pageSize > 999999 {
return invalid("PageSize must be between 1 and 999999")
}
return nil
}

switch req.Action.Operation {
case AssetOperationAssetCreate:
for _, key := range []string{"GroupId", "Name", "URL", "AssetType"} {
if err := requireString(key); err != nil {
return err
}
}
if len([]rune(stringValue(req.Body, "Name"))) > 64 {
return invalid("Name must not exceed 64 characters")
}
assetURL, err := url.ParseRequestURI(strings.TrimSpace(stringValue(req.Body, "URL")))
if err != nil || (assetURL.Scheme != "http" && assetURL.Scheme != "https") || assetURL.Host == "" {
return invalid("URL must be a valid public HTTP or HTTPS URL")
}
switch stringValue(req.Body, "AssetType") {
case "Image", "Video", "Audio":
default:
return invalid("AssetType must be one of Image, Video, Audio")
}
case AssetOperationAssetList:
if err := validatePage(); err != nil {
return err
}
groupType := strings.TrimSpace(stringValue(mapValue(req.Body, "Filter"), "GroupType"))
if groupType == "" {
return invalid("Filter.GroupType is required")
}
if groupType != "AIGC" && groupType != "LivenessFace" {
return invalid("Filter.GroupType must be AIGC or LivenessFace")
}
case AssetOperationAssetGroupCreate:
if stringValue(req.Body, "GroupType") != "AIGC" {
return invalid("GroupType must be AIGC")
}
if len([]rune(stringValue(req.Body, "Name"))) > 64 {
return invalid("Name must not exceed 64 characters")
}
if len([]rune(stringValue(req.Body, "Description"))) > 300 {
return invalid("Description must not exceed 300 characters")
}
case AssetOperationAssetGroupList:
return validatePage()
case AssetOperationAssetUpdate:
if err := requireString("Id"); err != nil {
return err
}
if len([]rune(stringValue(req.Body, "Name"))) > 64 {
return invalid("Name must not exceed 64 characters")
}
case AssetOperationAssetGroupUpdate:
if err := requireString("Id"); err != nil {
return err
}
if len([]rune(stringValue(req.Body, "Name"))) > 64 {
return invalid("Name must not exceed 64 characters")
}
if len([]rune(stringValue(req.Body, "Description"))) > 300 {
return invalid("Description must not exceed 300 characters")
}
case AssetOperationAssetGet, AssetOperationAssetDelete, AssetOperationAssetGroupGet, AssetOperationAssetGroupDelete:
return requireString("Id")
}
return nil
}

func newChinaMobileCreateAssetBody(body map[string]any) *cmmodel.CreateAssetBody {
assetType := cmmodel.CreateAssetBodyAssetTypeEnum(stringValue(body, "AssetType"))
result := &cmmodel.CreateAssetBody{}
result.SetGroupId(stringValue(body, "GroupId"))
result.SetAssetName(stringValue(body, "Name"))
result.SetAssetUrl(stringValue(body, "URL"))
if strings.TrimSpace(string(assetType)) != "" {
result.SetAssetType(assetType)
}
return result
}

func newChinaMobileListAssetsBody(body map[string]any) *cmmodel.ListAssetsBody {
mapped := mapChinaMobileListAssets(body)
result := &cmmodel.ListAssetsBody{}
result.SetPageNo(int32(intValue(mapped, "pageNo", 1)))
result.SetPageSize(int32(intValue(mapped, "pageSize", 10)))
if value := stringValue(mapped, "groupType"); value != "" {
result.SetGroupType(value)
}
if value := stringValue(mapped, "assetName"); value != "" {
result.SetAssetName(value)
}
if values := stringArrayValue(mapped, "groupIds"); len(values) > 0 {
result.SetGroupIds(values)
}
if values := stringArrayValue(mapped, "statuses"); len(values) > 0 {
result.SetStatuses(values)
}
return result
}

func newChinaMobileUpdateAssetBody(body map[string]any) *cmmodel.UpdateAssetBody {
result := &cmmodel.UpdateAssetBody{}
if value := stringValue(body, "Name"); value != "" {
result.SetAssetName(value)
}
return result
}

func newChinaMobileCreateAssetGroupBody(body map[string]any) *cmmodel.CreateAssetGroupBody {
result := &cmmodel.CreateAssetGroupBody{}
if value := stringValue(body, "GroupType"); value != "" {
result.SetGroupType(value)
}
if value := stringValue(body, "Name"); value != "" {
result.SetGroupName(value)
}
if value := stringValue(body, "Description"); value != "" {
result.SetDescription(value)
}
return result
}

func newChinaMobileListAssetGroupsBody(body map[string]any) *cmmodel.ListAssetGroupsBody {
mapped := mapChinaMobileListAssetGroups(body)
result := &cmmodel.ListAssetGroupsBody{}
result.SetPageNo(int32(intValue(mapped, "pageNo", 1)))
result.SetPageSize(int32(intValue(mapped, "pageSize", 10)))
if value := stringValue(mapped, "groupType"); value != "" {
result.SetGroupType(value)
}
if value := stringValue(mapped, "groupName"); value != "" {
result.SetGroupName(value)
}
if values := stringArrayValue(mapped, "groupIds"); len(values) > 0 {
result.SetGroupIds(values)
}
return result
}

func newChinaMobileUpdateAssetGroupBody(body map[string]any) *cmmodel.UpdateAssetGroupBody {
result := &cmmodel.UpdateAssetGroupBody{}
if value := stringValue(body, "Name"); value != "" {
result.SetGroupName(value)
}
if value := stringValue(body, "Description"); value != "" {
result.SetDescription(value)
}
return result
}

func buildChinaMobileAssetRequest(req AssetRequest) (string, string, map[string]any, error) {
switch req.Action.Operation {
case AssetOperationAssetCreate:
return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset", mapChinaMobileCreateAsset(req.Body), nil
case AssetOperationAssetList:
return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset/query", mapChinaMobileListAssets(req.Body), nil
case AssetOperationAssetGet:
id, err := requiredAssetString(req.Body, "Id")
return http.MethodGet, "/api/openapi-maas/exp/aicc/v2/asset/" + url.PathEscape(id), nil, err
case AssetOperationAssetUpdate:
id, err := requiredAssetString(req.Body, "Id")
return http.MethodPut, "/api/openapi-maas/exp/aicc/v2/asset/" + url.PathEscape(id), mapChinaMobileUpdateAsset(req.Body), err
case AssetOperationAssetDelete:
id, err := requiredAssetString(req.Body, "Id")
return http.MethodDelete, "/api/openapi-maas/exp/aicc/v2/asset/" + url.PathEscape(id), nil, err
case AssetOperationAssetGroupCreate:
return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset-group", mapChinaMobileCreateAssetGroup(req.Body), nil
case AssetOperationAssetGroupList:
return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset-group/query", mapChinaMobileListAssetGroups(req.Body), nil
case AssetOperationAssetGroupGet:
id, err := requiredAssetString(req.Body, "Id")
return http.MethodGet, "/api/openapi-maas/exp/aicc/v2/asset-group/" + url.PathEscape(id), nil, err
case AssetOperationAssetGroupUpdate:
id, err := requiredAssetString(req.Body, "Id")
return http.MethodPut, "/api/openapi-maas/exp/aicc/v2/asset-group/" + url.PathEscape(id), mapChinaMobileUpdateAssetGroup(req.Body), err
case AssetOperationAssetGroupDelete:
id, err := requiredAssetString(req.Body, "Id")
return http.MethodDelete, "/api/openapi-maas/exp/aicc/v2/asset-group/" + url.PathEscape(id), nil, err
default:
return "", "", nil, fmt.Errorf("unsupported asset operation %s", req.Action.Operation)
}
}

func mapChinaMobileCreateAsset(body map[string]any) map[string]any {
return compactAssetMap(map[string]any{
"groupId": stringValue(body, "GroupId"),
"assetName": stringValue(body, "Name"),
"assetUrl": stringValue(body, "URL"),
"assetType": stringValue(body, "AssetType"),
})
}

func mapChinaMobileListAssets(body map[string]any) map[string]any {
filter := mapValue(body, "Filter")
mapped := map[string]any{
"pageNo": intValue(body, "PageNumber", 1),
"pageSize": intValue(body, "PageSize", 10),
}
copyStringArray(mapped, "groupIds", filter, "GroupIds")
copyString(mapped, "groupType", filter, "GroupType")
copyString(mapped, "assetName", filter, "Name")
copyStatusArray(mapped, "statuses", filter, "Statuses")
return compactAssetMap(mapped)
}

func mapChinaMobileUpdateAsset(body map[string]any) map[string]any {
return compactAssetMap(map[string]any{"assetName": stringValue(body, "Name")})
}

func mapChinaMobileCreateAssetGroup(body map[string]any) map[string]any {
return compactAssetMap(map[string]any{
"groupType": stringValue(body, "GroupType"),
"groupName": stringValue(body, "Name"),
"description": stringValue(body, "Description"),
})
}

func mapChinaMobileListAssetGroups(body map[string]any) map[string]any {
filter := mapValue(body, "Filter")
mapped := map[string]any{
"pageNo": intValue(body, "PageNumber", 1),
"pageSize": intValue(body, "PageSize", 10),
}
copyString(mapped, "groupType", filter, "GroupType")
copyString(mapped, "groupName", filter, "Name")
copyStringArray(mapped, "groupIds", filter, "GroupIds")
return compactAssetMap(mapped)
}

func mapChinaMobileUpdateAssetGroup(body map[string]any) map[string]any {
return compactAssetMap(map[string]any{
"groupName": stringValue(body, "Name"),
"description": stringValue(body, "Description"),
})
}

func normalizeChinaMobileAssetResponse(spec AssetActionSpec, version string, data []byte) ([]byte, *AssetError) {
var payload struct {
RequestID string `json:"requestId"`
State string `json:"state"`
ErrorCode string `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
Body any `json:"body"`
}
if err := common.Unmarshal(data, &payload); err != nil {
return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
}
if strings.EqualFold(payload.State, "ERROR") {
message := strings.TrimSpace(payload.ErrorMessage)
if message == "" {
message = payload.ErrorCode
}
return nil, newAssetError(AssetErrorUpstream, message, http.StatusBadGateway)
}
if !strings.EqualFold(payload.State, "OK") {
return nil, newAssetError(AssetErrorUpstream, "China Mobile asset response has invalid state", http.StatusBadGateway)
}
if spec.Delete {
deleted, ok := payload.Body.(bool)
if !ok || !deleted {
return nil, newAssetError(AssetErrorUpstream, "China Mobile asset delete was not confirmed", http.StatusBadGateway)
}
}
var result any
if !spec.Delete {
result = normalizeChinaMobileResult(spec.Operation, payload.Body)
}
body, err := buildAssetSuccessResponseWithRequestID(spec.Action, version, payload.RequestID, result)
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
return body, nil
}

func normalizeChinaMobileAssetSDKResponse(spec AssetActionSpec, version string, response any) ([]byte, *AssetError) {
data, err := common.Marshal(response)
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
return normalizeChinaMobileAssetResponse(spec, version, data)
}

func normalizeChinaMobileResult(operation AssetOperation, body any) any {
raw, ok := body.(map[string]any)
if !ok {
return body
}
switch operation {
case AssetOperationAssetList:
return map[string]any{
"Items": normalizeChinaMobileItems(raw["data"], false),
"TotalCount": raw["total"],
}
case AssetOperationAssetGroupList:
return map[string]any{
"Items": normalizeChinaMobileItems(raw["data"], true),
"TotalCount": raw["total"],
}
case AssetOperationAssetGroupCreate, AssetOperationAssetGroupGet, AssetOperationAssetGroupUpdate:
return normalizeChinaMobileAssetGroup(raw)
default:
return normalizeChinaMobileAsset(raw)
}
}

func normalizeChinaMobileItems(value any, group bool) []any {
items, ok := value.([]any)
if !ok {
return nil
}
normalized := make([]any, 0, len(items))
for _, item := range items {
raw, ok := item.(map[string]any)
if !ok {
continue
}
if group {
normalized = append(normalized, normalizeChinaMobileAssetGroup(raw))
} else {
normalized = append(normalized, normalizeChinaMobileAsset(raw))
}
}
return normalized
}

func normalizeChinaMobileAsset(raw map[string]any) map[string]any {
return compactAssetMap(map[string]any{
"Id": raw["assetId"],
"GroupId": raw["groupId"],
"Name": raw["assetName"],
"AssetType": raw["assetType"],
"URL": raw["assetUrl"],
"Status": chinaMobileStatusToOfficial(fmt.Sprint(raw["status"])),
"ErrorMessage": raw["errorMessage"],
"CreatedAt": raw["createdTime"],
"UpdatedAt": raw["updatedTime"],
})
}

func normalizeChinaMobileAssetGroup(raw map[string]any) map[string]any {
return compactAssetMap(map[string]any{
"Id": raw["groupId"],
"GroupId": raw["groupId"],
"GroupType": raw["groupType"],
"Name": raw["groupName"],
"Description": raw["description"],
"CreatedAt": raw["createdTime"],
"UpdatedAt": raw["updatedTime"],
})
}

func validateChinaMobileCompatibility(body map[string]any) *AssetError {
if projectName := strings.TrimSpace(stringValue(body, "ProjectName")); projectName != "" && projectName != "default" {
return newAssetError(AssetErrorOperationNotSupported, "China Mobile asset API does not support non-default ProjectName", http.StatusBadRequest)
}
if sortBy := strings.TrimSpace(stringValue(body, "SortBy")); sortBy != "" && sortBy != "CreateTime" {
return newAssetError(AssetErrorOperationNotSupported, "China Mobile asset API does not support custom SortBy", http.StatusBadRequest)
}
if sortOrder := strings.TrimSpace(stringValue(body, "SortOrder")); sortOrder != "" && !strings.EqualFold(sortOrder, "Desc") {
return newAssetError(AssetErrorOperationNotSupported, "China Mobile asset API does not support custom SortOrder", http.StatusBadRequest)
}
return nil
}

func chinaMobileStatusToOfficial(status string) string {
switch strings.ToUpper(strings.TrimSpace(status)) {
case "PROCESSING":
return "Processing"
case "ACTIVE":
return "Active"
case "FAILED":
return "Failed"
default:
return status
}
}

func officialStatusToChinaMobile(status string) string {
switch strings.ToLower(strings.TrimSpace(status)) {
case "processing":
return "PROCESSING"
case "active":
return "ACTIVE"
case "failed":
return "FAILED"
default:
return status
}
}

func requiredAssetString(body map[string]any, key string) (string, error) {
value := strings.TrimSpace(stringValue(body, key))
if value == "" {
return "", fmt.Errorf("%s is required", key)
}
return value, nil
}

func stringValue(body map[string]any, key string) string {
if body == nil {
return ""
}
value, _ := body[key].(string)
return value
}

func mapValue(body map[string]any, key string) map[string]any {
if body == nil {
return nil
}
value, _ := body[key].(map[string]any)
return value
}

func intValue(body map[string]any, key string, fallback int) int {
if body == nil {
return fallback
}
switch value := body[key].(type) {
case int:
return value
case int32:
return int(value)
case int64:
return int(value)
case float64:
return int(value)
case float32:
return int(value)
default:
return fallback
}
}

func copyString(dst map[string]any, dstKey string, src map[string]any, srcKey string) {
value := stringValue(src, srcKey)
if strings.TrimSpace(value) != "" {
dst[dstKey] = value
}
}

func copyStringArray(dst map[string]any, dstKey string, src map[string]any, srcKey string) {
values := stringArrayValue(src, srcKey)
if len(values) > 0 {
dst[dstKey] = values
}
}

func copyStatusArray(dst map[string]any, dstKey string, src map[string]any, srcKey string) {
values := stringArrayValue(src, srcKey)
if len(values) == 0 {
return
}
mapped := make([]string, 0, len(values))
for _, value := range values {
mapped = append(mapped, officialStatusToChinaMobile(value))
}
dst[dstKey] = mapped
}

func stringArrayValue(body map[string]any, key string) []string {
if body == nil {
return nil
}
switch value := body[key].(type) {
case []string:
return value
case []any:
values := make([]string, 0, len(value))
for _, item := range value {
if s, ok := item.(string); ok && strings.TrimSpace(s) != "" {
values = append(values, s)
}
}
return values
default:
return nil
}
}

func compactAssetMap(input map[string]any) map[string]any {
output := make(map[string]any, len(input))
for key, value := range input {
switch v := value.(type) {
case string:
if strings.TrimSpace(v) != "" {
output[key] = v
}
case nil:
continue
default:
output[key] = value
}
}
return output
}

+ 41
- 0
service/asset_chinamobile_sdk.go Просмотреть файл

@@ -0,0 +1,41 @@
package service

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/config"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
"gitlab.ecloud.com/ecloud/ecloudsdkmaas"
cmmodel "gitlab.ecloud.com/ecloud/ecloudsdkmaas/model"
)

const (
chinaMobileAssetConnectTimeoutSeconds int32 = 10
chinaMobileAssetReadTimeoutSeconds int32 = 60
)

type chinaMobileAssetSDKClient interface {
CreateAsset(request *cmmodel.CreateAssetRequest) (*cmmodel.CreateAssetResponse, error)
ListAssets(request *cmmodel.ListAssetsRequest) (*cmmodel.ListAssetsResponse, error)
GetAsset(request *cmmodel.GetAssetRequest) (*cmmodel.GetAssetResponse, error)
UpdateAsset(request *cmmodel.UpdateAssetRequest) (*cmmodel.UpdateAssetResponse, error)
DeleteAsset(request *cmmodel.DeleteAssetRequest) (*cmmodel.DeleteAssetResponse, error)
CreateAssetGroup(request *cmmodel.CreateAssetGroupRequest) (*cmmodel.CreateAssetGroupResponse, error)
ListAssetGroups(request *cmmodel.ListAssetGroupsRequest) (*cmmodel.ListAssetGroupsResponse, error)
GetAssetGroup(request *cmmodel.GetAssetGroupRequest) (*cmmodel.GetAssetGroupResponse, error)
UpdateAssetGroup(request *cmmodel.UpdateAssetGroupRequest) (*cmmodel.UpdateAssetGroupResponse, error)
DeleteAssetGroup(request *cmmodel.DeleteAssetGroupRequest) (*cmmodel.DeleteAssetGroupResponse, error)
}

func newChinaMobileAssetSDKClient(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
return ecloudsdkmaas.NewClient(newChinaMobileAssetSDKConfig(credential))
}

func newChinaMobileAssetSDKConfig(credential chinaMobileAssetCredential) *config.Config {
return &config.Config{
AccessKey: &credential.AK,
SecretKey: &credential.SK,
PoolId: &credential.PoolID,
ConnectTimeout: utils.Int32(chinaMobileAssetConnectTimeoutSeconds),
ReadTimeout: utils.Int32(chinaMobileAssetReadTimeoutSeconds),
IgnoreSSL: utils.Bool(false),
}
}

+ 19
- 0
service/asset_chinamobile_sdk_test.go Просмотреть файл

@@ -0,0 +1,19 @@
package service

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestChinaMobileAssetSDKConfigUsesTimeoutsAndTLSVerification(t *testing.T) {
config := newChinaMobileAssetSDKConfig(chinaMobileAssetCredential{AK: "ak", SK: "sk", PoolID: "pool"})

require.NotNil(t, config.ConnectTimeout)
require.NotNil(t, config.ReadTimeout)
require.NotNil(t, config.IgnoreSSL)
assert.Greater(t, *config.ConnectTimeout, int32(0))
assert.Greater(t, *config.ReadTimeout, int32(0))
assert.False(t, *config.IgnoreSSL)
}

+ 391
- 0
service/asset_chinamobile_test.go Просмотреть файл

@@ -0,0 +1,391 @@
package service

import (
"context"
"errors"
"net/http"
"testing"
"time"

"github.com/QuantumNous/new-api/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
cmerrs "gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
cmmodel "gitlab.ecloud.com/ecloud/ecloudsdkmaas/model"
)

func TestChinaMobileAssetAdapterCreateAssetMapsRequestAndResponse(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

fake := &fakeChinaMobileAssetSDKClient{}
adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
assert.Equal(t, "ak", credential.AK)
assert.Equal(t, "sk", credential.SK)
assert.Equal(t, defaultChinaMobileAssetPoolID, credential.PoolID)
return fake
}}

spec, ok := ParseAssetAction("CreateAsset")
require.True(t, ok)
resp, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{
Action: spec,
Version: "2024-01-01",
Body: map[string]any{
"GroupId": "group-1",
"Name": "asset",
"URL": "https://example.com/a.png",
"AssetType": "Image",
},
})

require.Nil(t, assetErr)
require.NotNil(t, resp)
require.NotNil(t, fake.createAssetRequest)
assert.Equal(t, "group-1", *fake.createAssetRequest.CreateAssetBody.GroupId)
assert.Equal(t, "asset", *fake.createAssetRequest.CreateAssetBody.AssetName)
assert.Equal(t, "https://example.com/a.png", *fake.createAssetRequest.CreateAssetBody.AssetUrl)
assert.Equal(t, cmmodel.CreateAssetBodyAssetTypeEnumImage, *fake.createAssetRequest.CreateAssetBody.AssetType)
assert.Contains(t, string(resp.Body), `"RequestId":"req-1"`)
assert.Contains(t, string(resp.Body), `"Result":"asset-1"`)
}

func TestChinaMobileAssetCredentialFromEnvDefaultsCenterPool(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

credential, err := chinaMobileAssetCredentialFromEnv()

require.NoError(t, err)
assert.Equal(t, "ak", credential.AK)
assert.Equal(t, "sk", credential.SK)
assert.Equal(t, defaultChinaMobileAssetPoolID, credential.PoolID)
}

func TestChinaMobileAssetCredentialFromEnvSupportsPoolID(t *testing.T) {
setChinaMobileAssetTestEnv(t, "CIDC-RP-29")

credential, err := chinaMobileAssetCredentialFromEnv()

require.NoError(t, err)
assert.Equal(t, "ak", credential.AK)
assert.Equal(t, "sk", credential.SK)
assert.Equal(t, "CIDC-RP-29", credential.PoolID)
}

func TestChinaMobileAssetCredentialFromEnvRequiresAKAndSK(t *testing.T) {
t.Setenv(chinaMobileAssetAKEnv, "")
t.Setenv(chinaMobileAssetSKEnv, "")
t.Setenv(chinaMobileAssetPoolIDEnv, "")

_, err := chinaMobileAssetCredentialFromEnv()

require.Error(t, err)
assert.Contains(t, err.Error(), chinaMobileAssetAKEnv)
assert.Contains(t, err.Error(), chinaMobileAssetSKEnv)
}

func TestChinaMobileAssetAdapterAllOfficialActions(t *testing.T) {
cases := []struct {
action string
body map[string]any
assert func(t *testing.T, fake *fakeChinaMobileAssetSDKClient)
}{
{"CreateAssetGroup", map[string]any{"Name": "g", "GroupType": "AIGC"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) {
require.NotNil(t, fake.createAssetGroupRequest)
}},
{"CreateAsset", map[string]any{"GroupId": "g", "Name": "n", "URL": "https://example.com/a.png", "AssetType": "Image"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.createAssetRequest) }},
{"ListAssetGroups", map[string]any{"Filter": map[string]any{"GroupType": "AIGC"}, "PageNumber": float64(1), "PageSize": float64(10)}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) {
require.NotNil(t, fake.listAssetGroupsRequest)
}},
{"ListAssets", map[string]any{"Filter": map[string]any{"GroupType": "AIGC"}, "PageNumber": float64(1), "PageSize": float64(10)}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.listAssetsRequest) }},
{"GetAsset", map[string]any{"Id": "asset-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) {
require.NotNil(t, fake.getAssetRequest)
assert.Equal(t, "asset-1", *fake.getAssetRequest.GetAssetPath.AssetId)
}},
{"GetAssetGroup", map[string]any{"Id": "group-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) {
require.NotNil(t, fake.getAssetGroupRequest)
assert.Equal(t, "group-1", *fake.getAssetGroupRequest.GetAssetGroupPath.GroupId)
}},
{"UpdateAsset", map[string]any{"Id": "asset-1", "Name": "n"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.updateAssetRequest) }},
{"UpdateAssetGroup", map[string]any{"Id": "group-1", "Name": "g"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) {
require.NotNil(t, fake.updateAssetGroupRequest)
}},
{"DeleteAsset", map[string]any{"Id": "asset-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.deleteAssetRequest) }},
{"DeleteAssetGroup", map[string]any{"Id": "group-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) {
require.NotNil(t, fake.deleteAssetGroupRequest)
}},
}

for _, tc := range cases {
t.Run(tc.action, func(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

fake := &fakeChinaMobileAssetSDKClient{}
adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
return fake
}}
spec, ok := ParseAssetAction(tc.action)
require.True(t, ok)

resp, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{
Action: spec,
Version: "2024-01-01",
Body: tc.body,
})

require.Nil(t, assetErr)
require.NotNil(t, resp)
tc.assert(t, fake)
assert.Contains(t, string(resp.Body), `"ResponseMetadata"`)
})
}
}

func TestChinaMobileAssetAdapterRejectsUnsupportedCompatibilityOptions(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
return &fakeChinaMobileAssetSDKClient{}
}}
spec, ok := ParseAssetAction("ListAssets")
require.True(t, ok)

cases := []map[string]any{
{"ProjectName": "prod"},
{"SortBy": "Name"},
{"SortOrder": "Asc"},
}
for _, body := range cases {
_, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "cm-key"}, AssetRequest{
Action: spec,
Body: body,
})

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorOperationNotSupported, assetErr.Type)
}
}

func TestChinaMobileAssetAdapterMissingIdReturnsInvalidRequest(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
return &fakeChinaMobileAssetSDKClient{}
}}
spec, ok := ParseAssetAction("GetAsset")
require.True(t, ok)

_, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{
Action: spec,
Body: map[string]any{},
})

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorInvalidRequest, assetErr.Type)
assert.Contains(t, assetErr.Message, "Id is required")
}

func TestChinaMobileAssetAdapterStateErrorMapsToUpstreamError(t *testing.T) {
_, assetErr := normalizeChinaMobileAssetResponse(AssetActionSpec{Action: "CreateAsset", Operation: AssetOperationAssetCreate}, "2024-01-01", []byte(`{"requestId":"req","state":"ERROR","errorCode":"Bad","errorMessage":"bad request"}`))

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorUpstream, assetErr.Type)
assert.Equal(t, "bad request", assetErr.Message)
}

func TestChinaMobileAssetAdapterRequiresGroupTypeForListAssets(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
return &fakeChinaMobileAssetSDKClient{}
}}
spec, ok := ParseAssetAction("ListAssets")
require.True(t, ok)

_, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{
Action: spec,
Body: map[string]any{"PageNumber": 1, "PageSize": 10},
})

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorInvalidRequest, assetErr.Type)
assert.Equal(t, http.StatusBadRequest, assetErr.HTTPStatus)
}

func TestChinaMobileAssetDeleteFalseMapsToUpstreamError(t *testing.T) {
_, assetErr := normalizeChinaMobileAssetResponse(AssetActionSpec{Action: "DeleteAsset", Operation: AssetOperationAssetDelete, Delete: true}, "2024-01-01", []byte(`{"requestId":"req","state":"OK","body":false}`))

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorUpstream, assetErr.Type)
}

func TestChinaMobileAssetUnknownStateMapsToUpstreamError(t *testing.T) {
_, assetErr := normalizeChinaMobileAssetResponse(AssetActionSpec{Action: "CreateAsset", Operation: AssetOperationAssetCreate}, "2024-01-01", []byte(`{"requestId":"req","state":"","body":"asset-1"}`))

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorUpstream, assetErr.Type)
}

func TestChinaMobileAssetSDKBadRequestMapsToInvalidRequest(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
return &errorChinaMobileAssetSDKClient{
fakeChinaMobileAssetSDKClient: fakeChinaMobileAssetSDKClient{},
err: cmerrs.NewServerResponseError("bad request", nil, http.StatusBadRequest, nil, `{"errorMessage":"invalid asset"}`),
}
}}
spec, ok := ParseAssetAction("CreateAsset")
require.True(t, ok)

_, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{Action: spec, Body: validChinaMobileCreateAssetBody()})

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorInvalidRequest, assetErr.Type)
assert.Equal(t, http.StatusBadRequest, assetErr.HTTPStatus)
}

func TestChinaMobileAssetSDKServerErrorRemainsUpstreamError(t *testing.T) {
setChinaMobileAssetTestEnv(t, "")

adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient {
return &errorChinaMobileAssetSDKClient{
fakeChinaMobileAssetSDKClient: fakeChinaMobileAssetSDKClient{},
err: cmerrs.NewServerResponseError("server error", nil, http.StatusInternalServerError, nil, ""),
}
}}
spec, ok := ParseAssetAction("CreateAsset")
require.True(t, ok)

_, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{Action: spec, Body: validChinaMobileCreateAssetBody()})

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorUpstream, assetErr.Type)
assert.Equal(t, http.StatusBadGateway, assetErr.HTTPStatus)
}

func TestCallChinaMobileAssetSDKReturnsWhenContextCancelled(t *testing.T) {
client := &blockingChinaMobileAssetSDKClient{fakeChinaMobileAssetSDKClient: fakeChinaMobileAssetSDKClient{}, release: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
cancel()
spec, ok := ParseAssetAction("CreateAsset")
require.True(t, ok)

started := time.Now()
_, err := callChinaMobileAssetSDK(ctx, client, AssetRequest{Action: spec, Body: validChinaMobileCreateAssetBody()})

require.ErrorIs(t, err, context.Canceled)
assert.Less(t, time.Since(started), 500*time.Millisecond)
close(client.release)
}

func setChinaMobileAssetTestEnv(t *testing.T, poolID string) {
t.Helper()
t.Setenv(chinaMobileAssetAKEnv, "ak")
t.Setenv(chinaMobileAssetSKEnv, "sk")
t.Setenv(chinaMobileAssetPoolIDEnv, poolID)
}

type blockingChinaMobileAssetSDKClient struct {
fakeChinaMobileAssetSDKClient
release chan struct{}
}

type errorChinaMobileAssetSDKClient struct {
fakeChinaMobileAssetSDKClient
err error
}

func (f *errorChinaMobileAssetSDKClient) CreateAsset(request *cmmodel.CreateAssetRequest) (*cmmodel.CreateAssetResponse, error) {
if f.err == nil {
return nil, errors.New("missing test error")
}
return nil, f.err
}

func (f *blockingChinaMobileAssetSDKClient) CreateAsset(request *cmmodel.CreateAssetRequest) (*cmmodel.CreateAssetResponse, error) {
<-f.release
return f.fakeChinaMobileAssetSDKClient.CreateAsset(request)
}

func validChinaMobileCreateAssetBody() map[string]any {
return map[string]any{
"GroupId": "group-1",
"Name": "asset",
"URL": "https://example.com/asset.png",
"AssetType": "Image",
}
}

type fakeChinaMobileAssetSDKClient struct {
createAssetRequest *cmmodel.CreateAssetRequest
listAssetsRequest *cmmodel.ListAssetsRequest
getAssetRequest *cmmodel.GetAssetRequest
updateAssetRequest *cmmodel.UpdateAssetRequest
deleteAssetRequest *cmmodel.DeleteAssetRequest
createAssetGroupRequest *cmmodel.CreateAssetGroupRequest
listAssetGroupsRequest *cmmodel.ListAssetGroupsRequest
getAssetGroupRequest *cmmodel.GetAssetGroupRequest
updateAssetGroupRequest *cmmodel.UpdateAssetGroupRequest
deleteAssetGroupRequest *cmmodel.DeleteAssetGroupRequest
}

func (f *fakeChinaMobileAssetSDKClient) CreateAsset(request *cmmodel.CreateAssetRequest) (*cmmodel.CreateAssetResponse, error) {
f.createAssetRequest = request
return (&cmmodel.CreateAssetResponse{}).SetRequestId("req-1").SetState("OK").SetBody("asset-1"), nil
}

func (f *fakeChinaMobileAssetSDKClient) ListAssets(request *cmmodel.ListAssetsRequest) (*cmmodel.ListAssetsResponse, error) {
f.listAssetsRequest = request
item := cmmodel.ListAssetsResponseData{}
item.SetAssetId("asset-1").SetGroupId("group-1").SetAssetName("n").SetStatus(cmmodel.ListAssetsResponseDataStatusEnumActive)
body := (&cmmodel.ListAssetsResponseBody{}).SetData([]cmmodel.ListAssetsResponseData{item}).SetTotal(1)
return (&cmmodel.ListAssetsResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil
}

func (f *fakeChinaMobileAssetSDKClient) GetAsset(request *cmmodel.GetAssetRequest) (*cmmodel.GetAssetResponse, error) {
f.getAssetRequest = request
body := (&cmmodel.GetAssetResponseBody{}).SetAssetId("asset-1").SetGroupId("group-1").SetAssetName("n").SetStatus(cmmodel.GetAssetResponseBodyStatusEnumActive)
return (&cmmodel.GetAssetResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil
}

func (f *fakeChinaMobileAssetSDKClient) UpdateAsset(request *cmmodel.UpdateAssetRequest) (*cmmodel.UpdateAssetResponse, error) {
f.updateAssetRequest = request
body := (&cmmodel.UpdateAssetResponseBody{}).SetAssetId("asset-1").SetGroupId("group-1").SetAssetName("n").SetStatus(cmmodel.UpdateAssetResponseBodyStatusEnumActive)
return (&cmmodel.UpdateAssetResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil
}

func (f *fakeChinaMobileAssetSDKClient) DeleteAsset(request *cmmodel.DeleteAssetRequest) (*cmmodel.DeleteAssetResponse, error) {
f.deleteAssetRequest = request
return (&cmmodel.DeleteAssetResponse{}).SetRequestId("req").SetState("OK").SetBody(true), nil
}

func (f *fakeChinaMobileAssetSDKClient) CreateAssetGroup(request *cmmodel.CreateAssetGroupRequest) (*cmmodel.CreateAssetGroupResponse, error) {
f.createAssetGroupRequest = request
body := (&cmmodel.CreateAssetGroupResponseBody{}).SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.CreateAssetGroupResponseBodyGroupTypeEnumAigc)
return (&cmmodel.CreateAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil
}

func (f *fakeChinaMobileAssetSDKClient) ListAssetGroups(request *cmmodel.ListAssetGroupsRequest) (*cmmodel.ListAssetGroupsResponse, error) {
f.listAssetGroupsRequest = request
item := cmmodel.ListAssetGroupsResponseData{}
item.SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.ListAssetGroupsResponseDataGroupTypeEnumAigc)
body := (&cmmodel.ListAssetGroupsResponseBody{}).SetData([]cmmodel.ListAssetGroupsResponseData{item}).SetTotal(1)
return (&cmmodel.ListAssetGroupsResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil
}

func (f *fakeChinaMobileAssetSDKClient) GetAssetGroup(request *cmmodel.GetAssetGroupRequest) (*cmmodel.GetAssetGroupResponse, error) {
f.getAssetGroupRequest = request
body := (&cmmodel.GetAssetGroupResponseBody{}).SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.GetAssetGroupResponseBodyGroupTypeEnumAigc)
return (&cmmodel.GetAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil
}

func (f *fakeChinaMobileAssetSDKClient) UpdateAssetGroup(request *cmmodel.UpdateAssetGroupRequest) (*cmmodel.UpdateAssetGroupResponse, error) {
f.updateAssetGroupRequest = request
body := (&cmmodel.UpdateAssetGroupResponseBody{}).SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.UpdateAssetGroupResponseBodyGroupTypeEnumAigc)
return (&cmmodel.UpdateAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil
}

func (f *fakeChinaMobileAssetSDKClient) DeleteAssetGroup(request *cmmodel.DeleteAssetGroupRequest) (*cmmodel.DeleteAssetGroupResponse, error) {
f.deleteAssetGroupRequest = request
return (&cmmodel.DeleteAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(true), nil
}

+ 98
- 0
service/asset_compatible.go Просмотреть файл

@@ -0,0 +1,98 @@
package service

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/setting/system_setting"
)

type CompatibleAssetAdapter struct {
name string
operation map[AssetOperation]struct{}
}

func NewCompatibleAssetAdapter(name string, operations []AssetOperation) AssetAdapter {
supported := make(map[AssetOperation]struct{}, len(operations))
for _, op := range operations {
supported[op] = struct{}{}
}
return &CompatibleAssetAdapter{name: name, operation: supported}
}

func (a *CompatibleAssetAdapter) Name() string {
return a.name
}

func (a *CompatibleAssetAdapter) Supports(operation AssetOperation) bool {
_, ok := a.operation[operation]
return ok
}

func (a *CompatibleAssetAdapter) DoAssetRequest(ctx context.Context, channel *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError) {
if !a.Supports(req.Action.Operation) {
return nil, newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported", req.Action.Operation), http.StatusBadRequest)
}
upstreamURL, err := buildCompatibleAssetURL(channel, req.Action.Action, req.Version)
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
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 {
return nil, newAssetError(AssetErrorServer, fmt.Sprintf("request blocked: %v", err), http.StatusForbidden)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(req.RawBody))
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(channel.Key))
client, err := GetHttpClientWithProxy(channel.GetSetting().Proxy)
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(httpReq)
if err != nil {
return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, newAssetError(AssetErrorUpstream, string(data), http.StatusBadGateway)
}
return &AssetUpstreamResponse{StatusCode: resp.StatusCode, Header: resp.Header, Body: data}, nil
}

func buildCompatibleAssetURL(channel *model.Channel, action string, version string) (string, error) {
baseURL := strings.TrimSpace(channel.GetBaseURL())
if baseURL == "" {
baseURL = "https://ark.cn-beijing.volcengineapi.com"
}
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
u.Path = strings.TrimRight(u.Path, "/") + "/api/v1/multimodal/sd/assets"
q := u.Query()
q.Set("Action", action)
if strings.TrimSpace(version) == "" {
version = "2024-01-01"
}
q.Set("Version", version)
u.RawQuery = q.Encode()
return u.String(), nil
}

+ 27
- 0
service/asset_compatible_test.go Просмотреть файл

@@ -0,0 +1,27 @@
package service

import (
"context"
"net/http"
"testing"

"github.com/QuantumNous/new-api/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCompatibleAssetAdapterBlocksPrivateBaseURL(t *testing.T) {
baseURL := "http://127.0.0.1:8080"
adapter := NewCompatibleAssetAdapter("compatible", []AssetOperation{AssetOperationAssetCreate})
spec, ok := ParseAssetAction("CreateAsset")
require.True(t, ok)

_, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "key", BaseURL: &baseURL}, AssetRequest{
Action: spec,
Version: "2024-01-01",
RawBody: []byte(`{}`),
})

require.NotNil(t, assetErr)
assert.Equal(t, http.StatusForbidden, assetErr.HTTPStatus)
}

+ 77
- 0
service/asset_operation.go Просмотреть файл

@@ -0,0 +1,77 @@
package service

import (
"strings"

"github.com/QuantumNous/new-api/common"
)

type AssetOperation string

const (
AssetOperationAssetCreate AssetOperation = "asset.create"
AssetOperationAssetList AssetOperation = "asset.list"
AssetOperationAssetGet AssetOperation = "asset.get"
AssetOperationAssetUpdate AssetOperation = "asset.update"
AssetOperationAssetDelete AssetOperation = "asset.delete"
AssetOperationAssetGroupCreate AssetOperation = "asset_group.create"
AssetOperationAssetGroupList AssetOperation = "asset_group.list"
AssetOperationAssetGroupGet AssetOperation = "asset_group.get"
AssetOperationAssetGroupUpdate AssetOperation = "asset_group.update"
AssetOperationAssetGroupDelete AssetOperation = "asset_group.delete"
)

type AssetActionSpec struct {
Action string
Operation AssetOperation
Delete bool
}

var assetActionSpecs = map[string]AssetActionSpec{
"createassetgroup": {Action: "CreateAssetGroup", Operation: AssetOperationAssetGroupCreate},
"createasset": {Action: "CreateAsset", Operation: AssetOperationAssetCreate},
"listassetgroups": {Action: "ListAssetGroups", Operation: AssetOperationAssetGroupList},
"listassets": {Action: "ListAssets", Operation: AssetOperationAssetList},
"getasset": {Action: "GetAsset", Operation: AssetOperationAssetGet},
"getassetgroup": {Action: "GetAssetGroup", Operation: AssetOperationAssetGroupGet},
"updateasset": {Action: "UpdateAsset", Operation: AssetOperationAssetUpdate},
"updateassetgroup": {Action: "UpdateAssetGroup", Operation: AssetOperationAssetGroupUpdate},
"deleteasset": {Action: "DeleteAsset", Operation: AssetOperationAssetDelete, Delete: true},
"deleteassetgroup": {Action: "DeleteAssetGroup", Operation: AssetOperationAssetGroupDelete, Delete: true},
}

func ParseAssetAction(action string) (AssetActionSpec, bool) {
spec, ok := assetActionSpecs[strings.ToLower(strings.TrimSpace(action))]
return spec, ok
}

type assetResponseMetadata struct {
RequestID string `json:"RequestId,omitempty"`
Action string `json:"Action"`
Version string `json:"Version"`
Service string `json:"Service"`
Region string `json:"Region"`
}

func BuildAssetSuccessResponse(action string, version string, result any) ([]byte, error) {
return buildAssetSuccessResponseWithRequestID(action, version, "", result)
}

func buildAssetSuccessResponseWithRequestID(action string, version string, requestID string, result any) ([]byte, error) {
if strings.TrimSpace(version) == "" {
version = "2024-01-01"
}
payload := map[string]any{
"ResponseMetadata": assetResponseMetadata{
RequestID: requestID,
Action: action,
Version: version,
Service: "ark",
Region: "cn-beijing",
},
}
if result != nil {
payload["Result"] = result
}
return common.Marshal(payload)
}

+ 45
- 0
service/asset_operation_test.go Просмотреть файл

@@ -0,0 +1,45 @@
package service

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseAssetActionRecognizesOfficialActions(t *testing.T) {
cases := map[string]AssetOperation{
"CreateAssetGroup": AssetOperationAssetGroupCreate,
"CreateAsset": AssetOperationAssetCreate,
"ListAssetGroups": AssetOperationAssetGroupList,
"ListAssets": AssetOperationAssetList,
"GetAsset": AssetOperationAssetGet,
"GetAssetGroup": AssetOperationAssetGroupGet,
"UpdateAsset": AssetOperationAssetUpdate,
"UpdateAssetGroup": AssetOperationAssetGroupUpdate,
"DeleteAsset": AssetOperationAssetDelete,
"DeleteAssetGroup": AssetOperationAssetGroupDelete,
}

for action, operation := range cases {
t.Run(action, func(t *testing.T) {
spec, ok := ParseAssetAction(action)
require.True(t, ok)
assert.Equal(t, action, spec.Action)
assert.Equal(t, operation, spec.Operation)
})
}
}

func TestParseAssetActionRejectsUnknownAction(t *testing.T) {
_, ok := ParseAssetAction("CreateRealPersonAuthSession")
assert.False(t, ok)
}

func TestBuildAssetSuccessResponseAllowsDeleteWithoutResult(t *testing.T) {
body, err := BuildAssetSuccessResponse("DeleteAsset", "2024-01-01", nil)
require.NoError(t, err)
assert.Contains(t, string(body), `"Action":"DeleteAsset"`)
assert.Contains(t, string(body), `"ResponseMetadata"`)
assert.NotContains(t, string(body), `"Result"`)
}

+ 79
- 0
service/asset_resolver.go Просмотреть файл

@@ -0,0 +1,79 @@
package service

import (
"errors"
"fmt"
"net/http"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"gorm.io/gorm"
)

func ResolveAssetChannelForOperation(userID int, tokenGroup string, operation AssetOperation) (*model.Channel, AssetAdapter, *AssetError) {
bindings, err := model.GetUserAssetChannelsByTypes(userID, RegisteredAssetChannelTypes(), tokenGroup)
if err != nil {
return nil, nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
if len(bindings) > 0 {
binding := bindings[0]
channel, err := model.CacheGetChannel(binding.ChannelId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) || common.MemoryCacheEnabled {
return nil, nil, newAssetError(AssetErrorBindingInvalid, "bound asset channel not found", http.StatusBadGateway)
}
return nil, nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
adapter, ok := GetAssetAdapter(channel.Type)
if channel.Status != common.ChannelStatusEnabled || !assetChannelHasKey(channel) || !MatchDoubaoAssetGroup(channel.GetGroups(), tokenGroup) || !ok {
return nil, nil, newAssetError(AssetErrorBindingInvalid, "bound asset channel is not available for asset library", http.StatusBadGateway)
}
if !adapter.Supports(operation) {
return nil, nil, newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported by bound channel", operation), http.StatusBadRequest)
}
return channel, adapter, nil
}

channel, adapter, err := autoMatchAssetChannelForOperation(tokenGroup, operation)
if err != nil {
return nil, nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
if channel == nil {
return nil, nil, newAssetError(AssetErrorChannelNotFound, "no available asset channel supports requested operation", http.StatusBadGateway)
}
return channel, adapter, nil
}

func autoMatchAssetChannelForOperation(tokenGroup string, operation AssetOperation) (*model.Channel, AssetAdapter, error) {
for _, channelType := range RegisteredAssetChannelTypes() {
adapter, ok := GetAssetAdapter(channelType)
if !ok || !adapter.Supports(operation) {
continue
}
for startIdx := 0; ; startIdx += DoubaoAssetChannelPageSize {
candidates, err := model.GetChannelsByType(startIdx, DoubaoAssetChannelPageSize, true, channelType)
if err != nil {
return nil, nil, err
}
for _, candidate := range candidates {
if candidate == nil || !MatchDoubaoAssetGroup(candidate.GetGroups(), tokenGroup) {
continue
}
channel, err := model.CacheGetChannel(candidate.Id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) || common.MemoryCacheEnabled {
continue
}
return nil, nil, err
}
if channel.Status == common.ChannelStatusEnabled && assetChannelHasKey(channel) && MatchDoubaoAssetGroup(channel.GetGroups(), tokenGroup) {
return channel, adapter, nil
}
}
if len(candidates) < DoubaoAssetChannelPageSize {
break
}
}
}
return nil, nil, nil
}

+ 106
- 0
service/asset_resolver_test.go Просмотреть файл

@@ -0,0 +1,106 @@
package service

import (
"context"
"net/http"
"testing"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type fakeAssetAdapter struct {
name string
operation AssetOperation
statusCode int
}

func (a fakeAssetAdapter) Name() string {
return a.name
}

func (a fakeAssetAdapter) Supports(op AssetOperation) bool {
return a.operation == op
}

func (a fakeAssetAdapter) DoAssetRequest(context.Context, *model.Channel, AssetRequest) (*AssetUpstreamResponse, *AssetError) {
return &AssetUpstreamResponse{StatusCode: a.statusCode, Body: []byte(`{"ok":true}`)}, nil
}

func TestResolveAssetChannelAutoMatchesOnlyRegisteredOperation(t *testing.T) {
db := setupDoubaoAssetChannelDB(t)
resetAssetAdapterRegistryForTest(t)
RegisterAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fakeAssetAdapter{name: "cm", operation: AssetOperationAssetCreate, statusCode: http.StatusOK})
createDoubaoAssetChannelForTest(t, db, 1, constant.ChannelTypeOpenAI, "default", "openai-key", common.ChannelStatusEnabled)
createDoubaoAssetChannelForTest(t, db, 2, constant.ChannelTypeChinaMobileSeedance, "default", "cm-key", common.ChannelStatusEnabled)

ch, adapter, assetErr := ResolveAssetChannelForOperation(10, "default", AssetOperationAssetCreate)

require.Nil(t, assetErr)
require.NotNil(t, ch)
require.NotNil(t, adapter)
assert.Equal(t, 2, ch.Id)
assert.Equal(t, "cm", adapter.Name())
}

func TestResolveAssetChannelBoundUnsupportedOperationDoesNotFallback(t *testing.T) {
db := setupDoubaoAssetChannelDB(t)
resetAssetAdapterRegistryForTest(t)
RegisterAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fakeAssetAdapter{name: "cm", operation: AssetOperationAssetCreate})
RegisterAssetAdapterForTest(constant.ChannelTypeDoubaoVideoCompatibleAiping, fakeAssetAdapter{name: "aiping", operation: AssetOperationAssetGroupCreate})
createDoubaoAssetChannelForTest(t, db, 1, constant.ChannelTypeChinaMobileSeedance, "default", "cm-key", common.ChannelStatusEnabled)
createDoubaoAssetChannelForTest(t, db, 2, constant.ChannelTypeDoubaoVideoCompatibleAiping, "default", "aiping-key", common.ChannelStatusEnabled)
require.NoError(t, model.BindUserAssetChannel(10, constant.ChannelTypeChinaMobileSeedance, "default", 1))

ch, adapter, assetErr := ResolveAssetChannelForOperation(10, "default", AssetOperationAssetGroupCreate)

assert.Nil(t, ch)
assert.Nil(t, adapter)
require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorOperationNotSupported, assetErr.Type)
}

func TestResolveAssetChannelIgnoresBindingFromUnrelatedFamily(t *testing.T) {
db := setupDoubaoAssetChannelDB(t)
resetAssetAdapterRegistryForTest(t)
RegisterAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fakeAssetAdapter{name: "cm", operation: AssetOperationAssetCreate})
createDoubaoAssetChannelForTest(t, db, 1, constant.ChannelTypeKlingAiping, "default", "kling-key", common.ChannelStatusEnabled)
createDoubaoAssetChannelForTest(t, db, 2, constant.ChannelTypeChinaMobileSeedance, "default", "cm-key", common.ChannelStatusEnabled)
require.NoError(t, model.BindUserAssetChannel(10, constant.ChannelTypeKlingAiping, "default", 1))

ch, adapter, assetErr := ResolveAssetChannelForOperation(10, "default", AssetOperationAssetCreate)

require.Nil(t, assetErr)
require.NotNil(t, ch)
require.NotNil(t, adapter)
assert.Equal(t, 2, ch.Id)
}

func TestResolveAssetChannelAutoMatchDoesNotReplaceVideoBinding(t *testing.T) {
db := setupDoubaoAssetChannelDB(t)
resetAssetAdapterRegistryForTest(t)
RegisterAssetAdapterForTest(constant.ChannelTypeChinaMobileSeedance, fakeAssetAdapter{name: "cm", operation: AssetOperationAssetGroupCreate})
createDoubaoAssetChannelForTest(t, db, 1, constant.ChannelTypeKlingAiping, "default", "kling-key", common.ChannelStatusEnabled)
createDoubaoAssetChannelForTest(t, db, 2, constant.ChannelTypeChinaMobileSeedance, "default", "cm-key", common.ChannelStatusEnabled)
require.NoError(t, model.BindUserAssetChannel(10, constant.ChannelTypeKlingAiping, "default", 1))

ch, _, assetErr := ResolveAssetChannelForOperation(10, "default", AssetOperationAssetGroupCreate)

require.Nil(t, assetErr)
require.NotNil(t, ch)
assert.Equal(t, 2, ch.Id)
binding, err := model.GetUserAssetChannel(10, constant.ChannelTypeKlingAiping, "default")
require.NoError(t, err)
require.NotNil(t, binding)
assert.Equal(t, 1, binding.ChannelId)
}

func TestCompatibleAssetAdapterSupportsOnlyDeclaredOperations(t *testing.T) {
adapter := NewCompatibleAssetAdapter("aiping_asset", []AssetOperation{AssetOperationAssetCreate})

assert.True(t, adapter.Supports(AssetOperationAssetCreate))
assert.False(t, adapter.Supports(AssetOperationAssetGroupCreate))
}

+ 20
- 0
service/channel_select_test.go Просмотреть файл

@@ -179,6 +179,26 @@ func TestCacheGetRandomSatisfiedChannelFiltersAllowedChannelTypes(t *testing.T)
assert.Equal(t, 101, channel.Id)
}

func TestCacheGetRandomSatisfiedChannelAllowsChinaMobileSeedanceFamily(t *testing.T) {
db := setupServiceChannelSelectDB(t)
createServiceChannelSelectChannel(t, db, 101, constant.ChannelTypeChinaMobileSeedance, "default", "doubao-seedance-2.0")
createServiceChannelSelectChannel(t, db, 102, constant.ChannelTypeOpenAI, "default", "doubao-seedance-2.0")

c := buildRetryContext(t)
channel, group, err := CacheGetRandomSatisfiedChannel(&RetryParam{
Ctx: c,
TokenGroup: "default",
ModelName: "doubao-seedance-2.0",
Retry: common.GetPointer(0),
AllowedChannelTypes: VideoAssetChannelTypesForFamily(VideoAssetFamilySeedance),
})

require.NoError(t, err)
require.NotNil(t, channel)
assert.Equal(t, "default", group)
assert.Equal(t, 101, channel.Id)
}

// --- GetUserAutoGroup tests ---

func TestGetUserAutoGroup_Basic(t *testing.T) {


+ 128
- 0
service/chinamobile_user_asset_group.go Просмотреть файл

@@ -0,0 +1,128 @@
package service

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
)

type chinaMobileAssetGroupCreator func(context.Context, int, *model.Channel) (string, *AssetError)

func IsChinaMobileAssetChannel(channel *model.Channel) bool {
return channel != nil && channel.Type == constant.ChannelTypeChinaMobileSeedance
}

func IsAssetGroupOperation(operation AssetOperation) bool {
return strings.HasPrefix(string(operation), "asset_group.")
}

func GetOrCreateChinaMobileUserAssetGroup(ctx context.Context, userId int, channel *model.Channel, createGroup chinaMobileAssetGroupCreator) (string, *AssetError) {
if channel == nil {
return "", newAssetError(AssetErrorServer, "China Mobile asset channel is required", http.StatusInternalServerError)
}
binding, err := model.GetUserAssetGroup(userId, channel.Id)
if err != nil {
return "", newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
if binding != nil {
return binding.GroupId, nil
}

groupId, assetErr := createGroup(ctx, userId, channel)
if assetErr != nil {
return "", assetErr
}
if strings.TrimSpace(groupId) == "" {
return "", newAssetError(AssetErrorUpstream, "China Mobile asset group creation returned an empty group ID", http.StatusBadGateway)
}
if err := model.CreateUserAssetGroup(userId, channel.Id, groupId); err == nil {
return groupId, nil
}

// Another first request may have persisted the binding while this one created its upstream group.
binding, readErr := model.GetUserAssetGroup(userId, channel.Id)
if readErr == nil && binding != nil {
return binding.GroupId, nil
}
if readErr != nil {
return "", newAssetError(AssetErrorServer, readErr.Error(), http.StatusInternalServerError)
}
return "", newAssetError(AssetErrorServer, "failed to persist China Mobile user asset group", http.StatusInternalServerError)
}

func CreateChinaMobileUserAssetGroup(ctx context.Context, userId int, adapter AssetAdapter, channel *model.Channel) (string, *AssetError) {
spec, ok := ParseAssetAction("CreateAssetGroup")
if !ok {
return "", newAssetError(AssetErrorServer, "CreateAssetGroup action is not registered", http.StatusInternalServerError)
}
resp, assetErr := adapter.DoAssetRequest(ctx, channel, AssetRequest{
Action: spec,
Version: "2024-01-01",
Body: map[string]any{
"GroupType": "AIGC",
"Name": fmt.Sprintf("new-api-user-%d-channel-%d", userId, channel.Id),
},
})
if assetErr != nil {
return "", assetErr
}
var payload struct {
Result struct {
GroupId string `json:"GroupId"`
} `json:"Result"`
}
if err := common.Unmarshal(resp.Body, &payload); err != nil {
return "", newAssetError(AssetErrorUpstream, fmt.Sprintf("invalid China Mobile asset group response: %v", err), http.StatusBadGateway)
}
return strings.TrimSpace(payload.Result.GroupId), nil
}

func ScopeChinaMobileAssetRequest(req *AssetRequest, groupId string) {
switch req.Action.Operation {
case AssetOperationAssetCreate:
req.Body["GroupId"] = groupId
case AssetOperationAssetList:
filter := mapValue(req.Body, "Filter")
if filter == nil {
return
}
filter["GroupIds"] = []string{groupId}
req.Body["Filter"] = filter
}
}

func RequireChinaMobileAssetOwnership(ctx context.Context, adapter AssetAdapter, channel *model.Channel, request AssetRequest, groupId string) *AssetError {
spec, ok := ParseAssetAction("GetAsset")
if !ok {
return newAssetError(AssetErrorServer, "GetAsset action is not registered", http.StatusInternalServerError)
}
assetId := strings.TrimSpace(stringValue(request.Body, "Id"))
resp, assetErr := adapter.DoAssetRequest(ctx, channel, AssetRequest{
Action: spec,
Version: request.Version,
Body: map[string]any{"Id": assetId},
})
if assetErr != nil {
if assetErr.HTTPStatus == http.StatusNotFound {
return newAssetError(AssetErrorNotFound, "asset not found", http.StatusNotFound)
}
return assetErr
}
var payload struct {
Result struct {
GroupId string `json:"GroupId"`
} `json:"Result"`
}
if err := common.Unmarshal(resp.Body, &payload); err != nil {
return newAssetError(AssetErrorUpstream, fmt.Sprintf("invalid China Mobile asset response: %v", err), http.StatusBadGateway)
}
if strings.TrimSpace(payload.Result.GroupId) != groupId {
return newAssetError(AssetErrorNotFound, "asset not found", http.StatusNotFound)
}
return nil
}

+ 159
- 0
service/chinamobile_user_asset_group_test.go Просмотреть файл

@@ -0,0 +1,159 @@
package service

import (
"context"
"net/http"
"sync"
"testing"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

func setupChinaMobileUserAssetGroupDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:chinamobile_user_asset_groups?mode=memory&cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
require.NoError(t, err)
sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)

origDB := model.DB
model.DB = db
require.NoError(t, db.AutoMigrate(&model.UserAssetGroup{}))
t.Cleanup(func() {
model.DB = origDB
require.NoError(t, sqlDB.Close())
})
return db
}

func chinaMobileAssetAction(t *testing.T, action string) AssetActionSpec {
t.Helper()
spec, ok := ParseAssetAction(action)
require.True(t, ok)
return spec
}

func TestGetOrCreateChinaMobileUserAssetGroupCreatesThenReuses(t *testing.T) {
setupChinaMobileUserAssetGroupDB(t)
channel := &model.Channel{Id: 101, Type: constant.ChannelTypeChinaMobileSeedance}
createCalls := 0
creator := func(context.Context, int, *model.Channel) (string, *AssetError) {
createCalls++
return "group-1", nil
}

groupID, assetErr := GetOrCreateChinaMobileUserAssetGroup(context.Background(), 10, channel, creator)
require.Nil(t, assetErr)
assert.Equal(t, "group-1", groupID)

again, assetErr := GetOrCreateChinaMobileUserAssetGroup(context.Background(), 10, channel, creator)
require.Nil(t, assetErr)
assert.Equal(t, "group-1", again)
assert.Equal(t, 1, createCalls)
}

func TestGetOrCreateChinaMobileUserAssetGroupConcurrentRequestsReuseBinding(t *testing.T) {
setupChinaMobileUserAssetGroupDB(t)
channel := &model.Channel{Id: 102, Type: constant.ChannelTypeChinaMobileSeedance}
creator := func(context.Context, int, *model.Channel) (string, *AssetError) {
return "group-concurrent", nil
}

const workers = 8
results := make(chan string, workers)
errs := make(chan *AssetError, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
groupID, assetErr := GetOrCreateChinaMobileUserAssetGroup(context.Background(), 10, channel, creator)
results <- groupID
errs <- assetErr
}()
}
wg.Wait()
close(results)
close(errs)
for assetErr := range errs {
require.Nil(t, assetErr)
}
for groupID := range results {
assert.Equal(t, "group-concurrent", groupID)
}

binding, err := model.GetUserAssetGroup(10, 102)
require.NoError(t, err)
require.NotNil(t, binding)
assert.Equal(t, "group-concurrent", binding.GroupId)
}

func TestScopeChinaMobileAssetRequestOverwritesClientGroup(t *testing.T) {
create := AssetRequest{Action: chinaMobileAssetAction(t, "CreateAsset"), Body: map[string]any{"GroupId": "forged"}}
ScopeChinaMobileAssetRequest(&create, "owned")
assert.Equal(t, "owned", create.Body["GroupId"])

list := AssetRequest{Action: chinaMobileAssetAction(t, "ListAssets"), Body: map[string]any{"Filter": map[string]any{"GroupIds": []any{"forged"}}}}
ScopeChinaMobileAssetRequest(&list, "owned")
assert.Equal(t, []string{"owned"}, list.Body["Filter"].(map[string]any)["GroupIds"])
}

func TestRequireChinaMobileAssetOwnershipHidesMismatchedGroup(t *testing.T) {
adapter := &recordingAssetAdapter{getAssetGroupID: "other"}
request := AssetRequest{Action: chinaMobileAssetAction(t, "DeleteAsset"), Body: map[string]any{"Id": "asset-1"}}

assetErr := RequireChinaMobileAssetOwnership(context.Background(), adapter, &model.Channel{}, request, "owned")

require.NotNil(t, assetErr)
assert.Equal(t, AssetErrorNotFound, assetErr.Type)
assert.Equal(t, http.StatusNotFound, assetErr.HTTPStatus)
assert.Equal(t, AssetOperationAssetGet, adapter.requests[0].Action.Operation)
}

type recordingAssetAdapter struct {
requests []AssetRequest
getAssetGroupID string
}

func (a *recordingAssetAdapter) Name() string { return "recording" }
func (a *recordingAssetAdapter) Supports(AssetOperation) bool { return true }
func (a *recordingAssetAdapter) DoAssetRequest(_ context.Context, _ *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError) {
a.requests = append(a.requests, req)
if req.Action.Operation == AssetOperationAssetGroupCreate {
body, err := BuildAssetSuccessResponse(req.Action.Action, req.Version, map[string]any{"GroupId": "group-created"})
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
return &AssetUpstreamResponse{StatusCode: http.StatusOK, Body: body}, nil
}
if req.Action.Operation != AssetOperationAssetGet {
return &AssetUpstreamResponse{StatusCode: http.StatusOK}, nil
}
body, err := BuildAssetSuccessResponse(req.Action.Action, req.Version, map[string]any{"Id": "asset-1", "GroupId": a.getAssetGroupID})
if err != nil {
return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
}
return &AssetUpstreamResponse{StatusCode: http.StatusOK, Body: body}, nil
}

func TestCreateChinaMobileUserAssetGroupUsesInternalGroupAction(t *testing.T) {
adapter := &recordingAssetAdapter{}
groupID, assetErr := CreateChinaMobileUserAssetGroup(context.Background(), 10, adapter, &model.Channel{Id: 101})
require.Nil(t, assetErr)
assert.Equal(t, "group-created", groupID)
assert.Equal(t, AssetOperationAssetGroupCreate, adapter.requests[0].Action.Operation)
assert.Equal(t, "AIGC", adapter.requests[0].Body["GroupType"])
assert.Equal(t, "new-api-user-10-channel-101", adapter.requests[0].Body["Name"])

// Ensure the normalized response parser remains wired through common JSON helpers.
_, err := common.Marshal(adapter.requests[0].Body)
require.NoError(t, err)
}

+ 1
- 0
service/video_asset_channel.go Просмотреть файл

@@ -23,6 +23,7 @@ func VideoAssetChannelTypesForFamily(family VideoAssetFamily) []int {
return []int{
constant.ChannelTypeDoubaoVideoCompatibleAiping,
constant.ChannelTypeDoubaoVideoCompatibleTianyiYun,
constant.ChannelTypeChinaMobileSeedance,
}
case VideoAssetFamilyKling:
return []int{


+ 4
- 0
service/video_asset_channel_test.go Просмотреть файл

@@ -23,6 +23,10 @@ func TestGetBoundVideoAssetChannelForModelFindsSeedanceTianyiYunBinding(t *testi
assert.Equal(t, 16, ch.Id)
}

func TestVideoAssetSeedanceFamilyIncludesChinaMobileSeedance(t *testing.T) {
require.Contains(t, VideoAssetChannelTypesForFamily(VideoAssetFamilySeedance), constant.ChannelTypeChinaMobileSeedance)
}

func TestGetBoundVideoAssetChannelForModelFindsKlingBindingByFamily(t *testing.T) {
db := setupDoubaoAssetChannelDB(t)
createDoubaoAssetChannelForTest(t, db, 59, constant.ChannelTypeKlingAiping, "default", "key", common.ChannelStatusEnabled)


+ 17
- 0
setup_test.sql Просмотреть файл

@@ -0,0 +1,17 @@
DELETE FROM tokens WHERE user_id IN (SELECT id FROM users WHERE username LIKE 'asset-admin' OR username LIKE 'asset-user');
DELETE FROM user_asset_channels WHERE user_id IN (9001, 9002);
DELETE FROM channels WHERE name = 'cm-asset-channel';
DELETE FROM users WHERE username LIKE 'asset-admin' OR username LIKE 'asset-user';

INSERT INTO users (id, username, password, display_name, role, status, quota, "group", access_token)
VALUES (9001, 'asset-admin', 'pbkdf2_sha256$600000$dGVzdA==$hash', '素材管理员', 1, 1, 1000000000, 'default', 'admin-asset-token-9001');
INSERT INTO users (id, username, password, display_name, role, status, quota, "group")
VALUES (9002, 'asset-user', 'pbkdf2_sha256$600000$dGVzdA==$hash', '素材用户', 0, 1, 1000000000, 'default');
INSERT INTO channels (id, type, key, status, name, "group", models, weight, priority, auto_ban, created_time)
VALUES (9001, 61, '__CHINAMOBILE_ASSET_CHANNEL_KEY__', 1, 'cm-asset-channel', 'default', 'seedance-2', 10, 1, 1, 1784103836);
INSERT INTO tokens (id, user_id, key, status, name, "group", expired_time, unlimited_quota)
VALUES (9001, 9001, 'assetadmintestkey00000000000000000000000000001', 1, 'asset-admin-token', 'default', -1, 1);
INSERT INTO tokens (id, user_id, key, status, name, "group", expired_time, unlimited_quota)
VALUES (9002, 9002, 'assetusertestkey000000000000000000000000000001', 1, 'asset-user-token', 'default', -1, 1);
INSERT INTO user_asset_channels (user_id, channel_type, "group", channel_id, created_at, updated_at)
VALUES (9001, 61, 'default', 9001, 1784103836, 1784103836);

+ 24
- 0
setup_test_env.ps1 Просмотреть файл

@@ -0,0 +1,24 @@
param(
[string]$TemplatePath = (Join-Path $PSScriptRoot "setup_test.sql"),
[string]$OutputPath = (Join-Path $env:TEMP "new-api-asset-setup.sql")
)

$ak = $env:CHINAMOBILE_ASSET_AK
$sk = $env:CHINAMOBILE_ASSET_SK
$poolId = $env:CHINAMOBILE_ASSET_POOL_ID

if ([string]::IsNullOrWhiteSpace($ak) -or [string]::IsNullOrWhiteSpace($sk)) {
throw "CHINAMOBILE_ASSET_AK and CHINAMOBILE_ASSET_SK are required"
}
if ([string]::IsNullOrWhiteSpace($poolId)) {
$poolId = "CIDC-CORE-00"
}
if ($ak.Contains("'") -or $sk.Contains("'") -or $poolId.Contains("'")) {
throw "China Mobile asset credentials must not contain single quotes"
}

$channelKey = "$ak|$sk|$poolId"
$sql = Get-Content -LiteralPath $TemplatePath -Raw
$sql = $sql.Replace("__CHINAMOBILE_ASSET_CHANNEL_KEY__", $channelKey)
[System.IO.File]::WriteAllText($OutputPath, $sql, [System.Text.UTF8Encoding]::new($false))
Write-Output $OutputPath

+ 154
- 0
third_party/ecloudsdkcore/api_client.go Просмотреть файл

@@ -0,0 +1,154 @@
package ecloudsdkcore

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/auth"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/auth/provider"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/config"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/http"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/param"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/request"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/response"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/retry"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

// APIClient manages communication
type APIClient struct {
Config *config.Config
HttpClient *http.NetHttpClient
HttpRequest *request.HttpRequest
}

// NewAPIClient creates a new API http.
func NewAPIClient() *APIClient {
return &APIClient{HttpClient: http.NewHttpClient()}
}

// NewCustomizedAPIClient creates a new customized API http.
func NewCustomizedAPIClient(config *config.Config, httpRequest *request.HttpRequest) *APIClient {
return &APIClient{
Config: config,
HttpClient: http.NewHttpClient(),
HttpRequest: httpRequest,
}
}

func DefaultApiClient(config *config.Config, httpRequest *request.HttpRequest) *APIClient {
return NewCustomizedAPIClient(config, httpRequest)
}

// InitConfig init default configuration
func InitConfig(c *config.Config) {
if utils.IsUnSet(c.AutoRetry) {
autoRetry := false
c.AutoRetry = &autoRetry
}
if utils.IsUnSet(c.IgnoreSSL) {
ignoreSSL := true
c.IgnoreSSL = &ignoreSSL
}
if utils.IsUnSet(c.IgnoreGateway) {
ignoreGateway := false
c.IgnoreGateway = &ignoreGateway
}
if utils.IsUnSet(c.CentralTransportEnabled) {
centralTransportEnabled := true
c.CentralTransportEnabled = &centralTransportEnabled
}
}

// Excute entry for http call
func (c *APIClient) Excute(params *param.Params, rc *config.RuntimeConfig,
returnType interface{}) (*response.HttpResponse, error) {
httpReq := c.HttpRequest
httpReq = new(request.HttpRequest)
if err := utils.DeepCopy(httpReq, c.HttpRequest); err != nil {
errs.NewServerRequestError("copy object error", nil)
}
if rc == nil {
rc = &config.RuntimeConfig{}
}
c.resetHttpRequest(httpReq)
if rc == nil {
rc = &config.RuntimeConfig{}
}
cm := utils.Merge(c.Config, rc)
if err := c.buildHttpRequest(params, cm, httpReq); err != nil {
return nil, err
}
httpReq.BuildFinalUrl()
if utils.BoolValue(cm["AutoRetry"].(*bool)) {
retryTemplate := c.buildRetryTemplate(cm)
res, err := retryTemplate.Call(func() (interface{}, error) {
return c.HttpClient.Execute(httpReq, cm, returnType)
})
if err != nil {
return nil, err
}
httpResponse, _ := res.(*response.HttpResponse)
return httpResponse, nil
}
return c.HttpClient.Execute(httpReq, cm, returnType)
}

func (c *APIClient) buildRetryTemplate(cm map[string]interface{}) *retry.Template {
templateBuilder := retry.NewBuilder()
if utils.IsSet(cm["MaxRetryTimes"]) {
templateBuilder.SetRetryTimes(utils.Int32Value(cm["MaxRetryTimes"].(*int32)))
}
if utils.IsSet(cm["RetryPeriod"]) {
templateBuilder.SetRetryPolicy(retry.WaitRetryPolicy(utils.Int64Value((cm["RetryPeriod"].(*int64)))))
} else {
templateBuilder.SetRetryPolicy(retry.NoWait())
}
if utils.IsSet(cm["MaxDuringTime"]) {
templateBuilder.SetMaxDuringTime(utils.Int64Value(cm["MaxDuringTime"].(*int64)))
}
return templateBuilder.Build()
}

func (c *APIClient) buildHttpRequest(params *param.Params, cm map[string]interface{}, httpReq *request.HttpRequest) error {
httpReq.ConvertRequest(params.Request)
err := httpReq.BuildApiParams(params, cm)
if err != nil {
return err
}
if utils.IsUnSet(cm["Provider"]) {
if utils.IsUnSet(c.Config.AccessKey) || utils.IsUnSet(c.Config.SecretKey) {
return errs.NewServerRequestError("accessKey or secretKey can not be null", nil)
}
credential := auth.NewCredentialBuilder().AccessKey(*c.Config.AccessKey).SecretKey(*c.Config.SecretKey).Build()
err := auth.GetCredentialManager(*credential.CredentialType).Sign(httpReq, credential)
if err != nil {
return err
}
return nil

}
p := cm["Provider"].(provider.ICredentialProvider)
credential, err := p.GetCredential()
if err != nil {
return err
}
if utils.IsSet(c.Config.Provider) && p != c.Config.Provider {
otherCredential, err := c.Config.Provider.GetCredential()
if err != nil {
return err
}
credential.CopyValues(otherCredential)
}
if utils.IsUnSet(credential.AccessKey) || utils.IsUnSet(credential.SecretKey) {
credential.AccessKey = (c.Config.AccessKey)
credential.SecretKey = (c.Config.SecretKey)
}
err = auth.GetCredentialManager(*credential.CredentialType).Sign(httpReq, credential)
if err != nil {
return err
}
return nil
}

func (c *APIClient) resetHttpRequest(req *request.HttpRequest) {
req.Reset()
}

+ 14
- 0
third_party/ecloudsdkcore/auth/constants.go Просмотреть файл

@@ -0,0 +1,14 @@
package auth

const (
EnvAkKey = "ECLOUD_ACCESS_KEY"
EnvSkKey = "ECLOUD_SECRET_KEY"
EnvMopPrivateKey = "ECLOUD_MOP_PRIVATE_KEY"
EnvMopPublicKey = "ECLOUD_MOP_PUBLIC_KEY"
EnvCredentialFile = "ECLOUD_CREDENTIALS_FILE"
PathCredentialFile = "/.ecloud/credentials"
ProfileAccessKey = "access_key"
ProfileSecretKey = "secret_key"
ProfileMopPrivateKey = "mop_private_key"
ProfileMopPublicKey = "mop_public_key"
)

+ 117
- 0
third_party/ecloudsdkcore/auth/credential.go Просмотреть файл

@@ -0,0 +1,117 @@
package auth

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CredentialType string

const (
CredentialAkSk CredentialType = "ECLOUD_AKSK"
CredentialMop = "MOP"
CredentialS3 = "ECLOUD_S3"
CredentialNone = "NONE"
)

type EncryptionType string

const (
EncrytMopRsa EncryptionType = "MOP_RSA"
EncrytNone = "NONE"
)

type Credential struct {
AccessKey *string
SecretKey *string
SecurityToken *string
PrivateKey *string
PublicKey *string
CredentialType *CredentialType
EncryptionType *EncryptionType
}

func NewCredential() *Credential {
return &Credential{
CredentialType: CredentialTypePointer(CredentialAkSk),
EncryptionType: EncryptionTypePointer(EncrytNone),
}
}

func (c *Credential) CopyValues(other *Credential) {
if utils.IsUnSet(c.AccessKey) && utils.IsSet(other.AccessKey) {
c.AccessKey = other.AccessKey
}
if utils.IsUnSet(c.SecretKey) && utils.IsSet(other.SecretKey) {
c.SecretKey = other.SecretKey
}
if utils.IsUnSet(c.SecurityToken) && utils.IsSet(other.SecurityToken) {
c.SecurityToken = other.SecurityToken
}
if utils.IsUnSet(c.PrivateKey) && utils.IsSet(other.PrivateKey) {
c.PrivateKey = other.PrivateKey
}
if utils.IsUnSet(c.PublicKey) && utils.IsSet(other.PublicKey) {
c.PublicKey = other.PublicKey
}
}

func (c *Credential) String() string {
return utils.Beautify(c)
}

func (c *Credential) GoString() string {
return c.String()
}

func (c *Credential) ToJsonString() string {
return utils.ToJsonString(c)
}

type CredentialBuilder struct {
credential *Credential
}

func NewCredentialBuilder() *CredentialBuilder {
credential := NewCredential()
c := &CredentialBuilder{credential: credential}
return c
}

func (c *CredentialBuilder) AccessKey(accessKey string) *CredentialBuilder {
c.credential.AccessKey = utils.String(accessKey)
return c
}

func (c *CredentialBuilder) SecretKey(secretKey string) *CredentialBuilder {
c.credential.SecretKey = utils.String(secretKey)
return c
}

func (c *CredentialBuilder) PrivateKey(privateKey string) *CredentialBuilder {
c.credential.PrivateKey = utils.String(privateKey)
return c
}

func (c *CredentialBuilder) PublicKey(publicKey string) *CredentialBuilder {
c.credential.PublicKey = utils.String(publicKey)
return c
}

func (c *CredentialBuilder) SecurityToken(securityToken string) *CredentialBuilder {
c.credential.SecurityToken = utils.String(securityToken)
return c
}

func (c *CredentialBuilder) CredentialType(credentialType CredentialType) *CredentialBuilder {
c.credential.CredentialType = CredentialTypePointer(credentialType)
return c
}

func (c *CredentialBuilder) EncryptionType(encryptionType EncryptionType) *CredentialBuilder {
c.credential.EncryptionType = EncryptionTypePointer(encryptionType)
return c
}

func (c *CredentialBuilder) Build() *Credential {
return c.credential
}

+ 284
- 0
third_party/ecloudsdkcore/auth/credential_impl.go Просмотреть файл

@@ -0,0 +1,284 @@
package auth

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/consts"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/request"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
"net/url"
"sort"
"strings"
"time"
)

type ICredential interface {
Sign(request *request.HttpRequest, credential *Credential) error
Encrypt(data []byte, publicKey string) (string, error)
Decrypt(originStr string, privateKey string) (string, error)
}

type MopCredential struct {
}

type NoneCredential struct {
}

type AKSKCredential struct {
}

func NewMopCredential() *MopCredential {
return &MopCredential{}
}

func NewAKSKCredential() *AKSKCredential {
return &AKSKCredential{}
}

func NewNoneCredential() *NoneCredential {
return &NoneCredential{}
}

var MopCredentialInstance = NewMopCredential()
var NoneCredentialInstance = NewNoneCredential()
var AKSKCredentialInstance = NewAKSKCredential()

func GetCredentialManager(credType CredentialType) ICredential {
switch credType {
case CredentialAkSk:
return AKSKCredentialInstance
case CredentialMop:
return MopCredentialInstance
case CredentialNone:
return NoneCredentialInstance
default:
return AKSKCredentialInstance
}
}

func (none *NoneCredential) Sign(request *request.HttpRequest, credential *Credential) error {
request.BuildQueryParamsString()
return nil
}

func (none *NoneCredential) Encrypt(data []byte, publicKey string) (string, error) {
return string(data), nil
}

func (none *NoneCredential) Decrypt(originStr string, privateKey string) (string, error) {
return originStr, nil
}

func (mop *MopCredential) Sign(request *request.HttpRequest, credential *Credential) error {

if utils.IsUnSet(credential.PrivateKey) {
return errs.NewInvalidParameterError("RSA private key can not be null", nil)
}

request.ConvertQueryParamsFromPath()
parameters := make(map[string]string)
for k, v := range request.QueryParams {
if utils.IsSet(k) {
parameters[k] = v
}
}
flowdId := utils.Nonce()
parameters["flowdId"] = flowdId

keys := make([]string, len(parameters))
index := 0
for key := range parameters {
keys[index] = key
index++
}
sort.Strings(keys)
builder := strings.Builder{}
pos := 0
paramsLen := len(keys)
for _, key := range keys {
value := parameters[key]
builder.WriteString(utils.PercentEncode(key))
builder.WriteString(consts.QuerySeparator)
builder.WriteString(utils.PercentEncode(value))
if pos != paramsLen-1 {
builder.WriteString(consts.ParameterSeparator)
pos++
}
}
canonicalQueryString := builder.String()
keyBytes, err := utils.Base64Decode(*credential.PrivateKey)
if err != nil {
return err
}
privateKey, err := x509.ParsePKCS8PrivateKey(keyBytes)
if err != nil {
return errs.NewCredentialError("ParsePKCS8PrivateKey decode error", err)
}
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return errs.NewCredentialError("privateKey convert error", nil)
}
signature, err := utils.GenerateRSASignature(utils.StringToBytes(canonicalQueryString), rsaPrivateKey)
if err != nil {
return errs.NewCredentialError("GenerateRSASignature error", err)
}
signStr := utils.Base64Encode(signature)
request.QueryParams["sign"] = signStr
request.QueryParams["flowdId"] = flowdId
GetCredentialManager(CredentialAkSk).Sign(request, credential)
return nil
}

func (mop *MopCredential) Encrypt(data []byte, publicKey string) (string, error) {
if utils.IsUnSet(publicKey) {
return "", errs.NewInvalidParameterError("RSA public key can not be null", nil)
}
keyBytes, err := utils.Base64Decode(publicKey)
if err != nil {
return "", errs.NewInvalidParameterError("RSA public key is invalid", nil)
}
publicKeyInterface, err := x509.ParsePKIXPublicKey(keyBytes)
if err != nil {
return "", errs.NewCredentialError("ParsePKIXPublicKey decode error", nil)
}
rsaPublicKey, ok := publicKeyInterface.(*rsa.PublicKey)
if !ok {
return "", errs.NewCredentialError("publicKey convert error", nil)
}
var encryptedData []byte
for len(data) > 0 {
var chunk []byte
if len(data) > 64 {
chunk, err = rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, data[:64])
data = data[64:]
} else {
chunk, err = rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, data)
data = nil
}
if err != nil {
return "", errs.NewCredentialError("RSAPublicKeyEncrypt error", err)
}
encryptedData = append(encryptedData, chunk...)
}
return utils.Base64Encode(encryptedData), nil
}

func (mop *MopCredential) Decrypt(originStr string, privateKey string) (string, error) {
if utils.IsUnSet(privateKey) {
return "", errs.NewInvalidParameterError("RSA private key can not be null", nil)
}
data, err := utils.Base64Decode(originStr)
if err != nil {
return "", err
}
keyBytes, err := utils.Base64Decode(privateKey)
if err != nil {
return "", errs.NewInvalidParameterError("RSA private key is invalid", nil)
}
privateKeyInterface, err := x509.ParsePKCS8PrivateKey(keyBytes)
if err != nil {
return "", errs.NewCredentialError("ParsePKCS8PrivateKey decode error", nil)
}
rsaPrivateKey, ok := privateKeyInterface.(*rsa.PrivateKey)
if !ok {
return "", errs.NewCredentialError("privateKey convert error", nil)
}
var decryptedData []byte
for len(data) > 0 {
var chunk []byte
if len(data) > 75 {
chunk, err = rsa.DecryptPKCS1v15(rand.Reader, rsaPrivateKey, data[:75])
data = data[75:]
} else {
chunk, err = rsa.DecryptPKCS1v15(rand.Reader, rsaPrivateKey, data)
data = nil
}
if err != nil {
return "", errs.NewCredentialError("RSAPrivateKeyDecrypt error", err)
}
decryptedData = append(decryptedData, chunk...)
}
return string(decryptedData), nil
}

func (aksk *AKSKCredential) Sign(request *request.HttpRequest, credential *Credential) error {
request.ConvertQueryParamsFromPath()
params := make(map[string]string)
for key, value := range request.QueryParams {
params[key] = value
}
params[consts.AccessKey] = *credential.AccessKey
loc, _ := time.LoadLocation("Asia/Shanghai")
var now time.Time
if loc != nil {
now = time.Now().In(loc)
} else {
now = time.Now()
}
params[consts.Timetamp] = now.Format(consts.TimestampFormat)
params[consts.SignatureMethod] = consts.SignatureMethodValue
params[consts.SignatureVersion] = consts.SignatureVersionValue
params[consts.SignatureNonce] = utils.Nonce()
keys := make([]string, len(params))
index := 0
for key := range params {
keys[index] = key
index++
}
sort.Strings(keys)
builder := strings.Builder{}
pos := 0
paramsLen := len(keys)
for _, key := range keys {
value := params[key]
builder.WriteString(utils.PercentEncode(key))
builder.WriteString(consts.QuerySeparator)
builder.WriteString(utils.PercentEncode(value))
if pos != paramsLen-1 {
builder.WriteString(consts.ParameterSeparator)
pos++
}
}
canonicalQueryString := builder.String()
hashString := utils.ConvertToHexString(utils.Sha256Encode(canonicalQueryString))
unescapedPath, err := url.QueryUnescape(request.Path)
if nil != err {
return errs.NewSignatureError(err.Error(), err)
}
builder.Reset()
builder.WriteString(strings.ToUpper(request.Method))
builder.WriteString(consts.LineSeparator)
builder.WriteString(utils.PercentEncode(unescapedPath))
builder.WriteString(consts.LineSeparator)
builder.WriteString(hashString)
stringToSign := builder.String()
signature := utils.ConvertToHexString(utils.HmacSha256(stringToSign, consts.SecretKeyPrefix+*credential.SecretKey))
builder.Reset()
builder.WriteString(unescapedPath)
builder.WriteString(consts.QueryStartSymbol)
builder.WriteString(canonicalQueryString)
builder.WriteString(consts.ParameterSeparator)
builder.WriteString(consts.Signature)
builder.WriteString(consts.QuerySeparator)
builder.WriteString(utils.PercentEncode(signature))
request.Path = builder.String()
return nil
}

func (aksk *AKSKCredential) Encrypt(data []byte, publicKey string) (string, error) {
return string(data), nil
}

func (aksk *AKSKCredential) Decrypt(originStr string, privateKey string) (string, error) {
return originStr, nil
}

func CredentialTypePointer(a CredentialType) *CredentialType {
return &a
}

func EncryptionTypePointer(a EncryptionType) *EncryptionType {
return &a
}

+ 175
- 0
third_party/ecloudsdkcore/auth/provider/providers.go Просмотреть файл

@@ -0,0 +1,175 @@
package provider

import (
"bufio"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/auth"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
"os"
"path/filepath"
"strings"
)

type ICredentialProvider interface {
GetCredential() (*auth.Credential, error)
}

type BasicCredentialProvider struct {
credential *auth.Credential
}

func NewBasicCredentialProvider(credential *auth.Credential) *BasicCredentialProvider {
if utils.IsUnSet(credential) {
return nil
}
if utils.IsUnSet(credential.CredentialType) {
credential.CredentialType = auth.CredentialTypePointer(auth.CredentialAkSk)
}
if utils.IsUnSet(credential.EncryptionType) {
credential.EncryptionType = auth.EncryptionTypePointer(auth.EncrytNone)
}
return &BasicCredentialProvider{
credential: credential,
}
}

func (p *BasicCredentialProvider) GetCredential() (*auth.Credential, error) {
return p.credential, nil
}

func CreateBasicCredentialProvider(credential *auth.Credential) *BasicCredentialProvider {
return NewBasicCredentialProvider(credential)
}

type EnvCredentialProvider struct {
}

func NewEnvCredentialProvider() *EnvCredentialProvider {
return &EnvCredentialProvider{}
}

func (p *EnvCredentialProvider) GetCredential() (*auth.Credential, error) {
accessKey := os.Getenv(auth.EnvAkKey)
secretKey := os.Getenv(auth.EnvSkKey)
privateKey := os.Getenv(auth.EnvMopPrivateKey)
publicKey := os.Getenv(auth.EnvMopPublicKey)
if utils.IsSet(accessKey) && utils.IsSet(secretKey) {
return auth.NewCredentialBuilder().
AccessKey(accessKey).
SecretKey(secretKey).
PrivateKey(privateKey).
PublicKey(publicKey).
Build(), nil
}
return nil, errs.NewCredentialError("EnvCredentialProvider: accessKey or secretKey cannot be empty", nil)
}

func CreateEnvCredentialProvider() *EnvCredentialProvider {
return NewEnvCredentialProvider()
}

type ProfileCredentialProvider struct {
Credential *auth.Credential
profilePath string
}

func NewProfileCredentialProvider(credential *auth.Credential, profilePath string) *ProfileCredentialProvider {
return &ProfileCredentialProvider{Credential: credential, profilePath: profilePath}
}

func getProfilePath(profilePath string) (string, error) {
_, err := os.Stat(profilePath)
if err != nil {
return "", errs.NewCredentialError("Get the profile directory error", err)
}
return profilePath, nil
}

func getProfilePathByDefault() (string, error) {
dir, err := os.UserHomeDir()
if err != nil {
return "", errs.NewCredentialError("Get the user home directory error", err)

}
dir = filepath.Join(dir, strings.ReplaceAll(auth.PathCredentialFile, "/", string(filepath.Separator)))
_, err = os.Stat(dir)
if err != nil {
return "", errs.NewCredentialError("Get the profile directory error", err)
}
return dir, nil
}

func (p *ProfileCredentialProvider) GetCredential() (*auth.Credential, error) {
path, ok := os.LookupEnv(auth.EnvCredentialFile)
if !ok {
var err error
if p.profilePath != "" {
path, err = getProfilePath(p.profilePath)
} else {
path, err = getProfilePathByDefault()
}
if err != nil {
return nil, err
}
}
if path == "" {
return nil, errs.NewCredentialError(auth.EnvCredentialFile+" cannot be empty", nil)
}

file, err := os.Open(path)
if err != nil {
return nil, errs.NewCredentialError("Open the profile file error", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
config := make(map[string]string)
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
config[key] = value
}
}
return auth.NewCredentialBuilder().
AccessKey(config[auth.ProfileAccessKey]).
SecretKey(config[auth.ProfileSecretKey]).
PrivateKey(config[auth.ProfileMopPrivateKey]).
PublicKey(config[auth.ProfileMopPublicKey]).
Build(), nil
}

func CreateProfileCredentialProvider(credential *auth.Credential, profilePath string) *ProfileCredentialProvider {
return NewProfileCredentialProvider(credential, profilePath)
}

func CreateProfileCredentialProviderByDefault() *ProfileCredentialProvider {
return NewProfileCredentialProvider(auth.NewCredential(), "")
}

func CreateProfileCredentialProviderByPath(profilePath string) *ProfileCredentialProvider {
return NewProfileCredentialProvider(auth.NewCredential(), profilePath)
}

type CredentialProviderChain struct {
providers []ICredentialProvider
}

func NewCredentialProviderChain(providers ...ICredentialProvider) *CredentialProviderChain {
return &CredentialProviderChain{providers: providers}
}

func (c *CredentialProviderChain) GetCredential() (*auth.Credential, error) {
for _, provider := range c.providers {
credential, err := provider.GetCredential()
if err == nil && credential != nil {
return credential, nil
}
}
return nil, errs.NewCredentialError("no valid credential found", nil)
}

func CreateCredentialProviderChain(providers ...ICredentialProvider) *CredentialProviderChain {
return NewCredentialProviderChain(providers...)
}

+ 215
- 0
third_party/ecloudsdkcore/config/config.go Просмотреть файл

@@ -0,0 +1,215 @@
package config

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/auth/provider"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type Config struct {
AccessKey *string `json:"accessKey"`
SecretKey *string `json:"secretKey"`
SecurityToken *string `json:"securityToken"`
PoolId *string `json:"poolId"`
RegionId *string `json:"regionId"`
ReadTimeout *int32 `json:"readTimeout"`
ConnectTimeout *int32 `json:"connectTimeout"`
Protocol *string `json:"protocol"`
AutoRetry *bool `json:"autoRetry"`
IgnoreSSL *bool `json:"ignoreSSL"`
CertFile *string `json:"certFile"`
ClientCertFile *string `json:"clientCertFile"`
ClientKeyFile *string `json:"clientKeyFile"`
MaxRetryTimes *int32 `json:"maxRetryTimes"`
RetryPeriod *int64 `json:"retryPeriod"`
MaxDuringTime *int64 `json:"maxDuringTime"`
HttpProxy *string `json:"httpProxy"`
HttpsProxy *string `json:"httpsProxy"`
ClientProxyUsername *string `json:"clientProxyUsername"`
ClientProxyPassword *string `json:"clientProxyPassword"`
ClientProxyHost *string `json:"clientProxyHost"`
ClientProxyPort *int32 `json:"clientProxyPort"`
ClientProxyProtocol *string `json:"clientProxyProtocol"`
Source *string `json:"source"`
IgnoreGateway *bool `json:"ignoreGateway"`
CentralTransportEnabled *bool `json:"centralTransportEnabled"`
GlobalQueryParams map[string]string `json:"globalQueryParams"`
GlobalHeaderParams map[string]string `json:"globalHeaderParams"`
Provider provider.ICredentialProvider `json:"provider"`
}

func NewConfig() *Config {
return &Config{
Protocol: utils.String("http"),
IgnoreSSL: utils.Bool(true),
IgnoreGateway: utils.Bool(false),
CentralTransportEnabled: utils.Bool(true),
GlobalHeaderParams: map[string]string{},
GlobalQueryParams: map[string]string{},
}
}

func (c *Config) String() string {
return utils.Beautify(c)
}

func (c *Config) GoString() string {
return c.String()
}

func (c *Config) ToJsonString() string {
return utils.ToJsonString(c)
}

type ConfigBuilder struct {
config *Config
}

func NewConfigBuilder() *ConfigBuilder {
config := NewConfig()
b := &ConfigBuilder{config: config}
return b
}

func (c *ConfigBuilder) AccessKey(accessKey string) *ConfigBuilder {
c.config.AccessKey = utils.String(accessKey)
return c
}

func (c *ConfigBuilder) SecretKey(secretKey string) *ConfigBuilder {
c.config.SecretKey = utils.String(secretKey)
return c
}

func (c *ConfigBuilder) SecurityToken(securityToken string) *ConfigBuilder {
c.config.SecurityToken = utils.String(securityToken)
return c
}

func (c *ConfigBuilder) PoolId(poolId string) *ConfigBuilder {
c.config.PoolId = utils.String(poolId)
return c
}

func (c *ConfigBuilder) RegionId(regionId string) *ConfigBuilder {
c.config.RegionId = utils.String(regionId)
return c
}

func (c *ConfigBuilder) ReadTimeOut(readTimeOut int32) *ConfigBuilder {
c.config.ReadTimeout = utils.Int32(readTimeOut)
return c
}

func (c *ConfigBuilder) ConnectTimeout(connectTimeout int32) *ConfigBuilder {
c.config.ConnectTimeout = utils.Int32(connectTimeout)
return c
}

func (c *ConfigBuilder) Protocol(protocol string) *ConfigBuilder {
c.config.Protocol = utils.String(protocol)
return c
}

func (c *ConfigBuilder) AutoRetry(autoRetry bool) *ConfigBuilder {
c.config.AutoRetry = utils.Bool(autoRetry)
return c
}

func (c *ConfigBuilder) IgnoreSSL(ignoreSSL bool) *ConfigBuilder {
c.config.IgnoreSSL = utils.Bool(ignoreSSL)
return c
}

func (c *ConfigBuilder) CertFile(certFile string) *ConfigBuilder {
c.config.CertFile = utils.String(certFile)
return c
}

func (c *ConfigBuilder) ClientCertFile(clientCertFile string) *ConfigBuilder {
c.config.ClientCertFile = utils.String(clientCertFile)
return c
}

func (c *ConfigBuilder) ClientKeyFile(clientKeyFile string) *ConfigBuilder {
c.config.ClientKeyFile = utils.String(clientKeyFile)
return c
}

func (c *ConfigBuilder) MaxRetryTimes(maxRetryTimes int32) *ConfigBuilder {
c.config.MaxRetryTimes = utils.Int32(maxRetryTimes)
return c
}

func (c *ConfigBuilder) RetryPeriod(retryPeriod int64) *ConfigBuilder {
c.config.RetryPeriod = utils.Int64(retryPeriod)
return c
}

func (c *ConfigBuilder) MaxDuringTime(maxDuringTime int64) *ConfigBuilder {
c.config.MaxDuringTime = utils.Int64(maxDuringTime)
return c
}

func (c *ConfigBuilder) HttpProxy(httpProxy string) *ConfigBuilder {
c.config.HttpProxy = utils.String(httpProxy)
return c
}

func (c *ConfigBuilder) HttpsProxy(httpsProxy string) *ConfigBuilder {
c.config.HttpsProxy = utils.String(httpsProxy)
return c
}

func (c *ConfigBuilder) ClientProxyUsername(clientProxyUsername string) *ConfigBuilder {
c.config.ClientProxyUsername = utils.String(clientProxyUsername)
return c
}

func (c *ConfigBuilder) ClientProxyPassword(clientProxyPassword string) *ConfigBuilder {
c.config.ClientProxyPassword = utils.String(clientProxyPassword)
return c
}

func (c *ConfigBuilder) ClientProxyHost(clientProxyHost string) *ConfigBuilder {
c.config.ClientProxyHost = utils.String(clientProxyHost)
return c
}

func (c *ConfigBuilder) ClientProxyPort(clientProxyPort int32) *ConfigBuilder {
c.config.ClientProxyPort = utils.Int32(clientProxyPort)
return c
}

func (c *ConfigBuilder) ClientProxyProtocol(clientProxyProtocol string) *ConfigBuilder {
c.config.ClientProxyProtocol = utils.String(clientProxyProtocol)
return c
}

func (c *ConfigBuilder) IgnoreGateway(ignoreGateway bool) *ConfigBuilder {
c.config.IgnoreGateway = utils.Bool(ignoreGateway)
return c
}

func (c *ConfigBuilder) CentralTransportEnabled(centralTransportEnabled bool) *ConfigBuilder {
c.config.CentralTransportEnabled = utils.Bool(centralTransportEnabled)
return c
}

func (c *ConfigBuilder) GlobalQueryParams(globalQueryParams map[string]string) *ConfigBuilder {
c.config.GlobalQueryParams = globalQueryParams
return c
}

func (c *ConfigBuilder) GlobalHeaderParams(globalHeaderParams map[string]string) *ConfigBuilder {
c.config.GlobalHeaderParams = globalHeaderParams
return c
}

func (c *ConfigBuilder) Provider(provider provider.ICredentialProvider) *ConfigBuilder {
c.config.Provider = provider
return c
}

func (c *ConfigBuilder) Build() *Config {
return c.config
}

+ 131
- 0
third_party/ecloudsdkcore/config/runtime_config.go Просмотреть файл

@@ -0,0 +1,131 @@
package config

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/auth/provider"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type RuntimeConfig struct {
ReadTimeout *int32 `json:"readTimeout"`
ConnectTimeout *int32 `json:"connectTimeout"`
AutoRetry *bool `json:"autoRetry"`
IgnoreSSL *bool `json:"ignoreSSL"`
CertFile *string `json:"certFile"`
ClientCertFile *string `json:"clientCertFile"`
ClientKeyFile *string `json:"clientKeyFile"`
MaxRetryTimes *int32 `json:"maxRetryTimes"`
RetryPeriod *int64 `json:"retryPeriod"`
MaxDuringTime *int64 `json:"maxDuringTime"`
HttpProxy *string `json:"httpProxy"`
HttpsProxy *string `json:"httpsProxy"`
IgnoreGateway *bool `json:"ignoreGateway"`
CentralTransportEnabled *bool `json:"centralTransportEnabled"`
RuntimeHeaderParams map[string]string `json:"runtimeHeaderParams"`
Provider provider.ICredentialProvider `json:"provider"`
}

func (r *RuntimeConfig) String() string {
return utils.Beautify(r)
}

func (r *RuntimeConfig) GoString() string {
return r.String()
}

func (r *RuntimeConfig) ToJsonString() string {
return utils.ToJsonString(r)
}

type RuntimeConfigBuilder struct {
runtimeConfig *RuntimeConfig
}

func NewRuntimeConfigBuilder() *RuntimeConfigBuilder {
runtimeConfig := &RuntimeConfig{}
r := &RuntimeConfigBuilder{runtimeConfig: runtimeConfig}
return r
}

func (r *RuntimeConfigBuilder) ReadTimeOut(readTimeOut int32) *RuntimeConfigBuilder {
r.runtimeConfig.ReadTimeout = utils.Int32(readTimeOut)
return r
}

func (r *RuntimeConfigBuilder) ConnectTimeout(connectTimeout int32) *RuntimeConfigBuilder {
r.runtimeConfig.ConnectTimeout = utils.Int32(connectTimeout)
return r
}

func (r *RuntimeConfigBuilder) AutoRetry(autoRetry bool) *RuntimeConfigBuilder {
r.runtimeConfig.AutoRetry = utils.Bool(autoRetry)
return r
}

func (r *RuntimeConfigBuilder) IgnoreSSL(ignoreSSL bool) *RuntimeConfigBuilder {
r.runtimeConfig.IgnoreSSL = utils.Bool(ignoreSSL)
return r
}

func (r *RuntimeConfigBuilder) CertFile(certFile string) *RuntimeConfigBuilder {
r.runtimeConfig.CertFile = utils.String(certFile)
return r
}

func (r *RuntimeConfigBuilder) ClientCertFile(clientCertFile string) *RuntimeConfigBuilder {
r.runtimeConfig.ClientCertFile = utils.String(clientCertFile)
return r
}

func (r *RuntimeConfigBuilder) ClientKeyFile(clientKeyFile string) *RuntimeConfigBuilder {
r.runtimeConfig.ClientKeyFile = utils.String(clientKeyFile)
return r
}

func (r *RuntimeConfigBuilder) MaxRetryTimes(maxRetryTimes int32) *RuntimeConfigBuilder {
r.runtimeConfig.MaxRetryTimes = utils.Int32(maxRetryTimes)
return r
}

func (r *RuntimeConfigBuilder) RetryPeriod(retryPeriod int64) *RuntimeConfigBuilder {
r.runtimeConfig.RetryPeriod = utils.Int64(retryPeriod)
return r
}

func (r *RuntimeConfigBuilder) MaxDuringTime(maxDuringTime int64) *RuntimeConfigBuilder {
r.runtimeConfig.MaxDuringTime = utils.Int64(maxDuringTime)
return r
}

func (r *RuntimeConfigBuilder) HttpProxy(httpProxy string) *RuntimeConfigBuilder {
r.runtimeConfig.HttpProxy = utils.String(httpProxy)
return r
}

func (r *RuntimeConfigBuilder) HttpsProxy(httpsProxy string) *RuntimeConfigBuilder {
r.runtimeConfig.HttpsProxy = utils.String(httpsProxy)
return r
}

func (r *RuntimeConfigBuilder) IgnoreGateway(ignoreGateway bool) *RuntimeConfigBuilder {
r.runtimeConfig.IgnoreGateway = utils.Bool(ignoreGateway)
return r
}

func (r *RuntimeConfigBuilder) CentralTransportEnabled(centralTransportEnabled bool) *RuntimeConfigBuilder {
r.runtimeConfig.CentralTransportEnabled = utils.Bool(centralTransportEnabled)
return r
}

func (r *RuntimeConfigBuilder) RuntimeHeaderParams(runtimeHeaderParams map[string]string) *RuntimeConfigBuilder {
r.runtimeConfig.RuntimeHeaderParams = runtimeHeaderParams
return r
}

func (r *RuntimeConfigBuilder) Provider(provider provider.ICredentialProvider) *RuntimeConfigBuilder {
r.runtimeConfig.Provider = provider
return r
}

func (r *RuntimeConfigBuilder) Build() *RuntimeConfig {
return r.runtimeConfig
}

+ 26
- 0
third_party/ecloudsdkcore/consts/constants.go Просмотреть файл

@@ -0,0 +1,26 @@
package consts

type AuthType string

const (
AK AuthType = "AK"
Token = "TOKEN"
NoAuth = "NO_AUTH"
)

const (
AccessKey = "AccessKey"
Timetamp = "Timestamp"
TimestampFormat = "2006-01-02T15:04:05Z"
Signature = "Signature"
SecretKeyPrefix = "BC_SIGNATURE&"
SignatureMethod = "SignatureMethod"
SignatureMethodValue = "HmacSHA256"
SignatureVersion = "SignatureVersion"
SignatureVersionValue = "V2.0"
SignatureNonce = "SignatureNonce"
LineSeparator = "\n"
ParameterSeparator = "&"
QueryStartSymbol = "?"
QuerySeparator = "="
)

+ 106
- 0
third_party/ecloudsdkcore/errs/errors.go Просмотреть файл

@@ -0,0 +1,106 @@
package errs

import (
"fmt"
)

type SdkError struct {
ErrorType string
ErrorMessage string
error error
}

func NewSdkError(errorType string, msg string, err error) *SdkError {
return &SdkError{
ErrorType: errorType,
ErrorMessage: msg,
error: err,
}
}

func (err *SdkError) Error() string {
return fmt.Sprintf("{\"errorType\": \"%s\", \"error-message\": \"%s\"}", err.ErrorType, err.ErrorMessage)
}
func (err *SdkError) DetailError() string {
return fmt.Sprintf("{\"errorType\": \"%s\", \"error-message\": \"%s\", \"error\": \"%s\"}", err.ErrorType, err.ErrorMessage, err.error)
}

type ServerRequestError struct {
*SdkError
}

func NewServerRequestError(msg string, err error) *ServerRequestError {
return &ServerRequestError{
NewSdkError("ServerRequestError", msg, err),
}
}

type ServerResponseError struct {
*SdkError
Code int
Headers map[string][]string
Body string
}

func NewServerResponseError(msg string, err error, code int, headers map[string][]string, body string) *ServerResponseError {
message := msg
if len(message) == 0 {
message = fmt.Sprintf("code=%d", code)
}
return &ServerResponseError{
SdkError: NewSdkError("ServerResponseError", message, err),
Code: code,
Headers: headers,
Body: body,
}
}

type ConnectionTimeOutError struct {
*SdkError
}

func NewConnectionTimeOutError(msg string, err error) *ConnectionTimeOutError {
return &ConnectionTimeOutError{
SdkError: NewSdkError("ConnectionTimeOutError", msg, err),
}
}

type SignatureError struct {
*SdkError
}

func NewSignatureError(msg string, err error) *SignatureError {
return &SignatureError{
SdkError: NewSdkError("SignatureError", msg, err),
}
}

type SslHandShakeError struct {
*SdkError
}

func NewSslHandShakeError(msg string, err error) *SslHandShakeError {
return &SslHandShakeError{
SdkError: NewSdkError("SslHandShakeError", msg, err),
}
}

type InvalidParameterError struct {
*SdkError
}

func NewInvalidParameterError(msg string, err error) *InvalidParameterError {
return &InvalidParameterError{
NewSdkError("InvalidParameterError", msg, err),
}
}

type CredentialError struct {
*SdkError
}

func NewCredentialError(msg string, err error) *CredentialError {
return &CredentialError{
NewSdkError("CredentialError", msg, err),
}
}

+ 5
- 0
third_party/ecloudsdkcore/go.mod Просмотреть файл

@@ -0,0 +1,5 @@
module gitlab.ecloud.com/ecloud/ecloudsdkcore

go 1.14

require github.com/google/uuid v1.6.0

+ 280
- 0
third_party/ecloudsdkcore/http/http_client.go Просмотреть файл

@@ -0,0 +1,280 @@
package http

import (
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/auth"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/auth/provider"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/request"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/response"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
)

type NetHttpClient struct {
}

type RetryFunc func(request *request.HttpRequest,
returnType *interface{}) (*response.HttpResponse, error)

func NewHttpClient() *NetHttpClient {
return &NetHttpClient{}
}

var clientPool = &sync.Map{}

// doRequest do the request.
func (hc *NetHttpClient) getClient(request *request.HttpRequest, cm map[string]interface{}) (*http.Client, error) {
var host string

if cm["ClientProxyHost"] != nil && cm["ClientProxyPort"] != nil {
host = fmt.Sprintf("%s:%s", *cm["ClientProxyHost"].(*string), *cm["ClientProxyPort"].(*string))
} else {
url, err := url.Parse(request.Url)
if err != nil {
return nil, errs.NewInvalidParameterError(fmt.Sprintf("request url invalid, url=: %s", request.Url), err)
}
host = url.Host
}
client, ok := clientPool.Load(host)
if client == nil && !ok {
var err error
client, err = hc.buildHttpClient(cm)
if err != nil {
return nil, err
}
clientPool.Store(host, client)
}
return client.(*http.Client), nil
}

func (hc *NetHttpClient) Execute(hr *request.HttpRequest, cm map[string]interface{}, rt interface{}) (*response.HttpResponse, error) {
req, err := hc.buildRequest(hr, cm)
if err != nil {
return nil, err
}
client, err := hc.getClient(hr, cm)
if err != nil {
return nil, errs.NewServerRequestError("get http error: ", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, errs.NewServerRequestError(err.Error(), err)
}
if resp == nil {
return nil, errs.NewServerResponseError("response is nil", nil, -1, nil, "")
}
if err = handleResponse(resp, cm, rt); err != nil {
return nil, err
}
return &response.HttpResponse{
StatusCode: resp.StatusCode,
Headers: resp.Header,
Data: resp,
}, nil
}

func (hc *NetHttpClient) buildHttpClient(cm map[string]interface{}) (*http.Client, error) {
hcb := NewHttpClientBuilder()
if utils.IsSet(cm["ReadTimeout"]) {
hcb.SetReadTimeout(utils.Int32Value(cm["ReadTimeout"].(*int32)))
}

if utils.IsSet(cm["ConnectTimeout"]) {
hcb.SetConnectTimeout(utils.Int32Value(cm["ConnectTimeout"].(*int32)))
}

if utils.IsSet(cm["ClientProxyHost"]) {
proxy := Proxy{
Protocol: cm["ClientProxyProtocol"].(*string),
Host: cm["ClientProxyHost"].(*string),
Port: cm["ClientProxyPort"].(*int32),
Username: cm["ClientProxyUsername"].(*string),
Password: cm["ClientProxyPassword"].(*string),
}
hcb.SetClientProxy(proxy)
}
ignore := true
if utils.IsSet(cm["IgnoreSSL"]) {
ignore = utils.BoolValue(cm["IgnoreSSL"].(*bool))
}
certFile := ""
if utils.IsSet(cm["CertFile"]) {
certFile = utils.StringValue(cm["CertFile"].(*string))
}
clientCertFile := ""
if utils.IsSet(cm["ClientCertFile"]) {
clientCertFile = utils.StringValue(cm["ClientCertFile"].(*string))
}
clientKeyFile := ""
if utils.IsSet(cm["ClientKeyFile"]) {
clientKeyFile = utils.StringValue(cm["ClientKeyFile"].(*string))
}
err := hcb.ApplySSLSettings(ignore, certFile, clientCertFile, clientKeyFile)
if err != nil {
return nil, err
}
return hcb.Build(), nil
}

func handleResponse(resp *http.Response, cm map[string]interface{}, returnType interface{}) error {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
if resp.Body != nil {
_ = resp.Body.Close()
}
return errs.NewServerResponseError(fmt.Sprintf("response status is: %s,status code is:%d",
resp.Status, resp.StatusCode), err, resp.StatusCode, resp.Header, "can not read response body")
}
// successful
if isSuccessful(resp.StatusCode) {
if returnType == nil || resp.StatusCode == 204 {
if resp.Body != nil {
_ = resp.Body.Close()
}
return nil
}
// If we succeed, return the data, otherwise pass on to deserialize error.
err = deserialize(returnType, body, cm, resp.Header.Get("Content-Type"))
if err != nil {
return errs.NewServerResponseError(fmt.Sprintf("can't deserialize response body with content-type: %s,response body:%s",
resp.Header.Get("Content-Type"), string(body)), err, resp.StatusCode, resp.Header, string(body))
}
} else {
if body == nil {
return errs.NewServerResponseError(fmt.Sprintf("response status is: %s,status code is:%d",
resp.Status, resp.StatusCode), nil, resp.StatusCode, resp.Header, "response body is nil")
}
respBody := string(body)
return errs.NewServerResponseError(fmt.Sprintf("response status: %s,http status code:%d,response body:%s",
resp.Status, resp.StatusCode, respBody), nil, resp.StatusCode, resp.Header, respBody)
}
return nil
}

func isSuccessful(code int) bool {
return code >= 200 && code < 300
}

func (hc *NetHttpClient) buildRequest(hr *request.HttpRequest, cm map[string]interface{}) (request *http.Request, err error) {
req, err := prepareRequest(hr, cm)
if err != nil {
return nil, err
}
return req, nil
}

// prepareRequest build the request
func prepareRequest(hr *request.HttpRequest, cm map[string]interface{}) (req *http.Request, err error) {
var body *bytes.Buffer
rawBody := hr.Body
contentType := hr.ContentType
// Detect rawBody
if rawBody != nil {
if utils.IsSet(cm["Provider"]) {
provider := cm["Provider"].(provider.ICredentialProvider)
credential, err := provider.GetCredential()
if err != nil {
return nil, err
}
if *credential.CredentialType == auth.CredentialMop && *credential.EncryptionType == auth.EncrytMopRsa {
bodyBuf := &bytes.Buffer{}
err := json.NewEncoder(bodyBuf).Encode(rawBody)
if err != nil {
return nil, err
}
encryptContent, err := auth.GetCredentialManager(*credential.CredentialType).Encrypt(bodyBuf.Bytes(), *credential.PublicKey)
if err != nil {
return nil, err
}
rawBody = encryptContent
}
}
contentType = utils.DetectContentType(rawBody)
body, err = utils.SetBody(rawBody, contentType)
if err != nil {
return nil, errs.NewServerRequestError(fmt.Sprintf("set request body error: %s", err.Error()), err)
}
}

// Setup url and query parameters, url contains query strings
rawUrl := hr.Url
if len(rawUrl) == 0 {
return nil, errs.NewServerRequestError("request url is empty", nil)
}
realUrl, err := url.Parse(rawUrl)
if err != nil {
return nil, errs.NewServerRequestError(fmt.Sprintf("can't parse request url: %s, "+
"error is: %s", rawUrl, err.Error()), err)
}

// Generate a new http.Request
method := hr.Method
if body != nil {
req, err = http.NewRequest(method, realUrl.String(), body)
} else {
req, err = http.NewRequest(method, realUrl.String(), nil)
}
if err != nil {
return nil, errs.NewServerRequestError(fmt.Sprintf("can't create http request, "+
"method=%s, url=%s, error: %s", method, realUrl.String(), err.Error()), err)
}

// Add request headers
headers := hr.HeaderParams
for name, value := range headers {
if name == "Host" {
req.Host = value
continue
}
req.Header.Add(name, value)
}
if len(contentType) == 0 {
contentType = "application/json; charset=utf-8"
}
req.Header.Add("Content-Type", contentType)
return req, nil
}

func deserialize(respType interface{}, respBody []byte, configMap map[string]interface{}, contentType string) (err error) {
if respBody == nil {
//return errs.NewGenericResponseError("response body is nil", nil, respBody)
return errs.NewServerResponseError("response body is nil", nil, -1, nil, "")
}
if utils.IsSet(configMap["Provider"]) {
provider := configMap["Provider"].(provider.ICredentialProvider)
credential, err := provider.GetCredential()
if err != nil {
return err
}
if *credential.CredentialType == auth.CredentialMop && *credential.EncryptionType == auth.EncrytMopRsa {
decryptContent, err := auth.GetCredentialManager(*credential.CredentialType).Decrypt(string(respBody), *credential.PrivateKey)
if err != nil {
return err
}
respBody = utils.StringToBytes(decryptContent)
}
}
if strings.Contains(contentType, "application/xml") {
if err = xml.Unmarshal(respBody, respType); err != nil {
return errs.NewServerResponseError(fmt.Sprintf("can't deserialize response body with: %s",
string(respBody)), err, -1, nil, string(respBody))
}
return nil
} else if strings.Contains(contentType, "json") {
if err = json.Unmarshal(respBody, respType); err != nil {
return errs.NewServerResponseError(fmt.Sprintf("can't deserialize response body with: %s",
string(respBody)), err, -1, nil, string(respBody))
}
return nil
}
return errors.New("undefined response type")
}

+ 186
- 0
third_party/ecloudsdkcore/http/http_client_builder.go Просмотреть файл

@@ -0,0 +1,186 @@
package http

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
"io/ioutil"
"net"
"net/http"
"net/url"
"time"
)

type NetHttpClientBuilder struct {
goClient *http.Client
ignoreSSL bool
certFile string
clientCertFile string
clientKeyFile string
}

type Proxy struct {
Protocol *string
Host *string
Port *int32
Username *string
Password *string
}

func NewHttpClientBuilder() *NetHttpClientBuilder {
return &NetHttpClientBuilder{ignoreSSL: true}
}

func defaultGoClient() *http.Client {
return &http.Client{
Transport: defaultTransport(),
}
}

func defaultTransport() *http.Transport {
return &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}

func (hcb *NetHttpClientBuilder) Build() *http.Client {
if hcb.goClient == nil {
return defaultGoClient()
}
return hcb.goClient
}

func (hcb *NetHttpClientBuilder) SetReadTimeout(timeout int32) {
if hcb.goClient == nil {
hcb.goClient = defaultGoClient()
}
hcb.goClient.Timeout = time.Duration(timeout) * time.Second
}

func (hcb *NetHttpClientBuilder) SetConnectTimeout(timeout int32) {
if hcb.goClient == nil {
hcb.goClient = defaultGoClient()
}
transport := hcb.goClient.Transport.(*http.Transport)
transport.DialContext = hcb.setDialContext(timeout)
hcb.goClient.Transport = transport
}

func (hcb *NetHttpClientBuilder) SetClientProxy(proxy Proxy) {
if hcb.goClient == nil {
hcb.goClient = defaultGoClient()
}
transport := hcb.goClient.Transport.(*http.Transport)
proxyUrl := proxy.GetProxyUrl()
if proxyUrl != "" {
proxy, _ := url.Parse(proxyUrl)
transport.Proxy = http.ProxyURL(proxy)
}
hcb.goClient.Transport = transport
}

func (p *Proxy) GetProxyUrl() string {
var proxyUrl string
if p.Username != nil {
proxyUrl = fmt.Sprintf("%s://%s:%s@%s", *p.Protocol, *p.Username, *p.Password, *p.Host)
} else {
proxyUrl = fmt.Sprintf("%s://%s", *p.Protocol, *p.Host)
}
if p.Port != nil {
proxyUrl = fmt.Sprintf("%s:%d", proxyUrl, *p.Port)
}
return proxyUrl
}

func (hcb *NetHttpClientBuilder) setDialContext(timeout int32) func(ctx context.Context, network, addr string) (net.Conn, error) {
return func(ctx context.Context, network, address string) (net.Conn, error) {
return (&net.Dialer{
Timeout: time.Duration(timeout) * time.Second,
DualStack: true,
}).DialContext(ctx, network, address)
}
}

func (hcb *NetHttpClientBuilder) SetIgnoreSSL(ignoreSSL bool) {
if ignoreSSL != hcb.ignoreSSL {
transport := defaultTransport()
transport.TLSClientConfig.InsecureSkipVerify = ignoreSSL
hcb.goClient.Transport = transport
hcb.ignoreSSL = ignoreSSL
}
}

func (hcb *NetHttpClientBuilder) SetCertFile(certFile string) error {
if certFile != hcb.certFile {
return hcb.ApplySSLSettings(hcb.ignoreSSL, certFile, hcb.clientCertFile, hcb.clientKeyFile)
}
return nil
}

func (hcb *NetHttpClientBuilder) SetClientKeyPairFile(clientCertFile string, clientKeyFile string) error {
if clientCertFile != hcb.clientCertFile || clientKeyFile != hcb.clientKeyFile {
return hcb.ApplySSLSettings(hcb.ignoreSSL, hcb.certFile,
hcb.clientCertFile, hcb.clientKeyFile)
}
return nil
}

func (hcb *NetHttpClientBuilder) ApplySSLSettings(ignoreSSL bool, certFile string, clientCertFile string, clientKeyFile string) error {
if ignoreSSL && hcb.ignoreSSL {
return nil
}
if ignoreSSL && !hcb.ignoreSSL {
hcb.ignoreSSL = ignoreSSL
transport := hcb.goClient.Transport.(*http.Transport)
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: ignoreSSL,
}
hcb.goClient.Transport = transport
return nil
}
if !ignoreSSL && !hcb.ignoreSSL {
if certFile == hcb.certFile && clientCertFile == hcb.clientCertFile &&
clientKeyFile == hcb.clientKeyFile {
return nil
}
}

var certP *x509.CertPool = nil
if len(certFile) > 0 {
certP = &x509.CertPool{}
pemCert, err := ioutil.ReadFile(certFile)
if err != nil {
return errs.NewSslHandShakeError(fmt.Sprintf("can't read certfile: %s", certFile), err)
}
certP.AppendCertsFromPEM(pemCert)
}
transport := hcb.goClient.Transport.(*http.Transport)
var clientCert *tls.Certificate = nil
if len(clientKeyFile) > 0 && len(clientCertFile) > 0 {
cert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile)
if err != nil {
return errs.NewSslHandShakeError(fmt.Sprintf("can't load keypair of http cert file: %s, http key file: %s",
clientCertFile, clientKeyFile), err)
}
clientCert = &cert
}

transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: ignoreSSL,
}
if certP != nil {
transport.TLSClientConfig.RootCAs = certP
}
if clientCert != nil {
transport.TLSClientConfig.Certificates = []tls.Certificate{*clientCert}
}
hcb.goClient.Transport = transport
hcb.certFile = certFile
hcb.clientCertFile = clientCertFile
hcb.clientKeyFile = clientKeyFile
return nil
}

+ 80
- 0
third_party/ecloudsdkcore/param/api_params.go Просмотреть файл

@@ -0,0 +1,80 @@
package param

import "gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"

type Params struct {
Action string `json:"action,omitempty"`
Protocol string `json:"protocol,omitempty"`
Uri string `json:"uri,omitempty"`
GatewayUri string `json:"gatewayUri,omitempty"`
Request interface{} `json:"request,omitempty"`
Method string `json:"method,omitempty"`
AuthType int32 `json:"authType,omitempty"`
ContentType string `json:"contentType,omitempty"`
}

func (p *Params) String() string {
return utils.Beautify(p)
}

func (p *Params) GoString() string {
return p.String()
}

func (p *Params) ToJsonString() string {
return utils.ToJsonString(p)
}

type Builder struct {
params *Params
}

func NewParamsBuilder() *Builder {
params := &Params{}
b := &Builder{params: params}
return b
}

func (b *Builder) Action(action string) *Builder {
b.params.Action = action
return b
}

func (b *Builder) Protocol(protocol string) *Builder {
b.params.Protocol = protocol
return b
}

func (b *Builder) Uri(uri string) *Builder {
b.params.Uri = uri
return b
}

func (b *Builder) GatewayUri(gatewayUri string) *Builder {
b.params.GatewayUri = gatewayUri
return b
}

func (b *Builder) Request(request interface{}) *Builder {
b.params.Request = request
return b
}

func (b *Builder) Method(method string) *Builder {
b.params.Method = method
return b
}

func (b *Builder) AuthType(authType int32) *Builder {
b.params.AuthType = authType
return b
}

func (b *Builder) ContentType(contentType string) *Builder {
b.params.ContentType = contentType
return b
}

func (b *Builder) Build() *Params {
return b.params
}

+ 13
- 0
third_party/ecloudsdkcore/position/http_position.go Просмотреть файл

@@ -0,0 +1,13 @@
package position

type Body struct {
}

type Query struct {
}

type Path struct {
}

type Header struct {
}

+ 7
- 0
third_party/ecloudsdkcore/region/region.go Просмотреть файл

@@ -0,0 +1,7 @@
package region

type Region struct {
PoolId *string `json:"poolId"`
Endpoint *string `json:"endpoint"`
RegionId *string `json:"regionId"`
}

+ 320
- 0
third_party/ecloudsdkcore/request/http_request.go Просмотреть файл

@@ -0,0 +1,320 @@
package request

import (
"fmt"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/consts"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/param"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
"reflect"
"strings"
)

type HttpRequest struct {
Method string `json:"method,omitempty"`
Protocol string `json:"protocol,omitempty"`
Url string `json:"url,omitempty"`
Path string `json:"path,omitempty"`
Action string `json:"action,omitempty"`
Product string `json:"product,omitempty"`
Version string `json:"version,omitempty"`
SdkVersion string `json:"sdkVersion,omitempty"`
Source string `json:"source,omitempty"`
SecurityToken string `json:"securityToken,omitempty"`
QueryString string `json:"queryString,omitempty"`
ContentType string `json:"contentType,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Body interface{} `json:"body,omitempty"`
PathParams map[string]string `json:"pathParams,omitempty"`
QueryParams map[string]string `json:"queryParams,omitempty"`
HeaderParams map[string]string `json:"headerParams,omitempty"`
}

type HttpRequestPosition string

const (
BODY HttpRequestPosition = "Body"
QUERY HttpRequestPosition = "Query"
PATH HttpRequestPosition = "Path"
HEADER HttpRequestPosition = "Header"
)

func NewHttpRequest() *HttpRequest {
return &HttpRequest{
Method: "POST",
QueryParams: make(map[string]string),
HeaderParams: make(map[string]string),
PathParams: make(map[string]string),
ContentType: "application/json; charset=utf-8",
}
}

func DefaultHttpRequest() *HttpRequest {
return NewHttpRequest()
}

func (httpReq *HttpRequest) Reset() {
httpReq.Action = ""
httpReq.SecurityToken = ""
httpReq.QueryParams = make(map[string]string)
httpReq.HeaderParams = make(map[string]string)
httpReq.PathParams = make(map[string]string)
httpReq.Body = nil
httpReq.Method = "POST"
}

func (httpReq *HttpRequest) BuildHeaderParams(headerParams map[string]string) {
if httpReq.HeaderParams == nil {
httpReq.HeaderParams = make(map[string]string)
}
for name, value := range headerParams {
httpReq.HeaderParams[name] = value
}
}

func (httpReq *HttpRequest) AddDefaultHeaders() error {
if strings.HasSuffix(httpReq.Product, "inner") && utils.IsUnSet(httpReq.Source) {
return errs.NewInvalidParameterError("the attribute named source in SDK config must not be null", nil)
}
if httpReq.HeaderParams == nil {
httpReq.HeaderParams = make(map[string]string)
}
x := fmt.Sprintf("action:%s;product:%s;version:%s;sdkversion:%s;language:Golang;coreversion:1.0.5",
httpReq.Action, httpReq.Product, httpReq.Version, httpReq.SdkVersion)
if len(httpReq.Source) > 0 {
x += fmt.Sprintf(";source:%s", httpReq.Source)
}
if len(httpReq.SecurityToken) > 0 {
httpReq.HeaderParams["x-openapi-security-token"] = httpReq.SecurityToken
}
httpReq.HeaderParams["x-openapi-sdk"] = x
httpReq.HeaderParams["User-Agent"] = "OpenAPI/2.0/Golang"
return nil
}

func (httpReq *HttpRequest) BuildQueryParams(queryParams map[string]string) {
if httpReq.QueryParams == nil {
httpReq.QueryParams = make(map[string]string)
}
for name, value := range queryParams {
httpReq.QueryParams[name] = value
}
}

func (httpReq *HttpRequest) BuildPathParams(pathParams map[string]string) {
if httpReq.PathParams == nil {
httpReq.PathParams = make(map[string]string)
}
for name, value := range pathParams {
httpReq.PathParams[name] = value
}
}

func (httpReq *HttpRequest) BuildApiParams(params *param.Params, cm map[string]interface{}) error {
if params == nil {
return nil
}

if len(params.ContentType) > 0 {
httpReq.ContentType = params.ContentType
}
if len(params.Method) > 0 {
httpReq.Method = params.Method
}
if len(params.Protocol) > 0 {
httpReq.Protocol = params.Protocol
}
ignoreGateway := cm["IgnoreGateway"].(*bool)
if utils.BoolValue(ignoreGateway) {
httpReq.Path = params.Uri
} else {
httpReq.Path = params.GatewayUri
}
if len(params.Action) > 0 {
httpReq.Action = params.Action
}
if utils.IsSet(cm["Source"]) {
httpReq.Source = *cm["Source"].(*string)
}
if httpReq.PathParams != nil && len(httpReq.PathParams) > 0 {
httpReq.BuildPathParamsString()
}
if utils.IsSet(cm["RegionId"]) {
httpReq.AddHeaders(map[string]string{"Region-Id": utils.StringValue(cm["RegionId"].(*string))})
} else {
if utils.IsSet(cm["PoolId"]) {
httpReq.AddHeaders(map[string]string{"Pool-Id": utils.StringValue(cm["PoolId"].(*string))})
}
}

if utils.IsSet(cm["SecurityToken"]) {
httpReq.SecurityToken = utils.StringValue(cm["SecurityToken"].(*string))
}
err := httpReq.AddDefaultHeaders()
if err != nil {
return err
}
if utils.IsSet(cm["GlobalQueryParams"]) && len(cm["GlobalQueryParams"].(map[string]string)) > 0 {
httpReq.BuildQueryParams(cm["GlobalQueryParams"].(map[string]string))
}
if utils.IsSet(cm["GlobalHeaderParams"]) && len(cm["GlobalHeaderParams"].(map[string]string)) > 0 {
httpReq.BuildHeaderParams(cm["GlobalHeaderParams"].(map[string]string))
}
if utils.IsSet(cm["RuntimeHeaderParams"]) && len(cm["RuntimeHeaderParams"].(map[string]string)) > 0 {
httpReq.BuildHeaderParams(cm["RuntimeHeaderParams"].(map[string]string))
}
if !utils.BoolValue(cm["CentralTransportEnabled"].(*bool)) && len(httpReq.Endpoint) > 0 {
httpReq.Url = httpReq.Endpoint
} else {
httpReq.Url = utils.DefaultEndpoint
}
if utils.IsSet(cm["HttpProxy"]) {
httpReq.Url = utils.StringValue(cm["HttpProxy"].(*string))
}
if utils.IsSet(cm["HttpsProxy"]) {
httpReq.Url = utils.StringValue(cm["HttpsProxy"].(*string))
}
return nil
}

func (httpReq *HttpRequest) BuildFinalUrl() {
url := httpReq.Url
if len(url) == 0 {
return
}
protocol := httpReq.Protocol
if len(protocol) > 0 &&
!strings.HasPrefix(url, "http") &&
!strings.HasPrefix(url, "https") {
url = protocol + "://" + url
}
path := httpReq.Path
if len(path) > 0 {
url = url + path
}
queryString := httpReq.QueryString
if len(queryString) > 0 {
url = url + queryString
}
httpReq.Url = url
}

func (httpReq *HttpRequest) BuildPathParamsString() {
for name, value := range httpReq.PathParams {
httpReq.Path = strings.ReplaceAll(httpReq.Path, fmt.Sprintf("{%s}", name), value)
}
if !strings.HasPrefix(httpReq.Path, "/") {
httpReq.Path = "/" + httpReq.Path
}
if strings.HasSuffix(httpReq.Path, "/") {
httpReq.Path = httpReq.Path[0 : len(httpReq.Path)-1]
}
}

func (httpReq *HttpRequest) BuildQueryParamsString() {
build := strings.Builder{}
paramCount := len(httpReq.QueryParams)
for name, value := range httpReq.QueryParams {
build.WriteString(utils.PercentEncode(name))
build.WriteString("=")
build.WriteString(utils.PercentEncode(value))
paramCount--
if paramCount > 0 {
build.WriteString("&")
}
}
httpReq.QueryString = build.String()
}

func (httpReq *HttpRequest) ConvertRequest(request interface{}) {
if request == nil {
return
}
reqType := reflect.TypeOf(request)
if reqType.Kind() == reflect.Ptr {
reqType = reqType.Elem()
}
reqValue := reflect.ValueOf(request)
if reqValue.Kind() == reflect.Ptr {
reqValue = reqValue.Elem()
}
var flag = false
for i := 0; i < reqType.NumField(); i++ {
fieldType := reqType.Field(i)
value := reqValue.FieldByName(fieldType.Name)
if value.Kind() == reflect.Ptr {
if value.IsNil() {
continue
}
value = value.Elem()
}
propertyType := fieldType.Type
if propertyType.Kind() == reflect.Ptr {
propertyType = propertyType.Elem()
}

_, flag = propertyType.FieldByName(string(BODY))
if flag {
httpReq.Body = value.Interface()
continue
}
_, flag = propertyType.FieldByName(string(HEADER))
if flag {
httpReq.BuildHeaderParams(utils.StructToMap(value.Interface(), ignorePositionField))
continue
}
_, flag = propertyType.FieldByName(string(QUERY))
if flag {
httpReq.BuildQueryParams(utils.StructToMap(value.Interface(), ignorePositionField))
continue
}
_, flag = propertyType.FieldByName(string(PATH))
if flag {
httpReq.BuildPathParams(utils.StructToMap(value.Interface(), ignorePositionField))
continue
}
}
}

func (httpReq *HttpRequest) AddHeaders(headers map[string]string) {
if httpReq.HeaderParams == nil {
httpReq.HeaderParams = map[string]string{}
}
for name, value := range headers {
httpReq.HeaderParams[name] = value
}
}

var _typeOfHeader = reflect.TypeOf(position.Header{})
var _typeOfBody = reflect.TypeOf(position.Body{})
var _typeOfQuery = reflect.TypeOf(position.Query{})
var _typeOfPath = reflect.TypeOf(position.Path{})

func ignorePositionField(obj interface{}, field reflect.StructField, value reflect.Value) bool {
if utils.ValueIsEmpty(value) {
return true
}
typeOfValue := value.Type()
return typeOfValue == _typeOfBody || typeOfValue == _typeOfQuery ||
typeOfValue == _typeOfHeader || typeOfValue == _typeOfPath
}

func (httpReq *HttpRequest) ConvertQueryParamsFromPath() {
if !strings.Contains(httpReq.Path, consts.QueryStartSymbol) {
return
}
pathArray := strings.Split(httpReq.Path, consts.QueryStartSymbol)
paramArray := strings.Split(pathArray[1], consts.ParameterSeparator)
for _, param := range paramArray {
if len(param) == 0 {
continue
}
queryParamArray := strings.Split(param, consts.QuerySeparator)
if len(queryParamArray) != 2 {
continue
}
httpReq.QueryParams[queryParamArray[0]] = queryParamArray[1]
}
httpReq.Path = pathArray[0]
}

+ 7
- 0
third_party/ecloudsdkcore/response/http_response.go Просмотреть файл

@@ -0,0 +1,7 @@
package response

type HttpResponse struct {
StatusCode int `json:"statusCode,omitempty"`
Headers map[string][]string `json:"headers,omitempty"`
Data interface{} `json:"data,omitempty"`
}

+ 70
- 0
third_party/ecloudsdkcore/retry/retry_context.go Просмотреть файл

@@ -0,0 +1,70 @@
package retry

type Context interface {
get() interface{}
hasResult() bool
hasError() bool
getResult() interface{}
getRetryTimes() int32
getDelayTime() int64
}

type ResultContext struct {
Result interface{}
RetryTimes int32
DelayTime int64
}

func (r *ResultContext) get() interface{} {
return r.Result
}

func (r *ResultContext) hasResult() bool {
return true
}

func (r *ResultContext) hasError() bool {
return false
}

func (r *ResultContext) getResult() interface{} {
return r.Result
}

func (r *ResultContext) getRetryTimes() int32 {
return r.RetryTimes
}

func (r *ResultContext) getDelayTime() int64 {
return r.DelayTime
}

type ErrorContext struct {
Err error
RetryTimes int32
DelayTime int64
}

func (e *ErrorContext) get() interface{} {
return e.Err
}

func (e *ErrorContext) hasResult() bool {
return false
}

func (e *ErrorContext) hasError() bool {
return true
}

func (e *ErrorContext) getResult() interface{} {
return e.Err
}

func (e *ErrorContext) getRetryTimes() int32 {
return e.RetryTimes
}

func (e *ErrorContext) getDelayTime() int64 {
return e.DelayTime
}

+ 85
- 0
third_party/ecloudsdkcore/retry/retry_policy.go Просмотреть файл

@@ -0,0 +1,85 @@
package retry

import (
"math"
"math/rand"
"time"
)

type Policy interface {
computeWaitTime(failedRetryContext Context) int64
}

func NoWait() Policy {
return &FixedRetryPolicy{
0,
}
}

func WaitRetryPolicy(sleepTime int64) Policy {
return &FixedRetryPolicy{
sleepTime,
}
}

type FixedRetryPolicy struct {
SleepTime int64
}

func (f *FixedRetryPolicy) computeWaitTime(failedRetryContext Context) int64 {
return f.SleepTime
}

type RandomRetryPolicy struct {
Minimum int64
Maximum int64
}

func (r *RandomRetryPolicy) computeWaitTime(failedRetryContext Context) int64 {
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
t := rand.Int63n(int64(math.Abs(float64(r.Maximum - r.Minimum))))
return t + r.Minimum
}

type IncrementingRetryPolicy struct {
InitWaitTime int64
Increment int64
}

func (i *IncrementingRetryPolicy) computeWaitTime(failedRetryContext Context) int64 {
res := i.InitWaitTime + (i.Increment * int64(failedRetryContext.getRetryTimes()-1))
if res > 0 {
return res
}
return 0
}

type ExponentialRetryPolicy struct {
Multiplier int64
MaximumWait int64
}

func (e *ExponentialRetryPolicy) computeWaitTime(failedRetryContext Context) int64 {
exp := math.Pow(float64(failedRetryContext.getRetryTimes()), 2)
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
t := rand.Int63n(e.Multiplier * int64(exp))
if t > e.MaximumWait {
t = e.MaximumWait
}
if t >= 0 {
return t
}
return 0
}

type CompositeRetryPolicy struct {
WaitPolicies []Policy
}

func (c *CompositeRetryPolicy) computeWaitTime(failedRetryContext Context) int64 {
var waitTime int64 = 0
for _, policy := range c.WaitPolicies {
waitTime += policy.computeWaitTime(failedRetryContext)
}
return waitTime
}

+ 78
- 0
third_party/ecloudsdkcore/retry/retry_template.go Просмотреть файл

@@ -0,0 +1,78 @@
package retry

import (
"fmt"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
"time"
)

type Template struct {
RetryPolicy Policy
RetryTimes int32
MaxDuringTime int64
}

type RetryError struct {
*errs.SdkError
NumberOfFailedAttempts int32
MaxDuringTimeOfFailedAttempts int64
LastFailedRetryContext Context
}

func NewRetryError(numberOfFailedAttempts int32, maxDuringTimeOfFailedAttempts int64, lastFailedRetryContext Context) *RetryError {
var msg string
if numberOfFailedAttempts > 0 {
msg = fmt.Sprintf("Retrying failed to complete successfully after %v attempts.", numberOfFailedAttempts)
}
if maxDuringTimeOfFailedAttempts > 0 {
msg = fmt.Sprintf("Retrying failed to complete successfully after %v milliseconds.", maxDuringTimeOfFailedAttempts)
}
var retryErr error
if v, err := lastFailedRetryContext.(*ErrorContext); err {
retryErr = v.Err
} else {
retryErr = nil
}

return &RetryError{
SdkError: errs.NewSdkError("RetryError", msg, retryErr),
}
}

type ReFunc func() (interface{}, error)

func (t *Template) Call(f ReFunc) (interface{}, error) {
startTime := time.Now().UnixNano()
for retryTimes := 1; ; retryTimes++ {
var ctx Context
res, err := f()
duration := (time.Now().UnixNano() - startTime) / 1e6
if err != nil {
ctx = &ErrorContext{
Err: err,
RetryTimes: int32(retryTimes),
DelayTime: duration,
}
} else {
ctx = &ResultContext{
Result: res,
RetryTimes: int32(retryTimes),
DelayTime: duration,
}
}
if r, err := ctx.(*ResultContext); err {
return r.Result, nil
}
if int32(retryTimes) == t.RetryTimes {
return nil, NewRetryError(int32(retryTimes), 0, ctx)
} else if duration >= t.MaxDuringTime {
return nil, NewRetryError(int32(retryTimes), duration, ctx)
} else {
sleepTime := t.RetryPolicy.computeWaitTime(ctx)
if sleepTime <= 0 {
continue
}
time.Sleep(time.Duration(sleepTime * 1e6))
}
}
}

+ 47
- 0
third_party/ecloudsdkcore/retry/retry_template_builder.go Просмотреть файл

@@ -0,0 +1,47 @@
package retry

type TemplateBuilder struct {
RetryPolicy Policy
RetryTimes int32
MaxDuringTime int64
}

const (
MaxRetryTimes int32 = 10
MaxDuringTimeInMillisecond int64 = 60 * 1000
)

func NewBuilder() *TemplateBuilder {
return &TemplateBuilder{}
}

func (t *TemplateBuilder) SetRetryPolicy(retryPolicy Policy) *TemplateBuilder {
t.RetryPolicy = retryPolicy
return t
}

func (t *TemplateBuilder) SetRetryTimes(retryTimes int32) *TemplateBuilder {
t.RetryTimes = retryTimes
return t
}

func (t *TemplateBuilder) SetMaxDuringTime(maxDuringTime int64) *TemplateBuilder {
t.MaxDuringTime = maxDuringTime
return t
}

func (t *TemplateBuilder) Build() *Template {
retryTimes := t.RetryTimes
if t.RetryTimes <= 0 {
retryTimes = MaxRetryTimes
}
maxDuringTime := t.MaxDuringTime
if maxDuringTime <= 0 {
maxDuringTime = MaxDuringTimeInMillisecond
}
return &Template{
RetryPolicy: t.RetryPolicy,
RetryTimes: retryTimes,
MaxDuringTime: maxDuringTime,
}
}

+ 81
- 0
third_party/ecloudsdkcore/utils/algorithm_util.go Просмотреть файл

@@ -0,0 +1,81 @@
package utils

import (
"crypto"
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"github.com/google/uuid"
"strings"
)

const (
HighMask = 0xf0
LowMask = 0x0f
)

var HexCodeTable = []string{
"0", "1", "2", "3",
"4", "5", "6", "7",
"8", "9", "a", "b",
"c", "d", "e", "f",
}

func Base64Encode(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}

func Base64Decode(encodedString string) ([]byte, error) {
return base64.StdEncoding.DecodeString(encodedString)
}

func GenerateRSASignature(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
hash := md5.New()
hash.Write(data)
hashed := hash.Sum(nil)
return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.MD5, hashed)
}

func Nonce() string {
return strings.ReplaceAll(uuid.New().String(), "-", "")
}

func ConvertToHexString(data []byte) string {
if data == nil {
return ""
}
builder := strings.Builder{}
for _, d := range data {
builder.WriteString(HexCodeTable[(HighMask&d)>>4])
builder.WriteString(HexCodeTable[LowMask&d])
}
return builder.String()
}

func Sha256Encode(text string) []byte {
h := sha256.New()
h.Write([]byte(text))
return h.Sum(nil)
}

func HmacSha1(text string, keyStr string) []byte {
key := []byte(keyStr)
mac := hmac.New(sha1.New, key)
mac.Write([]byte(text))
return mac.Sum(nil)
}

func HmacSha256(text string, keyStr string) []byte {
key := []byte(keyStr)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(text))
return mac.Sum(nil)
}

func RSAPublicKeyEncrypt(publicKey *rsa.PublicKey, data []byte) ([]byte, error) {
return rsa.EncryptPKCS1v15(nil, publicKey, data)
}

+ 49
- 0
third_party/ecloudsdkcore/utils/convert_util.go Просмотреть файл

@@ -0,0 +1,49 @@
package utils

import (
"encoding/json"
"strconv"
"strings"
)

func ConvertInterfaceToString(value interface{}) string {
if value == nil {
return ""
}
switch value.(type) {
case int:
return strconv.Itoa(value.(int))
case string:
return value.(string)
case uint:
return strconv.Itoa(int(value.(uint)))
case int32:
return strconv.Itoa(int(value.(int32)))
case uint32:
return strconv.Itoa(int(value.(uint32)))
case int64:
return strconv.FormatInt(value.(int64), 10)
case uint64:
return strconv.FormatUint(value.(uint64), 10)
case float32:
return strconv.FormatFloat(float64(value.(float32)), 'f', -1, 64)
case float64:
return strconv.FormatFloat(value.(float64), 'f', -1, 64)
case int8:
return strconv.Itoa(int(value.(int8)))
case uint8:
return strconv.Itoa(int(value.(uint8)))
case int16:
return strconv.Itoa(int(value.(int16)))
case uint16:
return strconv.Itoa(int(value.(uint16)))
case []byte:
return string(value.([]byte))
default:
b, err := json.Marshal(value)
if err != nil {
return ""
}
return strings.ReplaceAll(strings.Trim(string(b[:]), "[]"), "\"", "")
}
}

+ 82
- 0
third_party/ecloudsdkcore/utils/http_util.go Просмотреть файл

@@ -0,0 +1,82 @@
package utils

import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"reflect"
"regexp"
)

var (
jsonCheck = regexp.MustCompile("(?i:(?:application|text)/json)")
xmlCheck = regexp.MustCompile("(?i:(?:application|text)/xml)")
)

// DetectContentType method is used to figure out `Request.Body` content type for request header
func DetectContentType(body interface{}) string {
contentType := "text/plain; charset=utf-8"
kind := reflect.TypeOf(body).Kind()

switch kind {
case reflect.Struct, reflect.Map, reflect.Ptr:
contentType = "application/json; charset=utf-8"
case reflect.String:
contentType = "text/plain; charset=utf-8"
default:
if b, ok := body.([]byte); ok {
contentType = http.DetectContentType(b)
} else if kind == reflect.Slice {
contentType = "application/json; charset=utf-8"
}
}

return contentType
}

// SetBody Set request body from an interface{}
func SetBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {
if bodyBuf == nil {
bodyBuf = &bytes.Buffer{}
}
switch v := body.(type) {
case io.Reader:
_, err = bodyBuf.ReadFrom(v)
case []byte:
_, err = bodyBuf.Write(v)
case string:
_, err = bodyBuf.WriteString(v)
case *string:
_, err = bodyBuf.WriteString(*v)
default:
if jsonCheck.MatchString(contentType) {
bodyType := reflect.TypeOf(body)
if bodyType.Kind() == reflect.Ptr {
bodyType = bodyType.Elem()
}
if bodyType.NumField() == 1 && bodyType.Field(0).Type.Kind() == reflect.Slice {
if IsUnSet(bodyType.Field(0).Tag.Get("json")) {
v := reflect.ValueOf(body)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
body = v.Field(0).Interface()
}
}
err = json.NewEncoder(bodyBuf).Encode(body)
} else if xmlCheck.MatchString(contentType) {
err = xml.NewEncoder(bodyBuf).Encode(body)
}
}
if err != nil {
return nil, err
}
if bodyBuf.Len() == 0 {
err = fmt.Errorf("Invalid body type %s", contentType)
return nil, err
}
return bodyBuf, nil
}

+ 150
- 0
third_party/ecloudsdkcore/utils/other_util.go Просмотреть файл

@@ -0,0 +1,150 @@
package utils

import (
"bytes"
"encoding/gob"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"time"
)

type cacheControl map[string]string

func parseCacheControl(headers http.Header) cacheControl {
cc := cacheControl{}
ccHeader := headers.Get("Cache-Control")
for _, part := range strings.Split(ccHeader, ",") {
part = strings.Trim(part, " ")
if part == "" {
continue
}
if strings.ContainsRune(part, '=') {
keyval := strings.Split(part, "=")
cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",")
} else {
cc[part] = ""
}
}
return cc
}

// CacheExpires helper function to determine remaining time before repeating a request.
func CacheExpires(r *http.Response) time.Time {
// Figure out when the cache expires.
var expires time.Time
now, err := time.Parse(time.RFC1123, r.Header.Get("date"))
if err != nil {
return time.Now()
}
respCacheControl := parseCacheControl(r.Header)

if maxAge, ok := respCacheControl["max-age"]; ok {
lifetime, err := time.ParseDuration(maxAge + "s")
if err != nil {
expires = now
}
expires = now.Add(lifetime)
} else {
expiresHeader := r.Header.Get("Expires")
if expiresHeader != "" {
expires, err = time.Parse(time.RFC1123, expiresHeader)
if err != nil {
expires = now
}
}
}
return expires
}

// Add a file to the multipart request
func addFile(w *multipart.Writer, fieldName, path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

part, err := w.CreateFormFile(fieldName, filepath.Base(path))
if err != nil {
return err
}
_, err = io.Copy(part, file)

return err
}

// selectHeaderContentType select a content type from the available list.
func selectHeaderContentType(contentTypes []string) string {
if len(contentTypes) == 0 {
return ""
}
if contains(contentTypes, "application/json") {
return "application/json"
}
return contentTypes[0]
}

// selectHeaderAccept join all accept types and return
func selectHeaderAccept(accepts []string) string {
if len(accepts) == 0 {
return ""
}

if contains(accepts, "application/json") {
return "application/json"
}

return strings.Join(accepts, ",")
}

// contains is a case insenstive match, finding needle in a haystack
func contains(haystack []string, needle string) bool {
for _, a := range haystack {
if strings.ToLower(a) == strings.ToLower(needle) {
return true
}
}
return false
}

// Verify optional parameters are of the correct type.
func typeCheckParameter(obj interface{}, expected string, name string) error {
if obj == nil {
return nil
}
if reflect.TypeOf(obj).String() != expected {
return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String())
}
return nil
}

func IsSet(val interface{}) bool {
if val == nil {
return false
}
v := reflect.ValueOf(val)
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Slice || v.Kind() == reflect.Map {
return !v.IsNil()
}
valType := reflect.TypeOf(val)
valZero := reflect.Zero(valType)
return valZero != v
}

func IsUnSet(val interface{}) bool {
return !IsSet(val)
}

func DeepCopy(dst, src interface{}) error {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(src); err != nil {
return err
}
return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}

+ 33
- 0
third_party/ecloudsdkcore/utils/string_util.go Просмотреть файл

@@ -0,0 +1,33 @@
package utils

import (
"encoding/json"
"net/url"
"strings"
)

const (
DefaultEndpoint = "https://ecloud.10086.cn"
)

func PercentEncode(urlStr string) string {
urlStr = url.QueryEscape(urlStr)
urlStr = strings.ReplaceAll(urlStr, "+", "%20")
urlStr = strings.ReplaceAll(urlStr, "*", "%2A")
urlStr = strings.ReplaceAll(urlStr, "%7E", "~")
return urlStr
}

func Beautify(i interface{}) string {
resp, _ := json.MarshalIndent(i, "", " ")
return string(resp)
}

func ToJsonString(i interface{}) string {
resp, _ := json.Marshal(i)
return string(resp)
}

func StringToBytes(s string) []byte {
return []byte(s)
}

+ 84
- 0
third_party/ecloudsdkcore/utils/struct_map_util.go Просмотреть файл

@@ -0,0 +1,84 @@
package utils

import (
"reflect"
"strings"
)

type IgnoreFieldOf func(obj interface{}, field reflect.StructField, value reflect.Value) bool

// StructToMap struct convert to map
func StructToMap(structObj interface{}, ignoreFieldOf IgnoreFieldOf) map[string]string {
structType := reflect.TypeOf(structObj)
if structType.Kind() == reflect.Ptr {
structType = structType.Elem()
}
structValue := reflect.ValueOf(structObj)
if structValue.Kind() == reflect.Ptr {
structValue = structValue.Elem()
}
params := make(map[string]string)
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
value := structValue.FieldByName(field.Name)
if ignoreFieldOf != nil && ignoreFieldOf(structObj, field, value) {
continue
}
tag := field.Tag.Get("json")
omitempty := false
name := field.Name
if len(tag) > 0 {
items := strings.Split(tag, ",")
name = strings.TrimSpace(items[0])
omitempty = len(items) > 1 && "omitempty" == strings.TrimSpace(items[1])
}
if omitempty && ValueIsEmpty(value) {
continue
}
params[name] = ConvertInterfaceToString(value.Interface())
}
return params
}

// MergeMap merge the two map results, last elements will override previous elements
func MergeMap(mObj ...map[string]interface{}) map[string]interface{} {
newMap := make(map[string]interface{})
for _, m := range mObj {
for k, v := range m {
vValue := reflect.ValueOf(v)
if vValue.Kind() == reflect.Ptr {
if !vValue.IsNil() {
newMap[k] = v
}
} else {
newMap[k] = v
}
}
}
return newMap
}

func Merge(sources ...interface{}) map[string]interface{} {
res := make(map[string]interface{})
for _, source := range sources {
sourceValue := reflect.ValueOf(source)
if sourceValue.Kind() == reflect.Ptr {
sourceValue = sourceValue.Elem()
}
sourceType := reflect.TypeOf(source)
if sourceType.Kind() == reflect.Ptr {
sourceType = sourceType.Elem()
}
for i := 0; i < sourceType.NumField(); i++ {
field := sourceType.Field(i)
if IsSet(sourceValue.Field(i).Interface()) {
res[field.Name] = sourceValue.Field(i).Interface()
}
}
}
return res
}

func ValueIsEmpty(value reflect.Value) bool {
return (value.Kind() == reflect.Ptr && value.IsNil()) || (value.Kind() != reflect.Ptr && value.IsZero())
}

+ 73
- 0
third_party/ecloudsdkcore/utils/trans_util.go Просмотреть файл

@@ -0,0 +1,73 @@
package utils

func String(a string) *string {
return &a
}

func StringValue(a *string) string {
if a == nil {
return ""
}
return *a
}

func Int32(a int32) *int32 {
return &a
}

func Int32Value(a *int32) int32 {
if a == nil {
return 0
}
return *a
}

func Int64(a int64) *int64 {
return &a
}

func Int64Value(a *int64) int64 {
if a == nil {
return 0
}
return *a
}

func Bool(a bool) *bool {
return &a
}

func BoolValue(a *bool) bool {
if a == nil {
return false
}
return *a
}

func DefaultInt32(reaNum, defaultNum *int32) *int32 {
if reaNum == nil {
return defaultNum
}
return reaNum
}

func DefaultInt64(reaNum, defaultNum *int64) *int64 {
if reaNum == nil {
return defaultNum
}
return reaNum
}

func DefaultBool(reaNum, defaultNum *bool) *bool {
if reaNum == nil {
return defaultNum
}
return reaNum
}

func DefaultString(reaStr, defaultStr *string) *string {
if reaStr == nil {
return defaultStr
}
return reaStr
}

+ 1228
- 0
third_party/ecloudsdkmaas/client.go
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 5
- 0
third_party/ecloudsdkmaas/go.mod Просмотреть файл

@@ -0,0 +1,5 @@
module gitlab.ecloud.com/ecloud/ecloudsdkmaas

go 1.14

require gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.6

+ 55
- 0
third_party/ecloudsdkmaas/model/auto_renew_order_body.go Просмотреть файл

@@ -0,0 +1,55 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type AutoRenewOrderBody struct {
position.Body
// 自动续订设置列表
AutoRenewList *[]AutoRenewOrderRequestAutoRenewList `json:"autoRenewList,omitempty"`
}

func (s AutoRenewOrderBody) String() string {
return utils.Beautify(s)
}

func (s AutoRenewOrderBody) GoString() string {
return s.String()
}

func (s AutoRenewOrderBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *AutoRenewOrderBody) SetAutoRenewList(v []AutoRenewOrderRequestAutoRenewList) *AutoRenewOrderBody {
s.AutoRenewList = &v
return s
}


type AutoRenewOrderBodyBuilder struct {
s *AutoRenewOrderBody
}

func NewAutoRenewOrderBodyBuilder() *AutoRenewOrderBodyBuilder {
s := &AutoRenewOrderBody{}
b := &AutoRenewOrderBodyBuilder{s: s}
return b
}

func (b *AutoRenewOrderBodyBuilder) AutoRenewList(v []AutoRenewOrderRequestAutoRenewList) *AutoRenewOrderBodyBuilder {
b.s.AutoRenewList = &v
return b
}

func (b *AutoRenewOrderBodyBuilder) Build() *AutoRenewOrderBody {
return b.s
}



+ 54
- 0
third_party/ecloudsdkmaas/model/auto_renew_order_request.go Просмотреть файл

@@ -0,0 +1,54 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type AutoRenewOrderRequest struct {


AutoRenewOrderBody *AutoRenewOrderBody `json:"autoRenewOrderBody,omitempty"`
}

func (s AutoRenewOrderRequest) String() string {
return utils.Beautify(s)
}

func (s AutoRenewOrderRequest) GoString() string {
return s.String()
}

func (s AutoRenewOrderRequest) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *AutoRenewOrderRequest) SetAutoRenewOrderBody(v *AutoRenewOrderBody) *AutoRenewOrderRequest {
s.AutoRenewOrderBody = v
return s
}


type AutoRenewOrderRequestBuilder struct {
s *AutoRenewOrderRequest
}

func NewAutoRenewOrderRequestBuilder() *AutoRenewOrderRequestBuilder {
s := &AutoRenewOrderRequest{}
b := &AutoRenewOrderRequestBuilder{s: s}
return b
}

func (b *AutoRenewOrderRequestBuilder) AutoRenewOrderBody(v *AutoRenewOrderBody) *AutoRenewOrderRequestBuilder {
b.s.AutoRenewOrderBody = v
return b
}

func (b *AutoRenewOrderRequestBuilder) Build() *AutoRenewOrderRequest {
return b.s
}



+ 66
- 0
third_party/ecloudsdkmaas/model/auto_renew_order_request_auto_renew_list.go Просмотреть файл

@@ -0,0 +1,66 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type AutoRenewOrderRequestAutoRenewList struct {

// 订购规格唯一标识信息
InstanceId *string `json:"instanceId,omitempty"`
// 是否开启自动续订
AutoRenew *bool `json:"autoRenew,omitempty"`
}

func (s AutoRenewOrderRequestAutoRenewList) String() string {
return utils.Beautify(s)
}

func (s AutoRenewOrderRequestAutoRenewList) GoString() string {
return s.String()
}

func (s AutoRenewOrderRequestAutoRenewList) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *AutoRenewOrderRequestAutoRenewList) SetInstanceId(v string) *AutoRenewOrderRequestAutoRenewList {
s.InstanceId = &v
return s
}

func (s *AutoRenewOrderRequestAutoRenewList) SetAutoRenew(v bool) *AutoRenewOrderRequestAutoRenewList {
s.AutoRenew = &v
return s
}


type AutoRenewOrderRequestAutoRenewListBuilder struct {
s *AutoRenewOrderRequestAutoRenewList
}

func NewAutoRenewOrderRequestAutoRenewListBuilder() *AutoRenewOrderRequestAutoRenewListBuilder {
s := &AutoRenewOrderRequestAutoRenewList{}
b := &AutoRenewOrderRequestAutoRenewListBuilder{s: s}
return b
}

func (b *AutoRenewOrderRequestAutoRenewListBuilder) InstanceId(v string) *AutoRenewOrderRequestAutoRenewListBuilder {
b.s.InstanceId = &v
return b
}

func (b *AutoRenewOrderRequestAutoRenewListBuilder) AutoRenew(v bool) *AutoRenewOrderRequestAutoRenewListBuilder {
b.s.AutoRenew = &v
return b
}

func (b *AutoRenewOrderRequestAutoRenewListBuilder) Build() *AutoRenewOrderRequestAutoRenewList {
return b.s
}



+ 102
- 0
third_party/ecloudsdkmaas/model/auto_renew_order_response.go Просмотреть файл

@@ -0,0 +1,102 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type AutoRenewOrderResponse struct {

// 请求id
RequestId *string `json:"requestId,omitempty"`
// 异常消息
ErrorMessage *string `json:"errorMessage,omitempty"`
// 异常码
ErrorCode *string `json:"errorCode,omitempty"`
// 状态是否正常,OK代表返回正常,ERROR代表返回错误
State *string `json:"state,omitempty"`
// 是否成功开启或关闭自动续订
Body *bool `json:"body,omitempty"`
}

func (s AutoRenewOrderResponse) String() string {
return utils.Beautify(s)
}

func (s AutoRenewOrderResponse) GoString() string {
return s.String()
}

func (s AutoRenewOrderResponse) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *AutoRenewOrderResponse) SetRequestId(v string) *AutoRenewOrderResponse {
s.RequestId = &v
return s
}

func (s *AutoRenewOrderResponse) SetErrorMessage(v string) *AutoRenewOrderResponse {
s.ErrorMessage = &v
return s
}

func (s *AutoRenewOrderResponse) SetErrorCode(v string) *AutoRenewOrderResponse {
s.ErrorCode = &v
return s
}

func (s *AutoRenewOrderResponse) SetState(v string) *AutoRenewOrderResponse {
s.State = &v
return s
}

func (s *AutoRenewOrderResponse) SetBody(v bool) *AutoRenewOrderResponse {
s.Body = &v
return s
}


type AutoRenewOrderResponseBuilder struct {
s *AutoRenewOrderResponse
}

func NewAutoRenewOrderResponseBuilder() *AutoRenewOrderResponseBuilder {
s := &AutoRenewOrderResponse{}
b := &AutoRenewOrderResponseBuilder{s: s}
return b
}

func (b *AutoRenewOrderResponseBuilder) RequestId(v string) *AutoRenewOrderResponseBuilder {
b.s.RequestId = &v
return b
}

func (b *AutoRenewOrderResponseBuilder) ErrorMessage(v string) *AutoRenewOrderResponseBuilder {
b.s.ErrorMessage = &v
return b
}

func (b *AutoRenewOrderResponseBuilder) ErrorCode(v string) *AutoRenewOrderResponseBuilder {
b.s.ErrorCode = &v
return b
}

func (b *AutoRenewOrderResponseBuilder) State(v string) *AutoRenewOrderResponseBuilder {
b.s.State = &v
return b
}

func (b *AutoRenewOrderResponseBuilder) Body(v bool) *AutoRenewOrderResponseBuilder {
b.s.Body = &v
return b
}

func (b *AutoRenewOrderResponseBuilder) Build() *AutoRenewOrderResponse {
return b.s
}



+ 55
- 0
third_party/ecloudsdkmaas/model/batch_delete_doc_body.go Просмотреть файл

@@ -0,0 +1,55 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type BatchDeleteDocBody struct {
position.Body
// 文档id列表
DocIds []string `json:"docIds,omitempty"`
}

func (s BatchDeleteDocBody) String() string {
return utils.Beautify(s)
}

func (s BatchDeleteDocBody) GoString() string {
return s.String()
}

func (s BatchDeleteDocBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *BatchDeleteDocBody) SetDocIds(v []string) *BatchDeleteDocBody {
s.DocIds = v
return s
}


type BatchDeleteDocBodyBuilder struct {
s *BatchDeleteDocBody
}

func NewBatchDeleteDocBodyBuilder() *BatchDeleteDocBodyBuilder {
s := &BatchDeleteDocBody{}
b := &BatchDeleteDocBodyBuilder{s: s}
return b
}

func (b *BatchDeleteDocBodyBuilder) DocIds(v []string) *BatchDeleteDocBodyBuilder {
b.s.DocIds = v
return b
}

func (b *BatchDeleteDocBodyBuilder) Build() *BatchDeleteDocBody {
return b.s
}



+ 55
- 0
third_party/ecloudsdkmaas/model/batch_delete_doc_path.go Просмотреть файл

@@ -0,0 +1,55 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type BatchDeleteDocPath struct {
position.Path

DatasetId *string `json:"datasetId,omitempty"`
}

func (s BatchDeleteDocPath) String() string {
return utils.Beautify(s)
}

func (s BatchDeleteDocPath) GoString() string {
return s.String()
}

func (s BatchDeleteDocPath) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *BatchDeleteDocPath) SetDatasetId(v string) *BatchDeleteDocPath {
s.DatasetId = &v
return s
}


type BatchDeleteDocPathBuilder struct {
s *BatchDeleteDocPath
}

func NewBatchDeleteDocPathBuilder() *BatchDeleteDocPathBuilder {
s := &BatchDeleteDocPath{}
b := &BatchDeleteDocPathBuilder{s: s}
return b
}

func (b *BatchDeleteDocPathBuilder) DatasetId(v string) *BatchDeleteDocPathBuilder {
b.s.DatasetId = &v
return b
}

func (b *BatchDeleteDocPathBuilder) Build() *BatchDeleteDocPath {
return b.s
}



+ 66
- 0
third_party/ecloudsdkmaas/model/batch_delete_doc_request.go Просмотреть файл

@@ -0,0 +1,66 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type BatchDeleteDocRequest struct {


BatchDeleteDocPath *BatchDeleteDocPath `json:"batchDeleteDocPath,omitempty"`

BatchDeleteDocBody *BatchDeleteDocBody `json:"batchDeleteDocBody,omitempty"`
}

func (s BatchDeleteDocRequest) String() string {
return utils.Beautify(s)
}

func (s BatchDeleteDocRequest) GoString() string {
return s.String()
}

func (s BatchDeleteDocRequest) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *BatchDeleteDocRequest) SetBatchDeleteDocPath(v *BatchDeleteDocPath) *BatchDeleteDocRequest {
s.BatchDeleteDocPath = v
return s
}

func (s *BatchDeleteDocRequest) SetBatchDeleteDocBody(v *BatchDeleteDocBody) *BatchDeleteDocRequest {
s.BatchDeleteDocBody = v
return s
}


type BatchDeleteDocRequestBuilder struct {
s *BatchDeleteDocRequest
}

func NewBatchDeleteDocRequestBuilder() *BatchDeleteDocRequestBuilder {
s := &BatchDeleteDocRequest{}
b := &BatchDeleteDocRequestBuilder{s: s}
return b
}

func (b *BatchDeleteDocRequestBuilder) BatchDeleteDocPath(v *BatchDeleteDocPath) *BatchDeleteDocRequestBuilder {
b.s.BatchDeleteDocPath = v
return b
}

func (b *BatchDeleteDocRequestBuilder) BatchDeleteDocBody(v *BatchDeleteDocBody) *BatchDeleteDocRequestBuilder {
b.s.BatchDeleteDocBody = v
return b
}

func (b *BatchDeleteDocRequestBuilder) Build() *BatchDeleteDocRequest {
return b.s
}



+ 102
- 0
third_party/ecloudsdkmaas/model/batch_delete_doc_response.go Просмотреть файл

@@ -0,0 +1,102 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type BatchDeleteDocResponse struct {

// 请求id
RequestId *string `json:"requestId,omitempty"`
// 异常消息
ErrorMessage *string `json:"errorMessage,omitempty"`
// 异常码
ErrorCode *string `json:"errorCode,omitempty"`
// 状态是否正常,OK代表返回正常,ERROR代表返回错误
State *string `json:"state,omitempty"`

Body *BatchDeleteDocResponseBody `json:"body,omitempty"`
}

func (s BatchDeleteDocResponse) String() string {
return utils.Beautify(s)
}

func (s BatchDeleteDocResponse) GoString() string {
return s.String()
}

func (s BatchDeleteDocResponse) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *BatchDeleteDocResponse) SetRequestId(v string) *BatchDeleteDocResponse {
s.RequestId = &v
return s
}

func (s *BatchDeleteDocResponse) SetErrorMessage(v string) *BatchDeleteDocResponse {
s.ErrorMessage = &v
return s
}

func (s *BatchDeleteDocResponse) SetErrorCode(v string) *BatchDeleteDocResponse {
s.ErrorCode = &v
return s
}

func (s *BatchDeleteDocResponse) SetState(v string) *BatchDeleteDocResponse {
s.State = &v
return s
}

func (s *BatchDeleteDocResponse) SetBody(v *BatchDeleteDocResponseBody) *BatchDeleteDocResponse {
s.Body = v
return s
}


type BatchDeleteDocResponseBuilder struct {
s *BatchDeleteDocResponse
}

func NewBatchDeleteDocResponseBuilder() *BatchDeleteDocResponseBuilder {
s := &BatchDeleteDocResponse{}
b := &BatchDeleteDocResponseBuilder{s: s}
return b
}

func (b *BatchDeleteDocResponseBuilder) RequestId(v string) *BatchDeleteDocResponseBuilder {
b.s.RequestId = &v
return b
}

func (b *BatchDeleteDocResponseBuilder) ErrorMessage(v string) *BatchDeleteDocResponseBuilder {
b.s.ErrorMessage = &v
return b
}

func (b *BatchDeleteDocResponseBuilder) ErrorCode(v string) *BatchDeleteDocResponseBuilder {
b.s.ErrorCode = &v
return b
}

func (b *BatchDeleteDocResponseBuilder) State(v string) *BatchDeleteDocResponseBuilder {
b.s.State = &v
return b
}

func (b *BatchDeleteDocResponseBuilder) Body(v *BatchDeleteDocResponseBody) *BatchDeleteDocResponseBuilder {
b.s.Body = v
return b
}

func (b *BatchDeleteDocResponseBuilder) Build() *BatchDeleteDocResponse {
return b.s
}



+ 54
- 0
third_party/ecloudsdkmaas/model/batch_delete_doc_response_body.go Просмотреть файл

@@ -0,0 +1,54 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type BatchDeleteDocResponseBody struct {

// 操作结果,true 表示执行成功,false表示执行失败,失败原因可以参考errorMessage或者联系客服人员
Result *bool `json:"result,omitempty"`
}

func (s BatchDeleteDocResponseBody) String() string {
return utils.Beautify(s)
}

func (s BatchDeleteDocResponseBody) GoString() string {
return s.String()
}

func (s BatchDeleteDocResponseBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *BatchDeleteDocResponseBody) SetResult(v bool) *BatchDeleteDocResponseBody {
s.Result = &v
return s
}


type BatchDeleteDocResponseBodyBuilder struct {
s *BatchDeleteDocResponseBody
}

func NewBatchDeleteDocResponseBodyBuilder() *BatchDeleteDocResponseBodyBuilder {
s := &BatchDeleteDocResponseBody{}
b := &BatchDeleteDocResponseBodyBuilder{s: s}
return b
}

func (b *BatchDeleteDocResponseBodyBuilder) Result(v bool) *BatchDeleteDocResponseBodyBuilder {
b.s.Result = &v
return b
}

func (b *BatchDeleteDocResponseBodyBuilder) Build() *BatchDeleteDocResponseBody {
return b.s
}



+ 55
- 0
third_party/ecloudsdkmaas/model/cancel_order_body.go Просмотреть файл

@@ -0,0 +1,55 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CancelOrderBody struct {
position.Body
// 订购规格唯一标识信息
InstanceIds []string `json:"instanceIds,omitempty"`
}

func (s CancelOrderBody) String() string {
return utils.Beautify(s)
}

func (s CancelOrderBody) GoString() string {
return s.String()
}

func (s CancelOrderBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CancelOrderBody) SetInstanceIds(v []string) *CancelOrderBody {
s.InstanceIds = v
return s
}


type CancelOrderBodyBuilder struct {
s *CancelOrderBody
}

func NewCancelOrderBodyBuilder() *CancelOrderBodyBuilder {
s := &CancelOrderBody{}
b := &CancelOrderBodyBuilder{s: s}
return b
}

func (b *CancelOrderBodyBuilder) InstanceIds(v []string) *CancelOrderBodyBuilder {
b.s.InstanceIds = v
return b
}

func (b *CancelOrderBodyBuilder) Build() *CancelOrderBody {
return b.s
}



+ 54
- 0
third_party/ecloudsdkmaas/model/cancel_order_request.go Просмотреть файл

@@ -0,0 +1,54 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CancelOrderRequest struct {


CancelOrderBody *CancelOrderBody `json:"cancelOrderBody,omitempty"`
}

func (s CancelOrderRequest) String() string {
return utils.Beautify(s)
}

func (s CancelOrderRequest) GoString() string {
return s.String()
}

func (s CancelOrderRequest) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CancelOrderRequest) SetCancelOrderBody(v *CancelOrderBody) *CancelOrderRequest {
s.CancelOrderBody = v
return s
}


type CancelOrderRequestBuilder struct {
s *CancelOrderRequest
}

func NewCancelOrderRequestBuilder() *CancelOrderRequestBuilder {
s := &CancelOrderRequest{}
b := &CancelOrderRequestBuilder{s: s}
return b
}

func (b *CancelOrderRequestBuilder) CancelOrderBody(v *CancelOrderBody) *CancelOrderRequestBuilder {
b.s.CancelOrderBody = v
return b
}

func (b *CancelOrderRequestBuilder) Build() *CancelOrderRequest {
return b.s
}



+ 102
- 0
third_party/ecloudsdkmaas/model/cancel_order_response.go Просмотреть файл

@@ -0,0 +1,102 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CancelOrderResponse struct {

// 请求id
RequestId *string `json:"requestId,omitempty"`
// 异常消息
ErrorMessage *string `json:"errorMessage,omitempty"`
// 异常码
ErrorCode *string `json:"errorCode,omitempty"`
// 状态是否正常,OK代表返回正常,ERROR代表返回错误
State *string `json:"state,omitempty"`
// 是否退订成功
Body *bool `json:"body,omitempty"`
}

func (s CancelOrderResponse) String() string {
return utils.Beautify(s)
}

func (s CancelOrderResponse) GoString() string {
return s.String()
}

func (s CancelOrderResponse) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CancelOrderResponse) SetRequestId(v string) *CancelOrderResponse {
s.RequestId = &v
return s
}

func (s *CancelOrderResponse) SetErrorMessage(v string) *CancelOrderResponse {
s.ErrorMessage = &v
return s
}

func (s *CancelOrderResponse) SetErrorCode(v string) *CancelOrderResponse {
s.ErrorCode = &v
return s
}

func (s *CancelOrderResponse) SetState(v string) *CancelOrderResponse {
s.State = &v
return s
}

func (s *CancelOrderResponse) SetBody(v bool) *CancelOrderResponse {
s.Body = &v
return s
}


type CancelOrderResponseBuilder struct {
s *CancelOrderResponse
}

func NewCancelOrderResponseBuilder() *CancelOrderResponseBuilder {
s := &CancelOrderResponse{}
b := &CancelOrderResponseBuilder{s: s}
return b
}

func (b *CancelOrderResponseBuilder) RequestId(v string) *CancelOrderResponseBuilder {
b.s.RequestId = &v
return b
}

func (b *CancelOrderResponseBuilder) ErrorMessage(v string) *CancelOrderResponseBuilder {
b.s.ErrorMessage = &v
return b
}

func (b *CancelOrderResponseBuilder) ErrorCode(v string) *CancelOrderResponseBuilder {
b.s.ErrorCode = &v
return b
}

func (b *CancelOrderResponseBuilder) State(v string) *CancelOrderResponseBuilder {
b.s.State = &v
return b
}

func (b *CancelOrderResponseBuilder) Body(v bool) *CancelOrderResponseBuilder {
b.s.Body = &v
return b
}

func (b *CancelOrderResponseBuilder) Build() *CancelOrderResponse {
return b.s
}



+ 99
- 0
third_party/ecloudsdkmaas/model/create_asset_body.go Просмотреть файл

@@ -0,0 +1,99 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)
type CreateAssetBodyAssetTypeEnum string

// List of AssetType
const (
CreateAssetBodyAssetTypeEnumImage CreateAssetBodyAssetTypeEnum = "Image"
CreateAssetBodyAssetTypeEnumVideo CreateAssetBodyAssetTypeEnum = "Video"
CreateAssetBodyAssetTypeEnumAudio CreateAssetBodyAssetTypeEnum = "Audio"
)

type CreateAssetBody struct {
position.Body
// 素材组ID(火山资产组ID)
GroupId *string `json:"groupId,omitempty"`
// 素材名称,最长64个字符
AssetName *string `json:"assetName,omitempty"`
// 素材URL,需为公网可访问的HTTP/HTTPS地址,火山侧将通过该URL下载素材
AssetUrl *string `json:"assetUrl,omitempty"`
// 素材类型,可选值:Image(图片)、Video(视频)、Audio(音频)
AssetType *CreateAssetBodyAssetTypeEnum `json:"assetType,omitempty"`
}

func (s CreateAssetBody) String() string {
return utils.Beautify(s)
}

func (s CreateAssetBody) GoString() string {
return s.String()
}

func (s CreateAssetBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateAssetBody) SetGroupId(v string) *CreateAssetBody {
s.GroupId = &v
return s
}

func (s *CreateAssetBody) SetAssetName(v string) *CreateAssetBody {
s.AssetName = &v
return s
}

func (s *CreateAssetBody) SetAssetUrl(v string) *CreateAssetBody {
s.AssetUrl = &v
return s
}

func (s *CreateAssetBody) SetAssetType(v CreateAssetBodyAssetTypeEnum) *CreateAssetBody {
s.AssetType = &v
return s
}


type CreateAssetBodyBuilder struct {
s *CreateAssetBody
}

func NewCreateAssetBodyBuilder() *CreateAssetBodyBuilder {
s := &CreateAssetBody{}
b := &CreateAssetBodyBuilder{s: s}
return b
}

func (b *CreateAssetBodyBuilder) GroupId(v string) *CreateAssetBodyBuilder {
b.s.GroupId = &v
return b
}

func (b *CreateAssetBodyBuilder) AssetName(v string) *CreateAssetBodyBuilder {
b.s.AssetName = &v
return b
}

func (b *CreateAssetBodyBuilder) AssetUrl(v string) *CreateAssetBodyBuilder {
b.s.AssetUrl = &v
return b
}

func (b *CreateAssetBodyBuilder) AssetType(v CreateAssetBodyAssetTypeEnum) *CreateAssetBodyBuilder {
b.s.AssetType = &v
return b
}

func (b *CreateAssetBodyBuilder) Build() *CreateAssetBody {
return b.s
}



+ 79
- 0
third_party/ecloudsdkmaas/model/create_asset_group_body.go Просмотреть файл

@@ -0,0 +1,79 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateAssetGroupBody struct {
position.Body
// 素材组类型,可选值:AIGC(虚拟人像素材库)、LivenessFace(真人素材库)。本接口仅支持创建AIGC类型,真人素材库需通过真人认证流程创建
GroupType *string `json:"groupType,omitempty"`
// 素材组名称,最长64个字符,非必填
GroupName *string `json:"groupName,omitempty"`
// 素材组简介,最长300个字符,非必填
Description *string `json:"description,omitempty"`
}

func (s CreateAssetGroupBody) String() string {
return utils.Beautify(s)
}

func (s CreateAssetGroupBody) GoString() string {
return s.String()
}

func (s CreateAssetGroupBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateAssetGroupBody) SetGroupType(v string) *CreateAssetGroupBody {
s.GroupType = &v
return s
}

func (s *CreateAssetGroupBody) SetGroupName(v string) *CreateAssetGroupBody {
s.GroupName = &v
return s
}

func (s *CreateAssetGroupBody) SetDescription(v string) *CreateAssetGroupBody {
s.Description = &v
return s
}


type CreateAssetGroupBodyBuilder struct {
s *CreateAssetGroupBody
}

func NewCreateAssetGroupBodyBuilder() *CreateAssetGroupBodyBuilder {
s := &CreateAssetGroupBody{}
b := &CreateAssetGroupBodyBuilder{s: s}
return b
}

func (b *CreateAssetGroupBodyBuilder) GroupType(v string) *CreateAssetGroupBodyBuilder {
b.s.GroupType = &v
return b
}

func (b *CreateAssetGroupBodyBuilder) GroupName(v string) *CreateAssetGroupBodyBuilder {
b.s.GroupName = &v
return b
}

func (b *CreateAssetGroupBodyBuilder) Description(v string) *CreateAssetGroupBodyBuilder {
b.s.Description = &v
return b
}

func (b *CreateAssetGroupBodyBuilder) Build() *CreateAssetGroupBody {
return b.s
}



+ 54
- 0
third_party/ecloudsdkmaas/model/create_asset_group_request.go Просмотреть файл

@@ -0,0 +1,54 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateAssetGroupRequest struct {


CreateAssetGroupBody *CreateAssetGroupBody `json:"createAssetGroupBody,omitempty"`
}

func (s CreateAssetGroupRequest) String() string {
return utils.Beautify(s)
}

func (s CreateAssetGroupRequest) GoString() string {
return s.String()
}

func (s CreateAssetGroupRequest) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateAssetGroupRequest) SetCreateAssetGroupBody(v *CreateAssetGroupBody) *CreateAssetGroupRequest {
s.CreateAssetGroupBody = v
return s
}


type CreateAssetGroupRequestBuilder struct {
s *CreateAssetGroupRequest
}

func NewCreateAssetGroupRequestBuilder() *CreateAssetGroupRequestBuilder {
s := &CreateAssetGroupRequest{}
b := &CreateAssetGroupRequestBuilder{s: s}
return b
}

func (b *CreateAssetGroupRequestBuilder) CreateAssetGroupBody(v *CreateAssetGroupBody) *CreateAssetGroupRequestBuilder {
b.s.CreateAssetGroupBody = v
return b
}

func (b *CreateAssetGroupRequestBuilder) Build() *CreateAssetGroupRequest {
return b.s
}



+ 102
- 0
third_party/ecloudsdkmaas/model/create_asset_group_response.go Просмотреть файл

@@ -0,0 +1,102 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateAssetGroupResponse struct {

// 请求id
RequestId *string `json:"requestId,omitempty"`
// 异常消息
ErrorMessage *string `json:"errorMessage,omitempty"`
// 异常码
ErrorCode *string `json:"errorCode,omitempty"`
// 状态是否正常,OK代表返回正常,ERROR代表返回错误
State *string `json:"state,omitempty"`

Body *CreateAssetGroupResponseBody `json:"body,omitempty"`
}

func (s CreateAssetGroupResponse) String() string {
return utils.Beautify(s)
}

func (s CreateAssetGroupResponse) GoString() string {
return s.String()
}

func (s CreateAssetGroupResponse) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateAssetGroupResponse) SetRequestId(v string) *CreateAssetGroupResponse {
s.RequestId = &v
return s
}

func (s *CreateAssetGroupResponse) SetErrorMessage(v string) *CreateAssetGroupResponse {
s.ErrorMessage = &v
return s
}

func (s *CreateAssetGroupResponse) SetErrorCode(v string) *CreateAssetGroupResponse {
s.ErrorCode = &v
return s
}

func (s *CreateAssetGroupResponse) SetState(v string) *CreateAssetGroupResponse {
s.State = &v
return s
}

func (s *CreateAssetGroupResponse) SetBody(v *CreateAssetGroupResponseBody) *CreateAssetGroupResponse {
s.Body = v
return s
}


type CreateAssetGroupResponseBuilder struct {
s *CreateAssetGroupResponse
}

func NewCreateAssetGroupResponseBuilder() *CreateAssetGroupResponseBuilder {
s := &CreateAssetGroupResponse{}
b := &CreateAssetGroupResponseBuilder{s: s}
return b
}

func (b *CreateAssetGroupResponseBuilder) RequestId(v string) *CreateAssetGroupResponseBuilder {
b.s.RequestId = &v
return b
}

func (b *CreateAssetGroupResponseBuilder) ErrorMessage(v string) *CreateAssetGroupResponseBuilder {
b.s.ErrorMessage = &v
return b
}

func (b *CreateAssetGroupResponseBuilder) ErrorCode(v string) *CreateAssetGroupResponseBuilder {
b.s.ErrorCode = &v
return b
}

func (b *CreateAssetGroupResponseBuilder) State(v string) *CreateAssetGroupResponseBuilder {
b.s.State = &v
return b
}

func (b *CreateAssetGroupResponseBuilder) Body(v *CreateAssetGroupResponseBody) *CreateAssetGroupResponseBuilder {
b.s.Body = v
return b
}

func (b *CreateAssetGroupResponseBuilder) Build() *CreateAssetGroupResponse {
return b.s
}



+ 133
- 0
third_party/ecloudsdkmaas/model/create_asset_group_response_body.go Просмотреть файл

@@ -0,0 +1,133 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)
type CreateAssetGroupResponseBodyGroupTypeEnum string

// List of GroupType
const (
CreateAssetGroupResponseBodyGroupTypeEnumAigc CreateAssetGroupResponseBodyGroupTypeEnum = "AIGC"
CreateAssetGroupResponseBodyGroupTypeEnumLivenessface CreateAssetGroupResponseBodyGroupTypeEnum = "LivenessFace"
)

type CreateAssetGroupResponseBody struct {

// 更新时间,格式:yyyy-MM-dd HH:mm:ss
UpdatedTime *string `json:"updatedTime,omitempty"`
// AssetGroupTypeEnum
GroupType *CreateAssetGroupResponseBodyGroupTypeEnum `json:"groupType,omitempty"`
// 资产组名称
GroupName *string `json:"groupName,omitempty"`
// 火山资产组ID
GroupId *string `json:"groupId,omitempty"`
// 资产组简介
Description *string `json:"description,omitempty"`
// 创建时间,格式:yyyy-MM-dd HH:mm:ss
CreatedTime *string `json:"createdTime,omitempty"`
// 主键ID(内部字段,OpenAPI不返回)
Id *string `json:"id,omitempty"`
}

func (s CreateAssetGroupResponseBody) String() string {
return utils.Beautify(s)
}

func (s CreateAssetGroupResponseBody) GoString() string {
return s.String()
}

func (s CreateAssetGroupResponseBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateAssetGroupResponseBody) SetUpdatedTime(v string) *CreateAssetGroupResponseBody {
s.UpdatedTime = &v
return s
}

func (s *CreateAssetGroupResponseBody) SetGroupType(v CreateAssetGroupResponseBodyGroupTypeEnum) *CreateAssetGroupResponseBody {
s.GroupType = &v
return s
}

func (s *CreateAssetGroupResponseBody) SetGroupName(v string) *CreateAssetGroupResponseBody {
s.GroupName = &v
return s
}

func (s *CreateAssetGroupResponseBody) SetGroupId(v string) *CreateAssetGroupResponseBody {
s.GroupId = &v
return s
}

func (s *CreateAssetGroupResponseBody) SetDescription(v string) *CreateAssetGroupResponseBody {
s.Description = &v
return s
}

func (s *CreateAssetGroupResponseBody) SetCreatedTime(v string) *CreateAssetGroupResponseBody {
s.CreatedTime = &v
return s
}

func (s *CreateAssetGroupResponseBody) SetId(v string) *CreateAssetGroupResponseBody {
s.Id = &v
return s
}


type CreateAssetGroupResponseBodyBuilder struct {
s *CreateAssetGroupResponseBody
}

func NewCreateAssetGroupResponseBodyBuilder() *CreateAssetGroupResponseBodyBuilder {
s := &CreateAssetGroupResponseBody{}
b := &CreateAssetGroupResponseBodyBuilder{s: s}
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) UpdatedTime(v string) *CreateAssetGroupResponseBodyBuilder {
b.s.UpdatedTime = &v
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) GroupType(v CreateAssetGroupResponseBodyGroupTypeEnum) *CreateAssetGroupResponseBodyBuilder {
b.s.GroupType = &v
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) GroupName(v string) *CreateAssetGroupResponseBodyBuilder {
b.s.GroupName = &v
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) GroupId(v string) *CreateAssetGroupResponseBodyBuilder {
b.s.GroupId = &v
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) Description(v string) *CreateAssetGroupResponseBodyBuilder {
b.s.Description = &v
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) CreatedTime(v string) *CreateAssetGroupResponseBodyBuilder {
b.s.CreatedTime = &v
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) Id(v string) *CreateAssetGroupResponseBodyBuilder {
b.s.Id = &v
return b
}

func (b *CreateAssetGroupResponseBodyBuilder) Build() *CreateAssetGroupResponseBody {
return b.s
}



+ 54
- 0
third_party/ecloudsdkmaas/model/create_asset_request.go Просмотреть файл

@@ -0,0 +1,54 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateAssetRequest struct {


CreateAssetBody *CreateAssetBody `json:"createAssetBody,omitempty"`
}

func (s CreateAssetRequest) String() string {
return utils.Beautify(s)
}

func (s CreateAssetRequest) GoString() string {
return s.String()
}

func (s CreateAssetRequest) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateAssetRequest) SetCreateAssetBody(v *CreateAssetBody) *CreateAssetRequest {
s.CreateAssetBody = v
return s
}


type CreateAssetRequestBuilder struct {
s *CreateAssetRequest
}

func NewCreateAssetRequestBuilder() *CreateAssetRequestBuilder {
s := &CreateAssetRequest{}
b := &CreateAssetRequestBuilder{s: s}
return b
}

func (b *CreateAssetRequestBuilder) CreateAssetBody(v *CreateAssetBody) *CreateAssetRequestBuilder {
b.s.CreateAssetBody = v
return b
}

func (b *CreateAssetRequestBuilder) Build() *CreateAssetRequest {
return b.s
}



+ 102
- 0
third_party/ecloudsdkmaas/model/create_asset_response.go Просмотреть файл

@@ -0,0 +1,102 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateAssetResponse struct {

// 请求id
RequestId *string `json:"requestId,omitempty"`
// 异常消息
ErrorMessage *string `json:"errorMessage,omitempty"`
// 异常码
ErrorCode *string `json:"errorCode,omitempty"`
// 状态是否正常,OK代表返回正常,ERROR代表返回错误
State *string `json:"state,omitempty"`
// 返回体
Body *string `json:"body,omitempty"`
}

func (s CreateAssetResponse) String() string {
return utils.Beautify(s)
}

func (s CreateAssetResponse) GoString() string {
return s.String()
}

func (s CreateAssetResponse) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateAssetResponse) SetRequestId(v string) *CreateAssetResponse {
s.RequestId = &v
return s
}

func (s *CreateAssetResponse) SetErrorMessage(v string) *CreateAssetResponse {
s.ErrorMessage = &v
return s
}

func (s *CreateAssetResponse) SetErrorCode(v string) *CreateAssetResponse {
s.ErrorCode = &v
return s
}

func (s *CreateAssetResponse) SetState(v string) *CreateAssetResponse {
s.State = &v
return s
}

func (s *CreateAssetResponse) SetBody(v string) *CreateAssetResponse {
s.Body = &v
return s
}


type CreateAssetResponseBuilder struct {
s *CreateAssetResponse
}

func NewCreateAssetResponseBuilder() *CreateAssetResponseBuilder {
s := &CreateAssetResponse{}
b := &CreateAssetResponseBuilder{s: s}
return b
}

func (b *CreateAssetResponseBuilder) RequestId(v string) *CreateAssetResponseBuilder {
b.s.RequestId = &v
return b
}

func (b *CreateAssetResponseBuilder) ErrorMessage(v string) *CreateAssetResponseBuilder {
b.s.ErrorMessage = &v
return b
}

func (b *CreateAssetResponseBuilder) ErrorCode(v string) *CreateAssetResponseBuilder {
b.s.ErrorCode = &v
return b
}

func (b *CreateAssetResponseBuilder) State(v string) *CreateAssetResponseBuilder {
b.s.State = &v
return b
}

func (b *CreateAssetResponseBuilder) Body(v string) *CreateAssetResponseBuilder {
b.s.Body = &v
return b
}

func (b *CreateAssetResponseBuilder) Build() *CreateAssetResponse {
return b.s
}



+ 139
- 0
third_party/ecloudsdkmaas/model/create_body.go Просмотреть файл

@@ -0,0 +1,139 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateBody struct {
position.Body
// 智能体系统人设
SystemPrompt *string `json:"systemPrompt,omitempty"`
// 推理服务ID,参考模型广场中,选择对应的模型中的API调用地址中的inf开头的部分,比如deepseek-r1的推理id 是inf-1336781912337387520, 不是所有的模型智能体都支持,详情可参考创建智能体页面或者联系客服人员
InferenceId *string `json:"inferenceId,omitempty"`
// 智能体图标可以不传,如果传则是bas64编码格式(更新智能体的icon也是一样的构建方法),小于50KB png、jpg和svg格式, data:image/jpeg;base64,{base64编码} python代码样例如下: ```language import base64 import os def image_to_base64_with_prefix(image_path): """ 将本地图片文件转换为Base64编码并添加数据前缀 Args: image_path (str): 图片文件路径 Returns: str: 带有数据URL前缀的Base64编码字符串 """ # 获取文件扩展名以确定MIME类型 _, ext = os.path.splitext(image_path) ext = ext.lower() # 根据文件扩展名确定MIME类型 mime_types = { '.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png' } mime_type = mime_types.get(ext, 'jpeg') # 默认为jpeg # 读取图片文件并转换为Base64 with open(image_path, 'rb') as image_file: encoded_string = base64.b64encode(image_file.read()).decode('utf-8') # 添加数据前缀 return f"data:image/{mime_type};base64,{encoded_string}" ```
Icon *string `json:"icon,omitempty"`
// 智能体名称(支持中英文、数字、下划线(_),2-20个字符,不能以下划线为开头)
Name *string `json:"name,omitempty"`
// 推荐问
SuggestQuestions []string `json:"suggestQuestions,omitempty"`
// 知识库ID(最多5个)
DocIdList []string `json:"docIdList,omitempty"`
// 智能体简介(不超过200字)
Introduction *string `json:"introduction,omitempty"`
// 开场白
Prologue *string `json:"prologue,omitempty"`
}

func (s CreateBody) String() string {
return utils.Beautify(s)
}

func (s CreateBody) GoString() string {
return s.String()
}

func (s CreateBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateBody) SetSystemPrompt(v string) *CreateBody {
s.SystemPrompt = &v
return s
}

func (s *CreateBody) SetInferenceId(v string) *CreateBody {
s.InferenceId = &v
return s
}

func (s *CreateBody) SetIcon(v string) *CreateBody {
s.Icon = &v
return s
}

func (s *CreateBody) SetName(v string) *CreateBody {
s.Name = &v
return s
}

func (s *CreateBody) SetSuggestQuestions(v []string) *CreateBody {
s.SuggestQuestions = v
return s
}

func (s *CreateBody) SetDocIdList(v []string) *CreateBody {
s.DocIdList = v
return s
}

func (s *CreateBody) SetIntroduction(v string) *CreateBody {
s.Introduction = &v
return s
}

func (s *CreateBody) SetPrologue(v string) *CreateBody {
s.Prologue = &v
return s
}


type CreateBodyBuilder struct {
s *CreateBody
}

func NewCreateBodyBuilder() *CreateBodyBuilder {
s := &CreateBody{}
b := &CreateBodyBuilder{s: s}
return b
}

func (b *CreateBodyBuilder) SystemPrompt(v string) *CreateBodyBuilder {
b.s.SystemPrompt = &v
return b
}

func (b *CreateBodyBuilder) InferenceId(v string) *CreateBodyBuilder {
b.s.InferenceId = &v
return b
}

func (b *CreateBodyBuilder) Icon(v string) *CreateBodyBuilder {
b.s.Icon = &v
return b
}

func (b *CreateBodyBuilder) Name(v string) *CreateBodyBuilder {
b.s.Name = &v
return b
}

func (b *CreateBodyBuilder) SuggestQuestions(v []string) *CreateBodyBuilder {
b.s.SuggestQuestions = v
return b
}

func (b *CreateBodyBuilder) DocIdList(v []string) *CreateBodyBuilder {
b.s.DocIdList = v
return b
}

func (b *CreateBodyBuilder) Introduction(v string) *CreateBodyBuilder {
b.s.Introduction = &v
return b
}

func (b *CreateBodyBuilder) Prologue(v string) *CreateBodyBuilder {
b.s.Prologue = &v
return b
}

func (b *CreateBodyBuilder) Build() *CreateBody {
return b.s
}



+ 79
- 0
third_party/ecloudsdkmaas/model/create_dataset_body.go Просмотреть файл

@@ -0,0 +1,79 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateDatasetBody struct {
position.Body
// 知识库名称
Name *string `json:"name,omitempty"`
// 知识库描述
Description *string `json:"description,omitempty"`
// 检索设置
RetrievalConfig *CreateDatasetRequestRetrievalConfig `json:"retrievalConfig,omitempty"`
}

func (s CreateDatasetBody) String() string {
return utils.Beautify(s)
}

func (s CreateDatasetBody) GoString() string {
return s.String()
}

func (s CreateDatasetBody) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateDatasetBody) SetName(v string) *CreateDatasetBody {
s.Name = &v
return s
}

func (s *CreateDatasetBody) SetDescription(v string) *CreateDatasetBody {
s.Description = &v
return s
}

func (s *CreateDatasetBody) SetRetrievalConfig(v *CreateDatasetRequestRetrievalConfig) *CreateDatasetBody {
s.RetrievalConfig = v
return s
}


type CreateDatasetBodyBuilder struct {
s *CreateDatasetBody
}

func NewCreateDatasetBodyBuilder() *CreateDatasetBodyBuilder {
s := &CreateDatasetBody{}
b := &CreateDatasetBodyBuilder{s: s}
return b
}

func (b *CreateDatasetBodyBuilder) Name(v string) *CreateDatasetBodyBuilder {
b.s.Name = &v
return b
}

func (b *CreateDatasetBodyBuilder) Description(v string) *CreateDatasetBodyBuilder {
b.s.Description = &v
return b
}

func (b *CreateDatasetBodyBuilder) RetrievalConfig(v *CreateDatasetRequestRetrievalConfig) *CreateDatasetBodyBuilder {
b.s.RetrievalConfig = v
return b
}

func (b *CreateDatasetBodyBuilder) Build() *CreateDatasetBody {
return b.s
}



+ 55
- 0
third_party/ecloudsdkmaas/model/create_dataset_query.go Просмотреть файл

@@ -0,0 +1,55 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/position"
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateDatasetQuery struct {
position.Query
// 项目ID,参考智算平台openApi文档,项目空间列表接口
ProjectId *string `json:"projectId,omitempty"`
}

func (s CreateDatasetQuery) String() string {
return utils.Beautify(s)
}

func (s CreateDatasetQuery) GoString() string {
return s.String()
}

func (s CreateDatasetQuery) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateDatasetQuery) SetProjectId(v string) *CreateDatasetQuery {
s.ProjectId = &v
return s
}


type CreateDatasetQueryBuilder struct {
s *CreateDatasetQuery
}

func NewCreateDatasetQueryBuilder() *CreateDatasetQueryBuilder {
s := &CreateDatasetQuery{}
b := &CreateDatasetQueryBuilder{s: s}
return b
}

func (b *CreateDatasetQueryBuilder) ProjectId(v string) *CreateDatasetQueryBuilder {
b.s.ProjectId = &v
return b
}

func (b *CreateDatasetQueryBuilder) Build() *CreateDatasetQuery {
return b.s
}



+ 66
- 0
third_party/ecloudsdkmaas/model/create_dataset_request.go Просмотреть файл

@@ -0,0 +1,66 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)

type CreateDatasetRequest struct {


CreateDatasetQuery *CreateDatasetQuery `json:"createDatasetQuery,omitempty"`

CreateDatasetBody *CreateDatasetBody `json:"createDatasetBody,omitempty"`
}

func (s CreateDatasetRequest) String() string {
return utils.Beautify(s)
}

func (s CreateDatasetRequest) GoString() string {
return s.String()
}

func (s CreateDatasetRequest) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateDatasetRequest) SetCreateDatasetQuery(v *CreateDatasetQuery) *CreateDatasetRequest {
s.CreateDatasetQuery = v
return s
}

func (s *CreateDatasetRequest) SetCreateDatasetBody(v *CreateDatasetBody) *CreateDatasetRequest {
s.CreateDatasetBody = v
return s
}


type CreateDatasetRequestBuilder struct {
s *CreateDatasetRequest
}

func NewCreateDatasetRequestBuilder() *CreateDatasetRequestBuilder {
s := &CreateDatasetRequest{}
b := &CreateDatasetRequestBuilder{s: s}
return b
}

func (b *CreateDatasetRequestBuilder) CreateDatasetQuery(v *CreateDatasetQuery) *CreateDatasetRequestBuilder {
b.s.CreateDatasetQuery = v
return b
}

func (b *CreateDatasetRequestBuilder) CreateDatasetBody(v *CreateDatasetBody) *CreateDatasetRequestBuilder {
b.s.CreateDatasetBody = v
return b
}

func (b *CreateDatasetRequestBuilder) Build() *CreateDatasetRequest {
return b.s
}



+ 123
- 0
third_party/ecloudsdkmaas/model/create_dataset_request_retrieval_config.go Просмотреть файл

@@ -0,0 +1,123 @@
// @Title Golang SDK Client
// @Description This code is auto generated
// @Author Ecloud SDK

package model

import (
"gitlab.ecloud.com/ecloud/ecloudsdkcore/utils"
)
type CreateDatasetRequestRetrievalConfigSearchMethodEnum string

// List of SearchMethod
const (
CreateDatasetRequestRetrievalConfigSearchMethodEnumSemantic CreateDatasetRequestRetrievalConfigSearchMethodEnum = "SEMANTIC"
CreateDatasetRequestRetrievalConfigSearchMethodEnumFullText CreateDatasetRequestRetrievalConfigSearchMethodEnum = "FULL_TEXT"
CreateDatasetRequestRetrievalConfigSearchMethodEnumHybridRerankingModel CreateDatasetRequestRetrievalConfigSearchMethodEnum = "HYBRID_RERANKING_MODEL"
CreateDatasetRequestRetrievalConfigSearchMethodEnumHybridWeightedScore CreateDatasetRequestRetrievalConfigSearchMethodEnum = "HYBRID_WEIGHTED_SCORE"
)

type CreateDatasetRequestRetrievalConfig struct {

// Score阈值 ,用于设置文本片段筛选的相似度阈值,只召回超过设置分数的文本片段,数值越高说明对于文本与问题要求的相似度越高,预期被召回的文本数量也越少,默认0.5,只有当score_threshold_enabled=true时,该配置才生效
ScoreThreshold *float32 `json:"score_threshold,omitempty"`
// 默认都开启,是否开启排序模型
RerankingEnabled *bool `json:"reranking_enabled,omitempty"`
// 是否设置Score阈值
ScoreThresholdEnabled *bool `json:"score_threshold_enabled,omitempty"`
// 召回数量 ,用于筛选与用户问题相似度最高的文本片段。系统同时会根据选用模型上下文窗口大小动态调整片段数量,数值越高,预期被召回的文本分段数量越多。
TopK *int32 `json:"top_k,omitempty"`
// 权重设置,search_method=HYBRID_WEIGHTED_SCORE时才需要设置
Weights *CreateDatasetRequestWeights `json:"weights,omitempty"`
// 检索方式: HYBRID_WEIGHTED_SCORE: 混合检索; HYBRID_RERANKING_MODEL: 混合检索 SEMANTIC:向量检索; FULL_TEXT:全文检索 HYBRID_WEIGHTED_SCORE和HYBRID_RERANKING_MODEL的区别就是是否要设置权重即weights的值
SearchMethod *CreateDatasetRequestRetrievalConfigSearchMethodEnum `json:"search_method,omitempty"`
}

func (s CreateDatasetRequestRetrievalConfig) String() string {
return utils.Beautify(s)
}

func (s CreateDatasetRequestRetrievalConfig) GoString() string {
return s.String()
}

func (s CreateDatasetRequestRetrievalConfig) ToJsonString() string {
return utils.ToJsonString(s)
}

func (s *CreateDatasetRequestRetrievalConfig) SetScoreThreshold(v float32) *CreateDatasetRequestRetrievalConfig {
s.ScoreThreshold = &v
return s
}

func (s *CreateDatasetRequestRetrievalConfig) SetRerankingEnabled(v bool) *CreateDatasetRequestRetrievalConfig {
s.RerankingEnabled = &v
return s
}

func (s *CreateDatasetRequestRetrievalConfig) SetScoreThresholdEnabled(v bool) *CreateDatasetRequestRetrievalConfig {
s.ScoreThresholdEnabled = &v
return s
}

func (s *CreateDatasetRequestRetrievalConfig) SetTopK(v int32) *CreateDatasetRequestRetrievalConfig {
s.TopK = &v
return s
}

func (s *CreateDatasetRequestRetrievalConfig) SetWeights(v *CreateDatasetRequestWeights) *CreateDatasetRequestRetrievalConfig {
s.Weights = v
return s
}

func (s *CreateDatasetRequestRetrievalConfig) SetSearchMethod(v CreateDatasetRequestRetrievalConfigSearchMethodEnum) *CreateDatasetRequestRetrievalConfig {
s.SearchMethod = &v
return s
}


type CreateDatasetRequestRetrievalConfigBuilder struct {
s *CreateDatasetRequestRetrievalConfig
}

func NewCreateDatasetRequestRetrievalConfigBuilder() *CreateDatasetRequestRetrievalConfigBuilder {
s := &CreateDatasetRequestRetrievalConfig{}
b := &CreateDatasetRequestRetrievalConfigBuilder{s: s}
return b
}

func (b *CreateDatasetRequestRetrievalConfigBuilder) ScoreThreshold(v float32) *CreateDatasetRequestRetrievalConfigBuilder {
b.s.ScoreThreshold = &v
return b
}

func (b *CreateDatasetRequestRetrievalConfigBuilder) RerankingEnabled(v bool) *CreateDatasetRequestRetrievalConfigBuilder {
b.s.RerankingEnabled = &v
return b
}

func (b *CreateDatasetRequestRetrievalConfigBuilder) ScoreThresholdEnabled(v bool) *CreateDatasetRequestRetrievalConfigBuilder {
b.s.ScoreThresholdEnabled = &v
return b
}

func (b *CreateDatasetRequestRetrievalConfigBuilder) TopK(v int32) *CreateDatasetRequestRetrievalConfigBuilder {
b.s.TopK = &v
return b
}

func (b *CreateDatasetRequestRetrievalConfigBuilder) Weights(v *CreateDatasetRequestWeights) *CreateDatasetRequestRetrievalConfigBuilder {
b.s.Weights = v
return b
}

func (b *CreateDatasetRequestRetrievalConfigBuilder) SearchMethod(v CreateDatasetRequestRetrievalConfigSearchMethodEnum) *CreateDatasetRequestRetrievalConfigBuilder {
b.s.SearchMethod = &v
return b
}

func (b *CreateDatasetRequestRetrievalConfigBuilder) Build() *CreateDatasetRequestRetrievalConfig {
return b.s
}



Некоторые файлы не были показаны из-за большого количества измененных файлов

Загрузка…
Отмена
Сохранить