Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

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