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 }