Co-Authored-By: Codex <noreply@anthropic.com>master
| @@ -34,6 +34,9 @@ docs/superpowers | |||||
| # Local dev server logs | # Local dev server logs | ||||
| web/vite-*.log | web/vite-*.log | ||||
| /server_*.log | |||||
| /server_*_err.log | |||||
| /asset_test_log.md | |||||
| # E2E / Playwright artifacts | # E2E / Playwright artifacts | ||||
| test-artifacts/ | test-artifacts/ | ||||
| @@ -23,6 +23,9 @@ ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} | |||||
| WORKDIR /build | WORKDIR /build | ||||
| ADD go.mod go.sum ./ | 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 | RUN --mount=type=cache,target=/go/pkg/mod go mod download | ||||
| COPY . . | COPY . . | ||||
| @@ -57,6 +57,8 @@ func ChannelType2APIType(channelType int) (int, bool) { | |||||
| apiType = constant.APITypeVolcEngine | apiType = constant.APITypeVolcEngine | ||||
| case constant.ChannelTypeDoubaoVideoCompatibleTianyiYun: | case constant.ChannelTypeDoubaoVideoCompatibleTianyiYun: | ||||
| apiType = constant.APITypeVolcEngine | apiType = constant.APITypeVolcEngine | ||||
| case constant.ChannelTypeChinaMobileSeedance: | |||||
| apiType = constant.APITypeVolcEngine | |||||
| case constant.ChannelTypeBaiduV2: | case constant.ChannelTypeBaiduV2: | ||||
| apiType = constant.APITypeBaiduV2 | apiType = constant.APITypeBaiduV2 | ||||
| case constant.ChannelTypeOpenRouter: | 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} | endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse} | ||||
| case constant.ChannelTypeSora: | case constant.ChannelTypeSora: | ||||
| endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIVideo} | 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} | endpointTypes = []constant.EndpointType{constant.EndpointTypeDoubaoVideo} | ||||
| default: | default: | ||||
| if IsOpenAIResponseOnlyModel(modelName) { | if IsOpenAIResponseOnlyModel(modelName) { | ||||
| @@ -58,6 +58,7 @@ const ( | |||||
| ChannelTypeDoubaoVideoCompatibleAiping = 58 | ChannelTypeDoubaoVideoCompatibleAiping = 58 | ||||
| ChannelTypeKlingAiping = 59 | ChannelTypeKlingAiping = 59 | ||||
| ChannelTypeDoubaoVideoCompatibleTianyiYun = 60 | ChannelTypeDoubaoVideoCompatibleTianyiYun = 60 | ||||
| ChannelTypeChinaMobileSeedance = 61 | |||||
| ChannelTypeDummy // this one is only for count, do not add any channel after this | ChannelTypeDummy // this one is only for count, do not add any channel after this | ||||
| ) | ) | ||||
| @@ -124,6 +125,7 @@ var ChannelBaseURLs = []string{ | |||||
| "", //58 | "", //58 | ||||
| "https://aiping.cn/api", //59 | "https://aiping.cn/api", //59 | ||||
| "https://ai.ctaigw.cn", //60 | "https://ai.ctaigw.cn", //60 | ||||
| "https://zhenze-huhehaote.cmecloud.cn/api/v3", //61 | |||||
| } | } | ||||
| var ChannelTypeNames = map[int]string{ | var ChannelTypeNames = map[int]string{ | ||||
| @@ -184,6 +186,7 @@ var ChannelTypeNames = map[int]string{ | |||||
| ChannelTypeDoubaoVideoCompatibleAiping: "DoubaoVideoCompatibleAiping", | ChannelTypeDoubaoVideoCompatibleAiping: "DoubaoVideoCompatibleAiping", | ||||
| ChannelTypeKlingAiping: "KlingAiping", | ChannelTypeKlingAiping: "KlingAiping", | ||||
| ChannelTypeDoubaoVideoCompatibleTianyiYun: "DoubaoVideoCompatibleTianyiYun", | ChannelTypeDoubaoVideoCompatibleTianyiYun: "DoubaoVideoCompatibleTianyiYun", | ||||
| ChannelTypeChinaMobileSeedance: "ChinaMobileSeedance", | |||||
| } | } | ||||
| func GetChannelTypeName(channelType int) string { | func GetChannelTypeName(channelType int) string { | ||||
| @@ -67,6 +67,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, | |||||
| constant.ChannelTypeDoubaoVideo, | constant.ChannelTypeDoubaoVideo, | ||||
| constant.ChannelTypeDoubaoVideoCompatibleAiping, | constant.ChannelTypeDoubaoVideoCompatibleAiping, | ||||
| constant.ChannelTypeDoubaoVideoCompatibleTianyiYun, | constant.ChannelTypeDoubaoVideoCompatibleTianyiYun, | ||||
| constant.ChannelTypeChinaMobileSeedance, | |||||
| constant.ChannelTypeVidu, | constant.ChannelTypeVidu, | ||||
| } | } | ||||
| if lo.Contains(unsupportedTestChannelTypes, channel.Type) { | if lo.Contains(unsupportedTestChannelTypes, channel.Type) { | ||||
| @@ -5,30 +5,17 @@ import ( | |||||
| "fmt" | "fmt" | ||||
| "io" | "io" | ||||
| "net/http" | "net/http" | ||||
| "net/url" | |||||
| "strings" | "strings" | ||||
| "time" | |||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/constant" | "github.com/QuantumNous/new-api/constant" | ||||
| "github.com/QuantumNous/new-api/logger" | "github.com/QuantumNous/new-api/logger" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| "github.com/QuantumNous/new-api/service" | "github.com/QuantumNous/new-api/service" | ||||
| "github.com/QuantumNous/new-api/setting/system_setting" | |||||
| "github.com/gin-gonic/gin" | "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) { | func assetProxyError(c *gin.Context, status int, errType, message string) { | ||||
| c.JSON(status, gin.H{ | c.JSON(status, gin.H{ | ||||
| "error": 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 { | func effectiveDoubaoAssetGroup(c *gin.Context) string { | ||||
| if group := strings.TrimSpace(common.GetContextKeyString(c, constant.ContextKeyUsingGroup)); group != "" { | if group := strings.TrimSpace(common.GetContextKeyString(c, constant.ContextKeyUsingGroup)); group != "" { | ||||
| @@ -98,34 +56,15 @@ func concreteDoubaoAssetGroupsForRequest(c *gin.Context, autoGroups func(string) | |||||
| return concreteGroups | 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) { | 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 | 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 | return | ||||
| } | } | ||||
| @@ -134,70 +73,85 @@ func DoubaoAssetProxy(c *gin.Context) { | |||||
| version = "2024-01-01" | version = "2024-01-01" | ||||
| } | } | ||||
| channel, _, err := resolveDoubaoAssetChannelForRequest(c) | |||||
| rawBody, err := io.ReadAll(c.Request.Body) | |||||
| if err != nil { | 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 | 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 | 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 | 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 { | for _, value := range values { | ||||
| c.Writer.Header().Add(key, value) | 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 | package controller | ||||
| import ( | import ( | ||||
| "context" | |||||
| "fmt" | |||||
| "net/http" | "net/http" | ||||
| "net/http/httptest" | "net/http/httptest" | ||||
| "os" | |||||
| "strings" | |||||
| "testing" | "testing" | ||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/constant" | "github.com/QuantumNous/new-api/constant" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| "github.com/QuantumNous/new-api/service" | |||||
| "github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
| "github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
| "github.com/stretchr/testify/require" | "github.com/stretchr/testify/require" | ||||
| "gorm.io/gorm" | |||||
| "gorm.io/gorm/logger" | |||||
| ) | ) | ||||
| func setupDoubaoAssetProxyRouter(t *testing.T) *gin.Engine { | func setupDoubaoAssetProxyRouter(t *testing.T) *gin.Engine { | ||||
| @@ -40,6 +47,74 @@ func decodeDoubaoAssetErrorMessage(t *testing.T, body string) string { | |||||
| return payload.Error.Message | 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) { | func TestDoubaoAssetProxyMissingActionReturns400(t *testing.T) { | ||||
| router := setupDoubaoAssetProxyRouter(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())) | 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) | 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) { | 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() | w := httptest.NewRecorder() | ||||
| req.Header.Set("Content-Type", "application/json") | |||||
| router.ServeHTTP(w, req) | 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) { | func TestEffectiveDoubaoAssetGroupUsesUsingGroupBeforeBlankTokenGroup(t *testing.T) { | ||||
| @@ -101,3 +371,49 @@ func TestConcreteDoubaoAssetGroupsForAutoUsesUserAutoGroups(t *testing.T) { | |||||
| assert.Equal(t, []string{"default", "vip"}, groups) | 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/tidwall/sjson v1.2.5 | ||||
| github.com/tiktoken-go/tokenizer v0.6.2 | github.com/tiktoken-go/tokenizer v0.6.2 | ||||
| github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c | 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/crypto v0.48.0 | ||||
| golang.org/x/image v0.23.0 | golang.org/x/image v0.23.0 | ||||
| golang.org/x/net v0.49.0 | golang.org/x/net v0.49.0 | ||||
| @@ -60,6 +62,7 @@ require ( | |||||
| gorm.io/driver/mysql v1.4.3 | gorm.io/driver/mysql v1.4.3 | ||||
| gorm.io/driver/postgres v1.5.2 | gorm.io/driver/postgres v1.5.2 | ||||
| gorm.io/gorm v1.25.2 | gorm.io/gorm v1.25.2 | ||||
| maas_seedance_sdk_1.0.0_go v1.0.0 | |||||
| ) | ) | ||||
| require ( | require ( | ||||
| @@ -110,6 +113,7 @@ require ( | |||||
| github.com/jfreymuth/vorbis v1.0.2 // indirect | github.com/jfreymuth/vorbis v1.0.2 // indirect | ||||
| github.com/jinzhu/inflection v1.0.0 // indirect | github.com/jinzhu/inflection v1.0.0 // indirect | ||||
| github.com/jinzhu/now v1.1.5 // 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/json-iterator/go v1.1.12 // indirect | ||||
| github.com/klauspost/compress v1.18.0 // indirect | github.com/klauspost/compress v1.18.0 // indirect | ||||
| github.com/klauspost/cpuid/v2 v2.3.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/modern-go/reflect2 v1.0.2 // indirect | ||||
| github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||||
| github.com/ncruces/go-strftime v0.1.9 // 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/pelletier/go-toml/v2 v2.2.1 // indirect | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| github.com/prometheus/client_model v0.6.1 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | ||||
| github.com/samber/go-singleflightx v0.3.2 // indirect | github.com/samber/go-singleflightx v0.3.2 // indirect | ||||
| github.com/tidwall/match v1.1.1 // 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/go-sysconf v0.3.12 // indirect | ||||
| github.com/tklauser/numcpus v0.6.1 // indirect | github.com/tklauser/numcpus v0.6.1 // indirect | ||||
| github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| github.com/ugorji/go/codec v1.2.12 // 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/x448/float16 v0.8.4 // indirect | ||||
| github.com/yusufpapurcu/wmi v1.2.3 // indirect | github.com/yusufpapurcu/wmi v1.2.3 // indirect | ||||
| golang.org/x/arch v0.21.0 // indirect | golang.org/x/arch v0.21.0 // indirect | ||||
| golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect | ||||
| google.golang.org/protobuf v1.36.5 // 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/libc v1.66.10 // indirect | ||||
| modernc.org/mathutil v1.7.1 // indirect | modernc.org/mathutil v1.7.1 // indirect | ||||
| modernc.org/memory v1.11.0 // indirect | modernc.org/memory v1.11.0 // indirect | ||||
| modernc.org/sqlite v1.40.1 // 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 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= | ||||
| github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= | github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= | ||||
| github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A= | 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/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 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI= | ||||
| github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8= | 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 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo= | ||||
| github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= | 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= | 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 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 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= | ||||
| github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= | 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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | ||||
| github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | 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 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= | ||||
| github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= | ||||
| github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= | 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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | 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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= | ||||
| github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= | ||||
| github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= | 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-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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= | ||||
| github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= | 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 h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= | ||||
| github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | 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.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/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.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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | 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 h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= | ||||
| github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= | 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.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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | 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= | 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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | 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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | 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= | 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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= | ||||
| github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= | 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.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.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.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= | ||||
| github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | 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/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 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | ||||
| github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= | 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 h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= | ||||
| github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= | 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= | 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/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 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= | ||||
| github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= | 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 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= | ||||
| github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= | ||||
| github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= | 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/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | 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.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/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/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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= | ||||
| github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= | 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.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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= | ||||
| github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= | 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= | 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.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= | ||||
| github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | 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/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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= | ||||
| github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= | ||||
| github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= | 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/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 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= | ||||
| github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= | 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 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= | ||||
| go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= | ||||
| go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= | 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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= | ||||
| golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= | golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= | ||||
| golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= | 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 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/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 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= | ||||
| golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= | 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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||
| golang.org/x/mod v0.12.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.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 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= | ||||
| golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= | 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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= | ||||
| golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= | 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/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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.1.0/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.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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= | ||||
| golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= | 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-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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= | ||||
| golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= | 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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
| golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | 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/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.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.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 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= | ||||
| google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | 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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | 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= | 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.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 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= | ||||
| gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= | 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 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= | ||||
| modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= | 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= | modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= | ||||
| @@ -207,7 +207,7 @@ func InitDB() (err error) { | |||||
| return err | return err | ||||
| } | } | ||||
| LoadEmailQuotaCache() | LoadEmailQuotaCache() | ||||
| return nil | |||||
| return nil | |||||
| } else { | } else { | ||||
| common.FatalLog(err) | common.FatalLog(err) | ||||
| } | } | ||||
| @@ -287,6 +287,7 @@ func migrateDB() error { | |||||
| &EmailQuotaRule{}, | &EmailQuotaRule{}, | ||||
| &UserModelRateLimit{}, | &UserModelRateLimit{}, | ||||
| &UserAssetChannel{}, | &UserAssetChannel{}, | ||||
| &UserAssetGroup{}, | |||||
| &UserMigrationBatch{}, | &UserMigrationBatch{}, | ||||
| &UserMigrationItem{}, | &UserMigrationItem{}, | ||||
| &MigrationQuotaGrant{}, | &MigrationQuotaGrant{}, | ||||
| @@ -355,6 +356,7 @@ func migrateDBFast() error { | |||||
| {&QuotaSyncLog{}, "QuotaSyncLog"}, | {&QuotaSyncLog{}, "QuotaSyncLog"}, | ||||
| {&EmailQuotaRule{}, "EmailQuotaRule"}, | {&EmailQuotaRule{}, "EmailQuotaRule"}, | ||||
| {&UserAssetChannel{}, "UserAssetChannel"}, | {&UserAssetChannel{}, "UserAssetChannel"}, | ||||
| {&UserAssetGroup{}, "UserAssetGroup"}, | |||||
| {&UserMigrationBatch{}, "UserMigrationBatch"}, | {&UserMigrationBatch{}, "UserMigrationBatch"}, | ||||
| {&UserMigrationItem{}, "UserMigrationItem"}, | {&UserMigrationItem{}, "UserMigrationItem"}, | ||||
| {&MigrationQuotaGrant{}, "MigrationQuotaGrant"}, | {&MigrationQuotaGrant{}, "MigrationQuotaGrant"}, | ||||
| @@ -73,6 +73,14 @@ func GetUserAssetChannelsByTypes(userId int, channelTypes []int, group string) ( | |||||
| return bindings, err | 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 { | func DeleteUserAssetChannelsByTypesWithTx(tx *gorm.DB, userId int, channelTypes []int, group string) error { | ||||
| if len(channelTypes) == 0 { | if len(channelTypes) == 0 { | ||||
| return nil | return nil | ||||
| @@ -92,6 +92,26 @@ func TestGetUserAssetChannelsByTypesSortsLatestFirst(t *testing.T) { | |||||
| assert.Equal(t, 2, bindings[1].ChannelType) | 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) { | func TestBindUserAssetChannelWithTxUpserts(t *testing.T) { | ||||
| db := setupUserAssetChannelDB(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: "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-0611", UpstreamModelName: "cdance2.0-0611", ChannelType: constant.ChannelTypeDoubaoVideoCompatibleTianyiYun}, | ||||
| {BillingModelName: "cdance2.0-fast-0611", UpstreamModelName: "cdance2.0-fast-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 | // 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}, | ||||
| {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")) | 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) { | func TestSupportsMatrixUsageBilling_KlingAipingVideoModels(t *testing.T) { | ||||
| cases := []struct { | cases := []struct { | ||||
| billingModel string | billingModel string | ||||
| @@ -31,6 +31,7 @@ import ( | |||||
| "github.com/QuantumNous/new-api/relay/channel/siliconflow" | "github.com/QuantumNous/new-api/relay/channel/siliconflow" | ||||
| "github.com/QuantumNous/new-api/relay/channel/submodel" | "github.com/QuantumNous/new-api/relay/channel/submodel" | ||||
| taskali "github.com/QuantumNous/new-api/relay/channel/task/ali" | 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" | taskdoubao "github.com/QuantumNous/new-api/relay/channel/task/doubao" | ||||
| taskdoubaoaiping "github.com/QuantumNous/new-api/relay/channel/task/doubao_aiping" | taskdoubaoaiping "github.com/QuantumNous/new-api/relay/channel/task/doubao_aiping" | ||||
| taskdoubaotianyiyun "github.com/QuantumNous/new-api/relay/channel/task/doubao_tianyiyun" | 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{} | return &taskdoubaoaiping.TaskAdaptor{} | ||||
| case constant.ChannelTypeDoubaoVideoCompatibleTianyiYun: | case constant.ChannelTypeDoubaoVideoCompatibleTianyiYun: | ||||
| return &taskdoubaotianyiyun.TaskAdaptor{} | return &taskdoubaotianyiyun.TaskAdaptor{} | ||||
| case constant.ChannelTypeChinaMobileSeedance: | |||||
| return &taskchinamobileseedance.TaskAdaptor{} | |||||
| case constant.ChannelTypeKlingAiping: | case constant.ChannelTypeKlingAiping: | ||||
| return &klingaiping.TaskAdaptor{} | return &klingaiping.TaskAdaptor{} | ||||
| case constant.ChannelTypeSora, constant.ChannelTypeOpenAI: | 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 | return nil | ||||
| } | } | ||||
| fetchModel := task.Properties.UpstreamModelName | |||||
| if strings.TrimSpace(fetchModel) == "" { | |||||
| fetchModel = task.Properties.OriginModelName | |||||
| } | |||||
| resp, err := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{ | resp, err := adaptor.FetchTask(baseURL, channelModel.Key, map[string]any{ | ||||
| "task_id": task.GetUpstreamTaskID(), | "task_id": task.GetUpstreamTaskID(), | ||||
| "action": task.Action, | "action": task.Action, | ||||
| "model": fetchModel, | |||||
| }, proxy) | }, proxy) | ||||
| if err != nil || resp == nil { | if err != nil || resp == nil { | ||||
| return 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) | 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 --- | // --- GetUserAutoGroup tests --- | ||||
| func TestGetUserAutoGroup_Basic(t *testing.T) { | 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{ | return []int{ | ||||
| constant.ChannelTypeDoubaoVideoCompatibleAiping, | constant.ChannelTypeDoubaoVideoCompatibleAiping, | ||||
| constant.ChannelTypeDoubaoVideoCompatibleTianyiYun, | constant.ChannelTypeDoubaoVideoCompatibleTianyiYun, | ||||
| constant.ChannelTypeChinaMobileSeedance, | |||||
| } | } | ||||
| case VideoAssetFamilyKling: | case VideoAssetFamilyKling: | ||||
| return []int{ | return []int{ | ||||
| @@ -23,6 +23,10 @@ func TestGetBoundVideoAssetChannelForModelFindsSeedanceTianyiYunBinding(t *testi | |||||
| assert.Equal(t, 16, ch.Id) | assert.Equal(t, 16, ch.Id) | ||||
| } | } | ||||
| func TestVideoAssetSeedanceFamilyIncludesChinaMobileSeedance(t *testing.T) { | |||||
| require.Contains(t, VideoAssetChannelTypesForFamily(VideoAssetFamilySeedance), constant.ChannelTypeChinaMobileSeedance) | |||||
| } | |||||
| func TestGetBoundVideoAssetChannelForModelFindsKlingBindingByFamily(t *testing.T) { | func TestGetBoundVideoAssetChannelForModelFindsKlingBindingByFamily(t *testing.T) { | ||||
| db := setupDoubaoAssetChannelDB(t) | db := setupDoubaoAssetChannelDB(t) | ||||
| createDoubaoAssetChannelForTest(t, db, 59, constant.ChannelTypeKlingAiping, "default", "key", common.ChannelStatusEnabled) | 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 | |||||
| } | |||||