Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

759 lignes
25 KiB

  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "strings"
  10. "github.com/QuantumNous/new-api/common"
  11. "github.com/QuantumNous/new-api/model"
  12. cmerrs "gitlab.ecloud.com/ecloud/ecloudsdkcore/errs"
  13. cmmodel "gitlab.ecloud.com/ecloud/ecloudsdkmaas/model"
  14. )
  15. const defaultChinaMobileAssetBaseURL = "https://ecloud.10086.cn"
  16. const defaultChinaMobileAssetPoolID = "CIDC-CORE-00"
  17. const chinaMobileAssetAKEnv = "CHINAMOBILE_ASSET_AK"
  18. const chinaMobileAssetSKEnv = "CHINAMOBILE_ASSET_SK"
  19. const chinaMobileAssetPoolIDEnv = "CHINAMOBILE_ASSET_POOL_ID"
  20. type ChinaMobileAssetAdapter struct {
  21. newClient func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient
  22. }
  23. func NewChinaMobileAssetAdapter() AssetAdapter {
  24. return &ChinaMobileAssetAdapter{newClient: newChinaMobileAssetSDKClient}
  25. }
  26. func (a *ChinaMobileAssetAdapter) Name() string {
  27. return "chinamobile_asset"
  28. }
  29. func (a *ChinaMobileAssetAdapter) Supports(operation AssetOperation) bool {
  30. switch operation {
  31. case AssetOperationAssetCreate,
  32. AssetOperationAssetList,
  33. AssetOperationAssetGet,
  34. AssetOperationAssetUpdate,
  35. AssetOperationAssetDelete,
  36. AssetOperationAssetGroupCreate,
  37. AssetOperationAssetGroupList,
  38. AssetOperationAssetGroupGet,
  39. AssetOperationAssetGroupUpdate,
  40. AssetOperationAssetGroupDelete:
  41. return true
  42. default:
  43. return false
  44. }
  45. }
  46. func (a *ChinaMobileAssetAdapter) DoAssetRequest(ctx context.Context, channel *model.Channel, req AssetRequest) (*AssetUpstreamResponse, *AssetError) {
  47. if !a.Supports(req.Action.Operation) {
  48. return nil, newAssetError(AssetErrorOperationNotSupported, fmt.Sprintf("asset operation %s is not supported", req.Action.Operation), http.StatusBadRequest)
  49. }
  50. if err := validateChinaMobileCompatibility(req.Body); err != nil {
  51. return nil, err
  52. }
  53. if err := validateChinaMobileAssetRequest(req); err != nil {
  54. return nil, err
  55. }
  56. credential, err := chinaMobileAssetCredentialFromChannel(channel)
  57. if err != nil {
  58. return nil, newAssetError(AssetErrorInvalidRequest, err.Error(), http.StatusBadRequest)
  59. }
  60. newClient := a.newClient
  61. if newClient == nil {
  62. newClient = newChinaMobileAssetSDKClient
  63. }
  64. result, err := callChinaMobileAssetSDK(ctx, newClient(credential), req)
  65. if err != nil {
  66. if assetErr := classifyChinaMobileAssetSDKError(err); assetErr != nil {
  67. return nil, assetErr
  68. }
  69. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  70. }
  71. normalized, assetErr := normalizeChinaMobileAssetSDKResponse(req.Action, req.Version, result)
  72. if assetErr != nil {
  73. return nil, assetErr
  74. }
  75. return &AssetUpstreamResponse{StatusCode: http.StatusOK, Header: http.Header{}, Body: normalized}, nil
  76. }
  77. func joinChinaMobileAssetURL(baseURL string, path string) (string, error) {
  78. u, err := url.Parse(baseURL)
  79. if err != nil {
  80. return "", err
  81. }
  82. u.Path = strings.TrimRight(u.Path, "/") + path
  83. u.RawQuery = ""
  84. u.Fragment = ""
  85. return u.String(), nil
  86. }
  87. type chinaMobileAssetCredential struct {
  88. AK string `json:"ak"`
  89. SK string `json:"sk"`
  90. PoolID string `json:"pool_id"`
  91. }
  92. func chinaMobileAssetCredentialFromChannel(channel *model.Channel) (chinaMobileAssetCredential, error) {
  93. if channel == nil {
  94. return chinaMobileAssetCredential{}, fmt.Errorf("China Mobile asset channel is required")
  95. }
  96. credential, err := model.GetChannelAssetCredential(channel.Id)
  97. if err != nil {
  98. return chinaMobileAssetCredential{}, err
  99. }
  100. if credential == nil {
  101. legacyCredential, legacyErr := normalizeChinaMobileAssetCredential(chinaMobileAssetCredential{
  102. AK: os.Getenv(chinaMobileAssetAKEnv),
  103. SK: os.Getenv(chinaMobileAssetSKEnv),
  104. PoolID: os.Getenv(chinaMobileAssetPoolIDEnv),
  105. })
  106. if legacyErr != nil {
  107. return chinaMobileAssetCredential{}, fmt.Errorf("该移动云渠道未配置素材凭证")
  108. }
  109. common.SysLog(fmt.Sprintf("using legacy China Mobile asset credentials for channel %d; configure channel asset credentials before removing environment fallback", channel.Id))
  110. return legacyCredential, nil
  111. }
  112. return normalizeChinaMobileAssetCredential(chinaMobileAssetCredential{
  113. AK: credential.AccessKey,
  114. SK: credential.SecretKey,
  115. PoolID: credential.PoolID,
  116. })
  117. }
  118. func normalizeChinaMobileAssetCredential(credential chinaMobileAssetCredential) (chinaMobileAssetCredential, error) {
  119. credential.AK = strings.TrimSpace(credential.AK)
  120. credential.SK = strings.TrimSpace(credential.SK)
  121. credential.PoolID = strings.TrimSpace(credential.PoolID)
  122. if credential.AK == "" || credential.SK == "" {
  123. return chinaMobileAssetCredential{}, fmt.Errorf("China Mobile asset AccessKey and SecretKey are required")
  124. }
  125. if credential.PoolID == "" {
  126. credential.PoolID = defaultChinaMobileAssetPoolID
  127. }
  128. return credential, nil
  129. }
  130. func callChinaMobileAssetSDK(ctx context.Context, client chinaMobileAssetSDKClient, req AssetRequest) (any, error) {
  131. type sdkResult struct {
  132. value any
  133. err error
  134. }
  135. resultCh := make(chan sdkResult, 1)
  136. go func() {
  137. value, err := executeChinaMobileAssetSDK(client, req)
  138. resultCh <- sdkResult{value: value, err: err}
  139. }()
  140. select {
  141. case <-ctx.Done():
  142. return nil, ctx.Err()
  143. case result := <-resultCh:
  144. return result.value, result.err
  145. }
  146. }
  147. func executeChinaMobileAssetSDK(client chinaMobileAssetSDKClient, req AssetRequest) (any, error) {
  148. switch req.Action.Operation {
  149. case AssetOperationAssetCreate:
  150. return client.CreateAsset(&cmmodel.CreateAssetRequest{CreateAssetBody: newChinaMobileCreateAssetBody(req.Body)})
  151. case AssetOperationAssetList:
  152. return client.ListAssets(&cmmodel.ListAssetsRequest{ListAssetsBody: newChinaMobileListAssetsBody(req.Body)})
  153. case AssetOperationAssetGet:
  154. id, err := requiredAssetString(req.Body, "Id")
  155. if err != nil {
  156. return nil, err
  157. }
  158. return client.GetAsset(&cmmodel.GetAssetRequest{GetAssetPath: (&cmmodel.GetAssetPath{}).SetAssetId(id)})
  159. case AssetOperationAssetUpdate:
  160. id, err := requiredAssetString(req.Body, "Id")
  161. if err != nil {
  162. return nil, err
  163. }
  164. return client.UpdateAsset(&cmmodel.UpdateAssetRequest{
  165. UpdateAssetPath: (&cmmodel.UpdateAssetPath{}).SetAssetId(id),
  166. UpdateAssetBody: newChinaMobileUpdateAssetBody(req.Body),
  167. })
  168. case AssetOperationAssetDelete:
  169. id, err := requiredAssetString(req.Body, "Id")
  170. if err != nil {
  171. return nil, err
  172. }
  173. return client.DeleteAsset(&cmmodel.DeleteAssetRequest{DeleteAssetPath: (&cmmodel.DeleteAssetPath{}).SetAssetId(id)})
  174. case AssetOperationAssetGroupCreate:
  175. return client.CreateAssetGroup(&cmmodel.CreateAssetGroupRequest{CreateAssetGroupBody: newChinaMobileCreateAssetGroupBody(req.Body)})
  176. case AssetOperationAssetGroupList:
  177. return client.ListAssetGroups(&cmmodel.ListAssetGroupsRequest{ListAssetGroupsBody: newChinaMobileListAssetGroupsBody(req.Body)})
  178. case AssetOperationAssetGroupGet:
  179. id, err := requiredAssetString(req.Body, "Id")
  180. if err != nil {
  181. return nil, err
  182. }
  183. return client.GetAssetGroup(&cmmodel.GetAssetGroupRequest{GetAssetGroupPath: (&cmmodel.GetAssetGroupPath{}).SetGroupId(id)})
  184. case AssetOperationAssetGroupUpdate:
  185. id, err := requiredAssetString(req.Body, "Id")
  186. if err != nil {
  187. return nil, err
  188. }
  189. return client.UpdateAssetGroup(&cmmodel.UpdateAssetGroupRequest{
  190. UpdateAssetGroupPath: (&cmmodel.UpdateAssetGroupPath{}).SetGroupId(id),
  191. UpdateAssetGroupBody: newChinaMobileUpdateAssetGroupBody(req.Body),
  192. })
  193. case AssetOperationAssetGroupDelete:
  194. id, err := requiredAssetString(req.Body, "Id")
  195. if err != nil {
  196. return nil, err
  197. }
  198. return client.DeleteAssetGroup(&cmmodel.DeleteAssetGroupRequest{DeleteAssetGroupPath: (&cmmodel.DeleteAssetGroupPath{}).SetGroupId(id)})
  199. default:
  200. return nil, fmt.Errorf("unsupported asset operation %s", req.Action.Operation)
  201. }
  202. }
  203. func isChinaMobileAssetLocalValidationError(err error) bool {
  204. return err != nil && strings.Contains(err.Error(), " is required")
  205. }
  206. func classifyChinaMobileAssetSDKError(err error) *AssetError {
  207. if err == nil {
  208. return nil
  209. }
  210. if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
  211. return newAssetError(AssetErrorUpstream, err.Error(), http.StatusGatewayTimeout)
  212. }
  213. if isChinaMobileAssetLocalValidationError(err) {
  214. return newAssetError(AssetErrorInvalidRequest, err.Error(), http.StatusBadRequest)
  215. }
  216. var responseErr *cmerrs.ServerResponseError
  217. if errors.As(err, &responseErr) && responseErr.Code == http.StatusBadRequest {
  218. message := strings.TrimSpace(responseErr.Body)
  219. if message == "" {
  220. message = responseErr.Error()
  221. }
  222. return newAssetError(AssetErrorInvalidRequest, message, http.StatusBadRequest)
  223. }
  224. return nil
  225. }
  226. func validateChinaMobileAssetRequest(req AssetRequest) *AssetError {
  227. invalid := func(message string) *AssetError {
  228. return newAssetError(AssetErrorInvalidRequest, message, http.StatusBadRequest)
  229. }
  230. requireString := func(key string) *AssetError {
  231. if strings.TrimSpace(stringValue(req.Body, key)) == "" {
  232. return invalid(key + " is required")
  233. }
  234. return nil
  235. }
  236. validatePage := func() *AssetError {
  237. pageNumber := intValue(req.Body, "PageNumber", 1)
  238. pageSize := intValue(req.Body, "PageSize", 10)
  239. if pageNumber < 1 {
  240. return invalid("PageNumber must be greater than or equal to 1")
  241. }
  242. if pageSize < 1 || pageSize > 999999 {
  243. return invalid("PageSize must be between 1 and 999999")
  244. }
  245. return nil
  246. }
  247. switch req.Action.Operation {
  248. case AssetOperationAssetCreate:
  249. for _, key := range []string{"GroupId", "Name", "URL", "AssetType"} {
  250. if err := requireString(key); err != nil {
  251. return err
  252. }
  253. }
  254. if len([]rune(stringValue(req.Body, "Name"))) > 64 {
  255. return invalid("Name must not exceed 64 characters")
  256. }
  257. assetURL, err := url.ParseRequestURI(strings.TrimSpace(stringValue(req.Body, "URL")))
  258. if err != nil || (assetURL.Scheme != "http" && assetURL.Scheme != "https") || assetURL.Host == "" {
  259. return invalid("URL must be a valid public HTTP or HTTPS URL")
  260. }
  261. switch stringValue(req.Body, "AssetType") {
  262. case "Image", "Video", "Audio":
  263. default:
  264. return invalid("AssetType must be one of Image, Video, Audio")
  265. }
  266. case AssetOperationAssetList:
  267. if err := validatePage(); err != nil {
  268. return err
  269. }
  270. groupType := strings.TrimSpace(stringValue(mapValue(req.Body, "Filter"), "GroupType"))
  271. if groupType == "" {
  272. return invalid("Filter.GroupType is required")
  273. }
  274. if groupType != "AIGC" && groupType != "LivenessFace" {
  275. return invalid("Filter.GroupType must be AIGC or LivenessFace")
  276. }
  277. case AssetOperationAssetGroupCreate:
  278. if stringValue(req.Body, "GroupType") != "AIGC" {
  279. return invalid("GroupType must be AIGC")
  280. }
  281. if len([]rune(stringValue(req.Body, "Name"))) > 64 {
  282. return invalid("Name must not exceed 64 characters")
  283. }
  284. if len([]rune(stringValue(req.Body, "Description"))) > 300 {
  285. return invalid("Description must not exceed 300 characters")
  286. }
  287. case AssetOperationAssetGroupList:
  288. return validatePage()
  289. case AssetOperationAssetUpdate:
  290. if err := requireString("Id"); err != nil {
  291. return err
  292. }
  293. if len([]rune(stringValue(req.Body, "Name"))) > 64 {
  294. return invalid("Name must not exceed 64 characters")
  295. }
  296. case AssetOperationAssetGroupUpdate:
  297. if err := requireString("Id"); err != nil {
  298. return err
  299. }
  300. if len([]rune(stringValue(req.Body, "Name"))) > 64 {
  301. return invalid("Name must not exceed 64 characters")
  302. }
  303. if len([]rune(stringValue(req.Body, "Description"))) > 300 {
  304. return invalid("Description must not exceed 300 characters")
  305. }
  306. case AssetOperationAssetGet, AssetOperationAssetDelete, AssetOperationAssetGroupGet, AssetOperationAssetGroupDelete:
  307. return requireString("Id")
  308. }
  309. return nil
  310. }
  311. func newChinaMobileCreateAssetBody(body map[string]any) *cmmodel.CreateAssetBody {
  312. assetType := cmmodel.CreateAssetBodyAssetTypeEnum(stringValue(body, "AssetType"))
  313. result := &cmmodel.CreateAssetBody{}
  314. result.SetGroupId(stringValue(body, "GroupId"))
  315. result.SetAssetName(stringValue(body, "Name"))
  316. result.SetAssetUrl(stringValue(body, "URL"))
  317. if strings.TrimSpace(string(assetType)) != "" {
  318. result.SetAssetType(assetType)
  319. }
  320. return result
  321. }
  322. func newChinaMobileListAssetsBody(body map[string]any) *cmmodel.ListAssetsBody {
  323. mapped := mapChinaMobileListAssets(body)
  324. result := &cmmodel.ListAssetsBody{}
  325. result.SetPageNo(int32(intValue(mapped, "pageNo", 1)))
  326. result.SetPageSize(int32(intValue(mapped, "pageSize", 10)))
  327. if value := stringValue(mapped, "groupType"); value != "" {
  328. result.SetGroupType(value)
  329. }
  330. if value := stringValue(mapped, "assetName"); value != "" {
  331. result.SetAssetName(value)
  332. }
  333. if values := stringArrayValue(mapped, "groupIds"); len(values) > 0 {
  334. result.SetGroupIds(values)
  335. }
  336. if values := stringArrayValue(mapped, "statuses"); len(values) > 0 {
  337. result.SetStatuses(values)
  338. }
  339. return result
  340. }
  341. func newChinaMobileUpdateAssetBody(body map[string]any) *cmmodel.UpdateAssetBody {
  342. result := &cmmodel.UpdateAssetBody{}
  343. if value := stringValue(body, "Name"); value != "" {
  344. result.SetAssetName(value)
  345. }
  346. return result
  347. }
  348. func newChinaMobileCreateAssetGroupBody(body map[string]any) *cmmodel.CreateAssetGroupBody {
  349. result := &cmmodel.CreateAssetGroupBody{}
  350. if value := stringValue(body, "GroupType"); value != "" {
  351. result.SetGroupType(value)
  352. }
  353. if value := stringValue(body, "Name"); value != "" {
  354. result.SetGroupName(value)
  355. }
  356. if value := stringValue(body, "Description"); value != "" {
  357. result.SetDescription(value)
  358. }
  359. return result
  360. }
  361. func newChinaMobileListAssetGroupsBody(body map[string]any) *cmmodel.ListAssetGroupsBody {
  362. mapped := mapChinaMobileListAssetGroups(body)
  363. result := &cmmodel.ListAssetGroupsBody{}
  364. result.SetPageNo(int32(intValue(mapped, "pageNo", 1)))
  365. result.SetPageSize(int32(intValue(mapped, "pageSize", 10)))
  366. if value := stringValue(mapped, "groupType"); value != "" {
  367. result.SetGroupType(value)
  368. }
  369. if value := stringValue(mapped, "groupName"); value != "" {
  370. result.SetGroupName(value)
  371. }
  372. if values := stringArrayValue(mapped, "groupIds"); len(values) > 0 {
  373. result.SetGroupIds(values)
  374. }
  375. return result
  376. }
  377. func newChinaMobileUpdateAssetGroupBody(body map[string]any) *cmmodel.UpdateAssetGroupBody {
  378. result := &cmmodel.UpdateAssetGroupBody{}
  379. if value := stringValue(body, "Name"); value != "" {
  380. result.SetGroupName(value)
  381. }
  382. if value := stringValue(body, "Description"); value != "" {
  383. result.SetDescription(value)
  384. }
  385. return result
  386. }
  387. func buildChinaMobileAssetRequest(req AssetRequest) (string, string, map[string]any, error) {
  388. switch req.Action.Operation {
  389. case AssetOperationAssetCreate:
  390. return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset", mapChinaMobileCreateAsset(req.Body), nil
  391. case AssetOperationAssetList:
  392. return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset/query", mapChinaMobileListAssets(req.Body), nil
  393. case AssetOperationAssetGet:
  394. id, err := requiredAssetString(req.Body, "Id")
  395. return http.MethodGet, "/api/openapi-maas/exp/aicc/v2/asset/" + url.PathEscape(id), nil, err
  396. case AssetOperationAssetUpdate:
  397. id, err := requiredAssetString(req.Body, "Id")
  398. return http.MethodPut, "/api/openapi-maas/exp/aicc/v2/asset/" + url.PathEscape(id), mapChinaMobileUpdateAsset(req.Body), err
  399. case AssetOperationAssetDelete:
  400. id, err := requiredAssetString(req.Body, "Id")
  401. return http.MethodDelete, "/api/openapi-maas/exp/aicc/v2/asset/" + url.PathEscape(id), nil, err
  402. case AssetOperationAssetGroupCreate:
  403. return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset-group", mapChinaMobileCreateAssetGroup(req.Body), nil
  404. case AssetOperationAssetGroupList:
  405. return http.MethodPost, "/api/openapi-maas/exp/aicc/v2/asset-group/query", mapChinaMobileListAssetGroups(req.Body), nil
  406. case AssetOperationAssetGroupGet:
  407. id, err := requiredAssetString(req.Body, "Id")
  408. return http.MethodGet, "/api/openapi-maas/exp/aicc/v2/asset-group/" + url.PathEscape(id), nil, err
  409. case AssetOperationAssetGroupUpdate:
  410. id, err := requiredAssetString(req.Body, "Id")
  411. return http.MethodPut, "/api/openapi-maas/exp/aicc/v2/asset-group/" + url.PathEscape(id), mapChinaMobileUpdateAssetGroup(req.Body), err
  412. case AssetOperationAssetGroupDelete:
  413. id, err := requiredAssetString(req.Body, "Id")
  414. return http.MethodDelete, "/api/openapi-maas/exp/aicc/v2/asset-group/" + url.PathEscape(id), nil, err
  415. default:
  416. return "", "", nil, fmt.Errorf("unsupported asset operation %s", req.Action.Operation)
  417. }
  418. }
  419. func mapChinaMobileCreateAsset(body map[string]any) map[string]any {
  420. return compactAssetMap(map[string]any{
  421. "groupId": stringValue(body, "GroupId"),
  422. "assetName": stringValue(body, "Name"),
  423. "assetUrl": stringValue(body, "URL"),
  424. "assetType": stringValue(body, "AssetType"),
  425. })
  426. }
  427. func mapChinaMobileListAssets(body map[string]any) map[string]any {
  428. filter := mapValue(body, "Filter")
  429. mapped := map[string]any{
  430. "pageNo": intValue(body, "PageNumber", 1),
  431. "pageSize": intValue(body, "PageSize", 10),
  432. }
  433. copyStringArray(mapped, "groupIds", filter, "GroupIds")
  434. copyString(mapped, "groupType", filter, "GroupType")
  435. copyString(mapped, "assetName", filter, "Name")
  436. copyStatusArray(mapped, "statuses", filter, "Statuses")
  437. return compactAssetMap(mapped)
  438. }
  439. func mapChinaMobileUpdateAsset(body map[string]any) map[string]any {
  440. return compactAssetMap(map[string]any{"assetName": stringValue(body, "Name")})
  441. }
  442. func mapChinaMobileCreateAssetGroup(body map[string]any) map[string]any {
  443. return compactAssetMap(map[string]any{
  444. "groupType": stringValue(body, "GroupType"),
  445. "groupName": stringValue(body, "Name"),
  446. "description": stringValue(body, "Description"),
  447. })
  448. }
  449. func mapChinaMobileListAssetGroups(body map[string]any) map[string]any {
  450. filter := mapValue(body, "Filter")
  451. mapped := map[string]any{
  452. "pageNo": intValue(body, "PageNumber", 1),
  453. "pageSize": intValue(body, "PageSize", 10),
  454. }
  455. copyString(mapped, "groupType", filter, "GroupType")
  456. copyString(mapped, "groupName", filter, "Name")
  457. copyStringArray(mapped, "groupIds", filter, "GroupIds")
  458. return compactAssetMap(mapped)
  459. }
  460. func mapChinaMobileUpdateAssetGroup(body map[string]any) map[string]any {
  461. return compactAssetMap(map[string]any{
  462. "groupName": stringValue(body, "Name"),
  463. "description": stringValue(body, "Description"),
  464. })
  465. }
  466. func normalizeChinaMobileAssetResponse(spec AssetActionSpec, version string, data []byte) ([]byte, *AssetError) {
  467. var payload struct {
  468. RequestID string `json:"requestId"`
  469. State string `json:"state"`
  470. ErrorCode string `json:"errorCode"`
  471. ErrorMessage string `json:"errorMessage"`
  472. Body any `json:"body"`
  473. }
  474. if err := common.Unmarshal(data, &payload); err != nil {
  475. return nil, newAssetError(AssetErrorUpstream, err.Error(), http.StatusBadGateway)
  476. }
  477. if strings.EqualFold(payload.State, "ERROR") {
  478. message := strings.TrimSpace(payload.ErrorMessage)
  479. if message == "" {
  480. message = payload.ErrorCode
  481. }
  482. return nil, newAssetError(AssetErrorUpstream, message, http.StatusBadGateway)
  483. }
  484. if !strings.EqualFold(payload.State, "OK") {
  485. return nil, newAssetError(AssetErrorUpstream, "China Mobile asset response has invalid state", http.StatusBadGateway)
  486. }
  487. if spec.Delete {
  488. deleted, ok := payload.Body.(bool)
  489. if !ok || !deleted {
  490. return nil, newAssetError(AssetErrorUpstream, "China Mobile asset delete was not confirmed", http.StatusBadGateway)
  491. }
  492. }
  493. var result any
  494. if !spec.Delete {
  495. result = normalizeChinaMobileResult(spec.Operation, payload.Body)
  496. }
  497. body, err := buildAssetSuccessResponseWithRequestID(spec.Action, version, payload.RequestID, result)
  498. if err != nil {
  499. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  500. }
  501. return body, nil
  502. }
  503. func normalizeChinaMobileAssetSDKResponse(spec AssetActionSpec, version string, response any) ([]byte, *AssetError) {
  504. data, err := common.Marshal(response)
  505. if err != nil {
  506. return nil, newAssetError(AssetErrorServer, err.Error(), http.StatusInternalServerError)
  507. }
  508. return normalizeChinaMobileAssetResponse(spec, version, data)
  509. }
  510. func normalizeChinaMobileResult(operation AssetOperation, body any) any {
  511. raw, ok := body.(map[string]any)
  512. if !ok {
  513. return body
  514. }
  515. switch operation {
  516. case AssetOperationAssetList:
  517. return map[string]any{
  518. "Items": normalizeChinaMobileItems(raw["data"], false),
  519. "TotalCount": raw["total"],
  520. }
  521. case AssetOperationAssetGroupList:
  522. return map[string]any{
  523. "Items": normalizeChinaMobileItems(raw["data"], true),
  524. "TotalCount": raw["total"],
  525. }
  526. case AssetOperationAssetGroupCreate, AssetOperationAssetGroupGet, AssetOperationAssetGroupUpdate:
  527. return normalizeChinaMobileAssetGroup(raw)
  528. default:
  529. return normalizeChinaMobileAsset(raw)
  530. }
  531. }
  532. func normalizeChinaMobileItems(value any, group bool) []any {
  533. items, ok := value.([]any)
  534. if !ok {
  535. return nil
  536. }
  537. normalized := make([]any, 0, len(items))
  538. for _, item := range items {
  539. raw, ok := item.(map[string]any)
  540. if !ok {
  541. continue
  542. }
  543. if group {
  544. normalized = append(normalized, normalizeChinaMobileAssetGroup(raw))
  545. } else {
  546. normalized = append(normalized, normalizeChinaMobileAsset(raw))
  547. }
  548. }
  549. return normalized
  550. }
  551. func normalizeChinaMobileAsset(raw map[string]any) map[string]any {
  552. return compactAssetMap(map[string]any{
  553. "Id": raw["assetId"],
  554. "GroupId": raw["groupId"],
  555. "Name": raw["assetName"],
  556. "AssetType": raw["assetType"],
  557. "URL": raw["assetUrl"],
  558. "Status": chinaMobileStatusToOfficial(fmt.Sprint(raw["status"])),
  559. "ErrorMessage": raw["errorMessage"],
  560. "CreatedAt": raw["createdTime"],
  561. "UpdatedAt": raw["updatedTime"],
  562. })
  563. }
  564. func normalizeChinaMobileAssetGroup(raw map[string]any) map[string]any {
  565. return compactAssetMap(map[string]any{
  566. "Id": raw["groupId"],
  567. "GroupId": raw["groupId"],
  568. "GroupType": raw["groupType"],
  569. "Name": raw["groupName"],
  570. "Description": raw["description"],
  571. "CreatedAt": raw["createdTime"],
  572. "UpdatedAt": raw["updatedTime"],
  573. })
  574. }
  575. func validateChinaMobileCompatibility(body map[string]any) *AssetError {
  576. if projectName := strings.TrimSpace(stringValue(body, "ProjectName")); projectName != "" && projectName != "default" {
  577. return newAssetError(AssetErrorOperationNotSupported, "China Mobile asset API does not support non-default ProjectName", http.StatusBadRequest)
  578. }
  579. if sortBy := strings.TrimSpace(stringValue(body, "SortBy")); sortBy != "" && sortBy != "CreateTime" {
  580. return newAssetError(AssetErrorOperationNotSupported, "China Mobile asset API does not support custom SortBy", http.StatusBadRequest)
  581. }
  582. if sortOrder := strings.TrimSpace(stringValue(body, "SortOrder")); sortOrder != "" && !strings.EqualFold(sortOrder, "Desc") {
  583. return newAssetError(AssetErrorOperationNotSupported, "China Mobile asset API does not support custom SortOrder", http.StatusBadRequest)
  584. }
  585. return nil
  586. }
  587. func chinaMobileStatusToOfficial(status string) string {
  588. switch strings.ToUpper(strings.TrimSpace(status)) {
  589. case "PROCESSING":
  590. return "Processing"
  591. case "ACTIVE":
  592. return "Active"
  593. case "FAILED":
  594. return "Failed"
  595. default:
  596. return status
  597. }
  598. }
  599. func officialStatusToChinaMobile(status string) string {
  600. switch strings.ToLower(strings.TrimSpace(status)) {
  601. case "processing":
  602. return "PROCESSING"
  603. case "active":
  604. return "ACTIVE"
  605. case "failed":
  606. return "FAILED"
  607. default:
  608. return status
  609. }
  610. }
  611. func requiredAssetString(body map[string]any, key string) (string, error) {
  612. value := strings.TrimSpace(stringValue(body, key))
  613. if value == "" {
  614. return "", fmt.Errorf("%s is required", key)
  615. }
  616. return value, nil
  617. }
  618. func stringValue(body map[string]any, key string) string {
  619. if body == nil {
  620. return ""
  621. }
  622. value, _ := body[key].(string)
  623. return value
  624. }
  625. func mapValue(body map[string]any, key string) map[string]any {
  626. if body == nil {
  627. return nil
  628. }
  629. value, _ := body[key].(map[string]any)
  630. return value
  631. }
  632. func intValue(body map[string]any, key string, fallback int) int {
  633. if body == nil {
  634. return fallback
  635. }
  636. switch value := body[key].(type) {
  637. case int:
  638. return value
  639. case int32:
  640. return int(value)
  641. case int64:
  642. return int(value)
  643. case float64:
  644. return int(value)
  645. case float32:
  646. return int(value)
  647. default:
  648. return fallback
  649. }
  650. }
  651. func copyString(dst map[string]any, dstKey string, src map[string]any, srcKey string) {
  652. value := stringValue(src, srcKey)
  653. if strings.TrimSpace(value) != "" {
  654. dst[dstKey] = value
  655. }
  656. }
  657. func copyStringArray(dst map[string]any, dstKey string, src map[string]any, srcKey string) {
  658. values := stringArrayValue(src, srcKey)
  659. if len(values) > 0 {
  660. dst[dstKey] = values
  661. }
  662. }
  663. func copyStatusArray(dst map[string]any, dstKey string, src map[string]any, srcKey string) {
  664. values := stringArrayValue(src, srcKey)
  665. if len(values) == 0 {
  666. return
  667. }
  668. mapped := make([]string, 0, len(values))
  669. for _, value := range values {
  670. mapped = append(mapped, officialStatusToChinaMobile(value))
  671. }
  672. dst[dstKey] = mapped
  673. }
  674. func stringArrayValue(body map[string]any, key string) []string {
  675. if body == nil {
  676. return nil
  677. }
  678. switch value := body[key].(type) {
  679. case []string:
  680. return value
  681. case []any:
  682. values := make([]string, 0, len(value))
  683. for _, item := range value {
  684. if s, ok := item.(string); ok && strings.TrimSpace(s) != "" {
  685. values = append(values, s)
  686. }
  687. }
  688. return values
  689. default:
  690. return nil
  691. }
  692. }
  693. func compactAssetMap(input map[string]any) map[string]any {
  694. output := make(map[string]any, len(input))
  695. for key, value := range input {
  696. switch v := value.(type) {
  697. case string:
  698. if strings.TrimSpace(v) != "" {
  699. output[key] = v
  700. }
  701. case nil:
  702. continue
  703. default:
  704. output[key] = value
  705. }
  706. }
  707. return output
  708. }