Co-Authored-By: Codex <noreply@anthropic.com>master
| @@ -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/ | |||
| @@ -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 . . | |||
| @@ -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: | |||
| @@ -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) | |||
| } | |||
| @@ -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) { | |||
| @@ -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 { | |||
| @@ -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) { | |||
| @@ -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())) | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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. | |||
| @@ -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 | |||
| @@ -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= | |||
| @@ -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"}, | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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" | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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" | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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}, | |||
| @@ -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 | |||
| @@ -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: | |||
| @@ -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()) | |||
| } | |||
| @@ -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 | |||
| @@ -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 | |||
| } | |||
| @@ -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()) | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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), | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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"`) | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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)) | |||
| } | |||
| @@ -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) { | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -23,6 +23,7 @@ func VideoAssetChannelTypesForFamily(family VideoAssetFamily) []int { | |||
| return []int{ | |||
| constant.ChannelTypeDoubaoVideoCompatibleAiping, | |||
| constant.ChannelTypeDoubaoVideoCompatibleTianyiYun, | |||
| constant.ChannelTypeChinaMobileSeedance, | |||
| } | |||
| case VideoAssetFamilyKling: | |||
| return []int{ | |||
| @@ -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) | |||
| @@ -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); | |||
| @@ -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 | |||
| @@ -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 = ¢ralTransportEnabled | |||
| } | |||
| } | |||
| // 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() | |||
| } | |||
| @@ -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" | |||
| ) | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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...) | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 = "=" | |||
| ) | |||
| @@ -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), | |||
| } | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| module gitlab.ecloud.com/ecloud/ecloudsdkcore | |||
| go 1.14 | |||
| require github.com/google/uuid v1.6.0 | |||
| @@ -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") | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| package position | |||
| type Body struct { | |||
| } | |||
| type Query struct { | |||
| } | |||
| type Path struct { | |||
| } | |||
| type Header struct { | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| package region | |||
| type Region struct { | |||
| PoolId *string `json:"poolId"` | |||
| Endpoint *string `json:"endpoint"` | |||
| RegionId *string `json:"regionId"` | |||
| } | |||
| @@ -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] | |||
| } | |||
| @@ -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"` | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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)) | |||
| } | |||
| } | |||
| } | |||
| @@ -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, | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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[:]), "[]"), "\"", "") | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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) | |||
| } | |||
| @@ -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()) | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| module gitlab.ecloud.com/ecloud/ecloudsdkmaas | |||
| go 1.14 | |||
| require gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.6 | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 | |||
| } | |||