This is page 6 of 7. Use http://codebase.md/higress-group/himarket?page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ ├── api-style.mdc
│ └── project-architecture.mdc
├── .gitignore
├── build.sh
├── deploy
│ ├── docker
│ │ ├── docker-compose.yml
│ │ └── Docker部署说明.md
│ └── helm
│ ├── Chart.yaml
│ ├── Helm部署说明.md
│ ├── templates
│ │ ├── _helpers.tpl
│ │ ├── himarket-admin-cm.yaml
│ │ ├── himarket-admin-deployment.yaml
│ │ ├── himarket-admin-service.yaml
│ │ ├── himarket-frontend-cm.yaml
│ │ ├── himarket-frontend-deployment.yaml
│ │ ├── himarket-frontend-service.yaml
│ │ ├── himarket-server-cm.yaml
│ │ ├── himarket-server-deployment.yaml
│ │ ├── himarket-server-service.yaml
│ │ ├── mysql.yaml
│ │ └── serviceaccount.yaml
│ └── values.yaml
├── LICENSE
├── NOTICE
├── pom.xml
├── portal-bootstrap
│ ├── Dockerfile
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── alibaba
│ │ │ └── apiopenplatform
│ │ │ ├── config
│ │ │ │ ├── AsyncConfig.java
│ │ │ │ ├── FilterConfig.java
│ │ │ │ ├── PageConfig.java
│ │ │ │ ├── RestTemplateConfig.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── SwaggerConfig.java
│ │ │ ├── filter
│ │ │ │ └── PortalResolvingFilter.java
│ │ │ └── PortalApplication.java
│ │ └── resources
│ │ └── application.yaml
│ └── test
│ └── java
│ └── com
│ └── alibaba
│ └── apiopenplatform
│ └── integration
│ └── AdministratorAuthIntegrationTest.java
├── portal-dal
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── apiopenplatform
│ ├── converter
│ │ ├── AdpAIGatewayConfigConverter.java
│ │ ├── APIGConfigConverter.java
│ │ ├── APIGRefConfigConverter.java
│ │ ├── ApiKeyConfigConverter.java
│ │ ├── ApsaraGatewayConfigConverter.java
│ │ ├── ConsumerAuthConfigConverter.java
│ │ ├── GatewayConfigConverter.java
│ │ ├── HigressConfigConverter.java
│ │ ├── HigressRefConfigConverter.java
│ │ ├── HmacConfigConverter.java
│ │ ├── JsonConverter.java
│ │ ├── JwtConfigConverter.java
│ │ ├── NacosRefConfigConverter.java
│ │ ├── PortalSettingConfigConverter.java
│ │ ├── PortalUiConfigConverter.java
│ │ └── ProductIconConverter.java
│ ├── entity
│ │ ├── Administrator.java
│ │ ├── BaseEntity.java
│ │ ├── Consumer.java
│ │ ├── ConsumerCredential.java
│ │ ├── ConsumerRef.java
│ │ ├── Developer.java
│ │ ├── DeveloperExternalIdentity.java
│ │ ├── Gateway.java
│ │ ├── NacosInstance.java
│ │ ├── Portal.java
│ │ ├── PortalDomain.java
│ │ ├── Product.java
│ │ ├── ProductPublication.java
│ │ ├── ProductRef.java
│ │ └── ProductSubscription.java
│ ├── repository
│ │ ├── AdministratorRepository.java
│ │ ├── BaseRepository.java
│ │ ├── ConsumerCredentialRepository.java
│ │ ├── ConsumerRefRepository.java
│ │ ├── ConsumerRepository.java
│ │ ├── DeveloperExternalIdentityRepository.java
│ │ ├── DeveloperRepository.java
│ │ ├── GatewayRepository.java
│ │ ├── NacosInstanceRepository.java
│ │ ├── PortalDomainRepository.java
│ │ ├── PortalRepository.java
│ │ ├── ProductPublicationRepository.java
│ │ ├── ProductRefRepository.java
│ │ ├── ProductRepository.java
│ │ └── SubscriptionRepository.java
│ └── support
│ ├── common
│ │ ├── Encrypted.java
│ │ ├── Encryptor.java
│ │ └── User.java
│ ├── consumer
│ │ ├── AdpAIAuthConfig.java
│ │ ├── APIGAuthConfig.java
│ │ ├── ApiKeyConfig.java
│ │ ├── ConsumerAuthConfig.java
│ │ ├── HigressAuthConfig.java
│ │ ├── HmacConfig.java
│ │ └── JwtConfig.java
│ ├── enums
│ │ ├── APIGAPIType.java
│ │ ├── ConsumerAuthType.java
│ │ ├── ConsumerStatus.java
│ │ ├── CredentialMode.java
│ │ ├── DeveloperAuthType.java
│ │ ├── DeveloperStatus.java
│ │ ├── DomainType.java
│ │ ├── GatewayType.java
│ │ ├── GrantType.java
│ │ ├── HigressAPIType.java
│ │ ├── JwtAlgorithm.java
│ │ ├── ProductIconType.java
│ │ ├── ProductStatus.java
│ │ ├── ProductType.java
│ │ ├── ProtocolType.java
│ │ ├── PublicKeyFormat.java
│ │ ├── SourceType.java
│ │ ├── SubscriptionStatus.java
│ │ └── UserType.java
│ ├── gateway
│ │ ├── AdpAIGatewayConfig.java
│ │ ├── APIGConfig.java
│ │ ├── ApsaraGatewayConfig.java
│ │ ├── GatewayConfig.java
│ │ └── HigressConfig.java
│ ├── portal
│ │ ├── AuthCodeConfig.java
│ │ ├── IdentityMapping.java
│ │ ├── JwtBearerConfig.java
│ │ ├── OAuth2Config.java
│ │ ├── OidcConfig.java
│ │ ├── PortalSettingConfig.java
│ │ ├── PortalUiConfig.java
│ │ └── PublicKeyConfig.java
│ └── product
│ ├── APIGRefConfig.java
│ ├── HigressRefConfig.java
│ ├── NacosRefConfig.java
│ └── ProductIcon.java
├── portal-server
│ ├── lib
│ │ └── csb220230206-1.5.3.jar
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── apiopenplatform
│ ├── controller
│ │ ├── AdministratorController.java
│ │ ├── ConsumerController.java
│ │ ├── DeveloperController.java
│ │ ├── GatewayController.java
│ │ ├── NacosController.java
│ │ ├── OAuth2Controller.java
│ │ ├── OidcController.java
│ │ ├── PortalController.java
│ │ └── ProductController.java
│ ├── core
│ │ ├── advice
│ │ │ ├── ExceptionAdvice.java
│ │ │ └── ResponseAdvice.java
│ │ ├── annotation
│ │ │ ├── AdminAuth.java
│ │ │ ├── AdminOrDeveloperAuth.java
│ │ │ └── DeveloperAuth.java
│ │ ├── constant
│ │ │ ├── CommonConstants.java
│ │ │ ├── IdpConstants.java
│ │ │ ├── JwtConstants.java
│ │ │ └── Resources.java
│ │ ├── event
│ │ │ ├── DeveloperDeletingEvent.java
│ │ │ ├── PortalDeletingEvent.java
│ │ │ └── ProductDeletingEvent.java
│ │ ├── exception
│ │ │ ├── BusinessException.java
│ │ │ └── ErrorCode.java
│ │ ├── response
│ │ │ └── Response.java
│ │ ├── security
│ │ │ ├── ContextHolder.java
│ │ │ ├── DeveloperAuthenticationProvider.java
│ │ │ └── JwtAuthenticationFilter.java
│ │ └── utils
│ │ ├── IdGenerator.java
│ │ ├── PasswordHasher.java
│ │ └── TokenUtil.java
│ ├── dto
│ │ ├── converter
│ │ │ ├── InputConverter.java
│ │ │ ├── NacosToGatewayToolsConverter.java
│ │ │ └── OutputConverter.java
│ │ ├── params
│ │ │ ├── admin
│ │ │ │ ├── AdminCreateParam.java
│ │ │ │ ├── AdminLoginParam.java
│ │ │ │ └── ResetPasswordParam.java
│ │ │ ├── consumer
│ │ │ │ ├── CreateConsumerParam.java
│ │ │ │ ├── CreateCredentialParam.java
│ │ │ │ ├── CreateSubscriptionParam.java
│ │ │ │ ├── QueryConsumerParam.java
│ │ │ │ ├── QuerySubscriptionParam.java
│ │ │ │ └── UpdateCredentialParam.java
│ │ │ ├── developer
│ │ │ │ ├── CreateDeveloperParam.java
│ │ │ │ ├── CreateExternalDeveloperParam.java
│ │ │ │ ├── DeveloperLoginParam.java
│ │ │ │ ├── QueryDeveloperParam.java
│ │ │ │ ├── UnbindExternalIdentityParam.java
│ │ │ │ ├── UpdateDeveloperParam.java
│ │ │ │ └── UpdateDeveloperStatusParam.java
│ │ │ ├── gateway
│ │ │ │ ├── ImportGatewayParam.java
│ │ │ │ ├── QueryAdpAIGatewayParam.java
│ │ │ │ ├── QueryAPIGParam.java
│ │ │ │ ├── QueryApsaraGatewayParam.java
│ │ │ │ └── QueryGatewayParam.java
│ │ │ ├── nacos
│ │ │ │ ├── CreateNacosParam.java
│ │ │ │ ├── QueryNacosNamespaceParam.java
│ │ │ │ ├── QueryNacosParam.java
│ │ │ │ └── UpdateNacosParam.java
│ │ │ ├── portal
│ │ │ │ ├── BindDomainParam.java
│ │ │ │ ├── CreatePortalParam.java
│ │ │ │ └── UpdatePortalParam.java
│ │ │ └── product
│ │ │ ├── CreateProductParam.java
│ │ │ ├── CreateProductRefParam.java
│ │ │ ├── PublishProductParam.java
│ │ │ ├── QueryProductParam.java
│ │ │ ├── QueryProductSubscriptionParam.java
│ │ │ ├── UnPublishProductParam.java
│ │ │ └── UpdateProductParam.java
│ │ └── result
│ │ ├── AdminResult.java
│ │ ├── AdpGatewayInstanceResult.java
│ │ ├── AdpMcpServerListResult.java
│ │ ├── AdpMCPServerResult.java
│ │ ├── APIConfigResult.java
│ │ ├── APIGMCPServerResult.java
│ │ ├── APIResult.java
│ │ ├── AuthResult.java
│ │ ├── ConsumerCredentialResult.java
│ │ ├── ConsumerResult.java
│ │ ├── DeveloperResult.java
│ │ ├── GatewayMCPServerResult.java
│ │ ├── GatewayResult.java
│ │ ├── HigressMCPServerResult.java
│ │ ├── IdpResult.java
│ │ ├── IdpState.java
│ │ ├── IdpTokenResult.java
│ │ ├── MCPConfigResult.java
│ │ ├── MCPServerResult.java
│ │ ├── MseNacosResult.java
│ │ ├── NacosMCPServerResult.java
│ │ ├── NacosNamespaceResult.java
│ │ ├── NacosResult.java
│ │ ├── PageResult.java
│ │ ├── PortalResult.java
│ │ ├── ProductPublicationResult.java
│ │ ├── ProductRefResult.java
│ │ ├── ProductResult.java
│ │ └── SubscriptionResult.java
│ └── service
│ ├── AdministratorService.java
│ ├── AdpAIGatewayService.java
│ ├── ApsaraGatewayService.java
│ ├── ConsumerService.java
│ ├── DeveloperService.java
│ ├── gateway
│ │ ├── AdpAIGatewayOperator.java
│ │ ├── AIGatewayOperator.java
│ │ ├── APIGOperator.java
│ │ ├── ApsaraGatewayOperator.java
│ │ ├── client
│ │ │ ├── AdpAIGatewayClient.java
│ │ │ ├── APIGClient.java
│ │ │ ├── ApsaraGatewayClient.java
│ │ │ ├── ApsaraStackGatewayClient.java
│ │ │ ├── GatewayClient.java
│ │ │ ├── HigressClient.java
│ │ │ ├── PopGatewayClient.java
│ │ │ └── SLSClient.java
│ │ ├── factory
│ │ │ └── HTTPClientFactory.java
│ │ ├── GatewayOperator.java
│ │ └── HigressOperator.java
│ ├── GatewayService.java
│ ├── IdpService.java
│ ├── impl
│ │ ├── AdministratorServiceImpl.java
│ │ ├── ConsumerServiceImpl.java
│ │ ├── DeveloperServiceImpl.java
│ │ ├── GatewayServiceImpl.java
│ │ ├── IdpServiceImpl.java
│ │ ├── NacosServiceImpl.java
│ │ ├── OAuth2ServiceImpl.java
│ │ ├── OidcServiceImpl.java
│ │ ├── PortalServiceImpl.java
│ │ └── ProductServiceImpl.java
│ ├── NacosService.java
│ ├── OAuth2Service.java
│ ├── OidcService.java
│ ├── PortalService.java
│ └── ProductService.java
├── portal-web
│ ├── api-portal-admin
│ │ ├── .env
│ │ ├── .gitignore
│ │ ├── bin
│ │ │ ├── replace_var.py
│ │ │ └── start.sh
│ │ ├── Dockerfile
│ │ ├── eslint.config.js
│ │ ├── index.html
│ │ ├── nginx.conf
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── proxy.conf
│ │ ├── public
│ │ │ ├── logo.png
│ │ │ └── vite.svg
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── aliyunThemeToken.ts
│ │ │ ├── App.css
│ │ │ ├── App.tsx
│ │ │ ├── assets
│ │ │ │ └── react.svg
│ │ │ ├── components
│ │ │ │ ├── api-product
│ │ │ │ │ ├── ApiProductApiDocs.tsx
│ │ │ │ │ ├── ApiProductDashboard.tsx
│ │ │ │ │ ├── ApiProductFormModal.tsx
│ │ │ │ │ ├── ApiProductLinkApi.tsx
│ │ │ │ │ ├── ApiProductOverview.tsx
│ │ │ │ │ ├── ApiProductPolicy.tsx
│ │ │ │ │ ├── ApiProductPortal.tsx
│ │ │ │ │ ├── ApiProductUsageGuide.tsx
│ │ │ │ │ ├── SwaggerUIWrapper.css
│ │ │ │ │ └── SwaggerUIWrapper.tsx
│ │ │ │ ├── common
│ │ │ │ │ ├── AdvancedSearch.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── console
│ │ │ │ │ ├── GatewayTypeSelector.tsx
│ │ │ │ │ ├── ImportGatewayModal.tsx
│ │ │ │ │ ├── ImportHigressModal.tsx
│ │ │ │ │ ├── ImportMseNacosModal.tsx
│ │ │ │ │ └── NacosTypeSelector.tsx
│ │ │ │ ├── icons
│ │ │ │ │ └── McpServerIcon.tsx
│ │ │ │ ├── Layout.tsx
│ │ │ │ ├── LayoutWrapper.tsx
│ │ │ │ ├── portal
│ │ │ │ │ ├── PortalConsumers.tsx
│ │ │ │ │ ├── PortalDashboard.tsx
│ │ │ │ │ ├── PortalDevelopers.tsx
│ │ │ │ │ ├── PortalDomain.tsx
│ │ │ │ │ ├── PortalFormModal.tsx
│ │ │ │ │ ├── PortalOverview.tsx
│ │ │ │ │ ├── PortalPublishedApis.tsx
│ │ │ │ │ ├── PortalSecurity.tsx
│ │ │ │ │ ├── PortalSettings.tsx
│ │ │ │ │ ├── PublicKeyManager.tsx
│ │ │ │ │ └── ThirdPartyAuthManager.tsx
│ │ │ │ └── subscription
│ │ │ │ └── SubscriptionListModal.tsx
│ │ │ ├── contexts
│ │ │ │ └── LoadingContext.tsx
│ │ │ ├── index.css
│ │ │ ├── lib
│ │ │ │ ├── api.ts
│ │ │ │ ├── constant.ts
│ │ │ │ └── utils.ts
│ │ │ ├── main.tsx
│ │ │ ├── pages
│ │ │ │ ├── ApiProductDetail.tsx
│ │ │ │ ├── ApiProducts.tsx
│ │ │ │ ├── Dashboard.tsx
│ │ │ │ ├── GatewayConsoles.tsx
│ │ │ │ ├── Login.tsx
│ │ │ │ ├── NacosConsoles.tsx
│ │ │ │ ├── PortalDetail.tsx
│ │ │ │ ├── Portals.tsx
│ │ │ │ └── Register.tsx
│ │ │ ├── routes
│ │ │ │ └── index.tsx
│ │ │ ├── types
│ │ │ │ ├── api-product.ts
│ │ │ │ ├── consumer.ts
│ │ │ │ ├── gateway.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── portal.ts
│ │ │ │ ├── shims-js-yaml.d.ts
│ │ │ │ └── subscription.ts
│ │ │ └── vite-env.d.ts
│ │ ├── tailwind.config.js
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.ts
│ └── api-portal-frontend
│ ├── .env
│ ├── .gitignore
│ ├── .husky
│ │ └── pre-commit
│ ├── bin
│ │ ├── replace_var.py
│ │ └── start.sh
│ ├── Dockerfile
│ ├── eslint.config.js
│ ├── index.html
│ ├── nginx.conf
│ ├── package.json
│ ├── postcss.config.js
│ ├── proxy.conf
│ ├── public
│ │ ├── favicon.ico
│ │ ├── logo.png
│ │ ├── logo.svg
│ │ ├── MCP.png
│ │ ├── MCP.svg
│ │ └── vite.svg
│ ├── README.md
│ ├── src
│ │ ├── aliyunThemeToken.ts
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── assets
│ │ │ ├── aliyun.png
│ │ │ ├── github.png
│ │ │ ├── google.png
│ │ │ └── react.svg
│ │ ├── components
│ │ │ ├── consumer
│ │ │ │ ├── ConsumerBasicInfo.tsx
│ │ │ │ ├── CredentialManager.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── SubscriptionManager.tsx
│ │ │ ├── Layout.tsx
│ │ │ ├── Navigation.tsx
│ │ │ ├── ProductHeader.tsx
│ │ │ ├── SwaggerUIWrapper.css
│ │ │ ├── SwaggerUIWrapper.tsx
│ │ │ └── UserInfo.tsx
│ │ ├── index.css
│ │ ├── lib
│ │ │ ├── api.ts
│ │ │ ├── statusUtils.ts
│ │ │ └── utils.ts
│ │ ├── main.tsx
│ │ ├── pages
│ │ │ ├── ApiDetail.tsx
│ │ │ ├── Apis.tsx
│ │ │ ├── Callback.tsx
│ │ │ ├── ConsumerDetail.tsx
│ │ │ ├── Consumers.tsx
│ │ │ ├── GettingStarted.tsx
│ │ │ ├── Home.tsx
│ │ │ ├── Login.tsx
│ │ │ ├── Mcp.tsx
│ │ │ ├── McpDetail.tsx
│ │ │ ├── OidcCallback.tsx
│ │ │ ├── Profile.tsx
│ │ │ ├── Register.tsx
│ │ │ └── Test.css
│ │ ├── router.tsx
│ │ ├── types
│ │ │ ├── consumer.ts
│ │ │ └── index.ts
│ │ └── vite-env.d.ts
│ ├── tailwind.config.js
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
└── README.md
```
# Files
--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/AIGatewayOperator.java:
--------------------------------------------------------------------------------
```java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.alibaba.apiopenplatform.service.gateway;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.dto.result.GatewayMCPServerResult;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.entity.Gateway;
import com.alibaba.apiopenplatform.service.gateway.client.APIGClient;
import com.alibaba.apiopenplatform.service.gateway.client.PopGatewayClient;
import com.alibaba.apiopenplatform.service.gateway.client.SLSClient;
import com.alibaba.apiopenplatform.support.consumer.APIGAuthConfig;
import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
import com.alibaba.apiopenplatform.support.enums.APIGAPIType;
import com.alibaba.apiopenplatform.support.enums.GatewayType;
import com.alibaba.apiopenplatform.support.product.APIGRefConfig;
import com.aliyuncs.http.MethodType;
import com.aliyun.sdk.gateway.pop.exception.PopClientException;
import com.aliyun.sdk.service.apig20240327.models.*;
import com.aliyun.sdk.service.sls20201230.models.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@Service
@Slf4j
public class AIGatewayOperator extends APIGOperator {
@Override
public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
PopGatewayClient client = new PopGatewayClient(gateway.getApigConfig());
Map<String , String> queryParams = MapUtil.<String, String>builder()
.put("gatewayId", gateway.getGatewayId())
.put("pageNumber", String.valueOf(page))
.put("pageSize", String.valueOf(size))
.build();
return client.execute("/v1/mcp-servers", MethodType.GET, queryParams, data -> {
List<APIGMCPServerResult> mcpServers = Optional.ofNullable(data.getJSONArray("items"))
.map(items -> items.stream()
.map(JSONObject.class::cast)
.map(json -> {
APIGMCPServerResult result = new APIGMCPServerResult();
result.setMcpServerName(json.getStr("name"));
result.setMcpServerId(json.getStr("mcpServerId"));
result.setMcpRouteId(json.getStr("routeId"));
result.setApiId(json.getStr("apiId"));
return result;
})
.collect(Collectors.toList()))
.orElse(new ArrayList<>());
return PageResult.of(mcpServers, page, size, data.getInt("totalSize"));
});
}
public PageResult<? extends GatewayMCPServerResult> fetchMcpServers_V1(Gateway gateway, int page, int size) {
PageResult<APIResult> apiPage = fetchAPIs(gateway, APIGAPIType.MCP, 0, 1);
if (apiPage.getTotalElements() == 0) {
return PageResult.empty(page, size);
}
// MCP Server定义在一个API下
String apiId = apiPage.getContent().get(0).getApiId();
try {
PageResult<HttpRoute> routesPage = fetchHttpRoutes(gateway, apiId, page, size);
if (routesPage.getTotalElements() == 0) {
return PageResult.empty(page, size);
}
return PageResult.<APIGMCPServerResult>builder().build()
.mapFrom(routesPage, route -> {
APIGMCPServerResult r = new APIGMCPServerResult().convertFrom(route);
r.setApiId(apiId);
return r;
});
} catch (Exception e) {
log.error("Error fetching MCP servers", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching MCP servers,Cause:" + e.getMessage());
}
}
@Override
public String fetchMcpConfig(Gateway gateway, Object conf) {
APIGRefConfig config = (APIGRefConfig) conf;
PopGatewayClient client = new PopGatewayClient(gateway.getApigConfig());
String mcpServerId = config.getMcpServerId();
MCPConfigResult mcpConfig = new MCPConfigResult();
return client.execute("/v1/mcp-servers/" + mcpServerId, MethodType.GET, null, data -> {
mcpConfig.setMcpServerName(data.getStr("name"));
// mcpServer config
MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig();
String path = data.getStr("mcpServerPath");
String exposedUriPath = data.getStr("exposedUriPath");
if (StrUtil.isNotBlank(exposedUriPath)) {
path += exposedUriPath;
}
serverConfig.setPath(path);
JSONArray domains = data.getJSONArray("domainInfos");
if (domains != null && !domains.isEmpty()) {
serverConfig.setDomains(domains.stream()
.map(JSONObject.class::cast)
.map(json -> MCPConfigResult.Domain.builder()
.domain(json.getStr("name"))
.protocol(Optional.ofNullable(json.getStr("protocol"))
.map(String::toLowerCase)
.orElse(null))
.build())
.collect(Collectors.toList()));
}
mcpConfig.setMcpServerConfig(serverConfig);
// meta
MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
meta.setSource(GatewayType.APIG_AI.name());
meta.setProtocol(data.getStr("protocol"));
meta.setCreateFromType(data.getStr("createFromType"));
mcpConfig.setMeta(meta);
// tools
String tools = data.getStr("mcpServerConfig");
if (StrUtil.isNotBlank(tools)) {
mcpConfig.setTools(Base64.decodeStr(tools));
}
return JSONUtil.toJsonStr(mcpConfig);
});
}
public String fetchMcpConfig_V1(Gateway gateway, Object conf) {
APIGRefConfig config = (APIGRefConfig) conf;
HttpRoute httpRoute = fetchHTTPRoute(gateway, config.getApiId(), config.getMcpRouteId());
MCPConfigResult m = new MCPConfigResult();
m.setMcpServerName(httpRoute.getName());
// mcpServer config
MCPConfigResult.MCPServerConfig c = new MCPConfigResult.MCPServerConfig();
if (httpRoute.getMatch() != null) {
c.setPath(httpRoute.getMatch().getPath().getValue());
}
if (httpRoute.getDomainInfos() != null) {
c.setDomains(httpRoute.getDomainInfos().stream()
.map(domainInfo -> MCPConfigResult.Domain.builder()
.domain(domainInfo.getName())
.protocol(Optional.ofNullable(domainInfo.getProtocol())
.map(String::toLowerCase)
.orElse(null))
.build())
.collect(Collectors.toList()));
}
m.setMcpServerConfig(c);
// meta
MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
meta.setSource(GatewayType.APIG_AI.name());
// tools
HttpRoute.McpServerInfo mcpServerInfo = httpRoute.getMcpServerInfo();
boolean fetchTool = true;
if (mcpServerInfo.getMcpRouteConfig() != null) {
String protocol = mcpServerInfo.getMcpRouteConfig().getProtocol();
meta.setCreateFromType(protocol);
// HTTP转MCP需从插件获取tools配置
fetchTool = StrUtil.equalsIgnoreCase(protocol, "HTTP");
}
if (fetchTool) {
String toolSpec = fetchMcpTools(gateway, config.getMcpRouteId());
if (StrUtil.isNotBlank(toolSpec)) {
m.setTools(toolSpec);
// 默认为HTTP转MCP
if (StrUtil.isBlank(meta.getCreateFromType())) {
meta.setCreateFromType("HTTP");
}
}
}
m.setMeta(meta);
return JSONUtil.toJsonStr(m);
}
@Override
public GatewayType getGatewayType() {
return GatewayType.APIG_AI;
}
@Override
public String getDashboard(Gateway gateway, String type) {
SLSClient ticketClient = new SLSClient(gateway.getApigConfig(), true);
String ticket = null;
try {
CreateTicketResponse response = ticketClient.execute(c -> {
CreateTicketRequest request = CreateTicketRequest.builder().build();
try {
return c.createTicket(request).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
});
ticket = response.getBody().getTicket();
} catch (Exception e) {
log.error("Error fetching API", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching createTicket API,Cause:" + e.getMessage());
}
SLSClient client = new SLSClient(gateway.getApigConfig(), false);
String projectName = null;
try {
ListProjectResponse response = client.execute(c -> {
ListProjectRequest request = ListProjectRequest.builder().projectName("product").build();
try {
return c.listProject(request).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
});
projectName = response.getBody().getProjects().get(0).getProjectName();
} catch (Exception e) {
log.error("Error fetching Project", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Project,Cause:" + e.getMessage());
}
String region = gateway.getApigConfig().getRegion();
String gatewayId = gateway.getGatewayId();
String dashboardId = "";
String gatewayFilter = "";
if (type.equals("Portal")) {
dashboardId = "dashboard-1758009692051-393998";
gatewayFilter = "";
} else if (type.equals("MCP")) {
dashboardId = "dashboard-1757483808537-433375";
gatewayFilter = "filters=cluster_id%%253A%%2520" + gatewayId;
} else if (type.equals("API")) {
dashboardId = "dashboard-1756276497392-966932";
gatewayFilter = "filters=cluster_id%%253A%%2520" + gatewayId;
;
}
String dashboardUrl = String.format("https://sls.console.aliyun.com/lognext/project/%s/dashboard/%s?%s&slsRegion=%s&sls_ticket=%s&isShare=true&hideTopbar=true&hideSidebar=true&ignoreTabLocalStorage=true", projectName, dashboardId, gatewayFilter, region, ticket);
log.info("Dashboard URL: {}", dashboardUrl);
return dashboardUrl;
}
public String fetchMcpTools(Gateway gateway, String routeId) {
APIGClient client = getClient(gateway);
try {
CompletableFuture<ListPluginAttachmentsResponse> f = client.execute(c -> {
ListPluginAttachmentsRequest request = ListPluginAttachmentsRequest.builder()
.gatewayId(gateway.getGatewayId())
.attachResourceId(routeId)
.attachResourceType("GatewayRoute")
.pageNumber(1)
.pageSize(100)
.build();
return c.listPluginAttachments(request);
});
ListPluginAttachmentsResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
for (ListPluginAttachmentsResponseBody.Items item : response.getBody().getData().getItems()) {
PluginClassInfo classInfo = item.getPluginClassInfo();
if (!StrUtil.equalsIgnoreCase(classInfo.getName(), "mcp-server")) {
continue;
}
String pluginConfig = item.getPluginConfig();
if (StrUtil.isNotBlank(pluginConfig)) {
return Base64.decodeStr(pluginConfig);
}
}
} catch (Exception e) {
log.error("Error fetching Plugin Attachment", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Plugin Attachment,Cause:" + e.getMessage());
}
return null;
}
@Override
public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) {
APIGClient client = getClient(gateway);
APIGRefConfig config = (APIGRefConfig) refConfig;
// MCP Server 授权
String mcpRouteId = config.getMcpRouteId();
try {
// 确认Gateway的EnvId
String envId = fetchGatewayEnv(gateway);
CreateConsumerAuthorizationRulesRequest.AuthorizationRules rule = CreateConsumerAuthorizationRulesRequest.AuthorizationRules.builder()
.consumerId(consumerId)
.expireMode("LongTerm")
.resourceType("MCP")
.resourceIdentifier(CreateConsumerAuthorizationRulesRequest.ResourceIdentifier.builder()
.resourceId(mcpRouteId)
.environmentId(envId).build())
.build();
CompletableFuture<CreateConsumerAuthorizationRulesResponse> f = client.execute(c -> {
CreateConsumerAuthorizationRulesRequest request = CreateConsumerAuthorizationRulesRequest.builder()
.authorizationRules(Collections.singletonList(rule))
.build();
return c.createConsumerAuthorizationRules(request);
}
);
CreateConsumerAuthorizationRulesResponse response = f.join();
if (200 != response.getStatusCode()) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
.authorizationRuleIds(response.getBody().getData().getConsumerAuthorizationRuleIds())
.build();
return ConsumerAuthConfig.builder()
.apigAuthConfig(apigAuthConfig)
.build();
} catch (Exception e) {
Throwable cause = e.getCause();
if (cause instanceof PopClientException
&& "Conflict.ConsumerAuthorizationForbidden".equals(((PopClientException) cause).getErrCode())) {
return getConsumerAuthorization(gateway, consumerId, mcpRouteId);
}
log.error("Error authorizing consumer {} to mcp server {} in AI gateway {}", consumerId, mcpRouteId, gateway.getGatewayId(), e);
throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to mcp server in AI gateway: " + e.getMessage());
}
}
public ConsumerAuthConfig getConsumerAuthorization(Gateway gateway, String consumerId, String resourceId) {
APIGClient client = getClient(gateway);
CompletableFuture<QueryConsumerAuthorizationRulesResponse> f = client.execute(c -> {
QueryConsumerAuthorizationRulesRequest request = QueryConsumerAuthorizationRulesRequest.builder()
.consumerId(consumerId)
.resourceId(resourceId)
.resourceType("MCP")
.build();
return c.queryConsumerAuthorizationRules(request);
});
QueryConsumerAuthorizationRulesResponse response = f.join();
if (200 != response.getStatusCode()) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
QueryConsumerAuthorizationRulesResponseBody.Items items = response.getBody().getData().getItems().get(0);
APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
.authorizationRuleIds(Collections.singletonList(items.getConsumerAuthorizationRuleId()))
.build();
return ConsumerAuthConfig.builder()
.apigAuthConfig(apigAuthConfig)
.build();
}
}
```
--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/components/ProductHeader.tsx:
--------------------------------------------------------------------------------
```typescript
import React, { useState, useEffect } from "react";
import { Typography, Button, Modal, Select, message, Popconfirm, Input, Pagination, Spin } from "antd";
import { ApiOutlined, CheckCircleFilled, ClockCircleFilled, ExclamationCircleFilled, PlusOutlined } from "@ant-design/icons";
import { useParams } from "react-router-dom";
import { getConsumers, subscribeProduct, getProductSubscriptionStatus, unsubscribeProduct, getProductSubscriptions } from "../lib/api";
import type { Consumer } from "../types/consumer";
import type { McpConfig, ProductIcon } from "../types";
const { Title, Paragraph } = Typography;
const { Search } = Input;
interface ProductHeaderProps {
name: string;
description: string;
icon?: ProductIcon | null;
defaultIcon?: string;
mcpConfig?: McpConfig | null;
updatedAt?: string;
productType?: 'REST_API' | 'MCP_SERVER';
}
// 处理产品图标的函数
const getIconUrl = (icon?: ProductIcon | null, defaultIcon?: string): string => {
const fallback = defaultIcon || "/logo.svg";
if (!icon) {
return fallback;
}
switch (icon.type) {
case "URL":
return icon.value || fallback;
case "BASE64":
// 如果value已经包含data URL前缀,直接使用;否则添加前缀
return icon.value ? (icon.value.startsWith('data:') ? icon.value : `data:image/png;base64,${icon.value}`) : fallback;
default:
return fallback;
}
};
export const ProductHeader: React.FC<ProductHeaderProps> = ({
name,
description,
icon,
defaultIcon = "/default-icon.png",
mcpConfig,
updatedAt,
productType,
}) => {
const { id, mcpName } = useParams();
const [isManageModalVisible, setIsManageModalVisible] = useState(false);
const [isApplyingSubscription, setIsApplyingSubscription] = useState(false);
const [selectedConsumerId, setSelectedConsumerId] = useState<string>('');
const [consumers, setConsumers] = useState<Consumer[]>([]);
// 分页相关state
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(5); // 每页显示5个订阅
// 分开管理不同的loading状态
const [consumersLoading, setConsumersLoading] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [imageLoadFailed, setImageLoadFailed] = useState(false);
// 订阅状态相关的state
const [subscriptionStatus, setSubscriptionStatus] = useState<{
hasSubscription: boolean;
subscribedConsumers: any[];
allConsumers: any[];
fullSubscriptionData?: {
content: any[];
totalElements: number;
totalPages: number;
};
} | null>(null);
const [subscriptionLoading, setSubscriptionLoading] = useState(false);
// 订阅详情分页数据(用于管理弹窗)
const [subscriptionDetails, setSubscriptionDetails] = useState<{
content: any[];
totalElements: number;
totalPages: number;
}>({ content: [], totalElements: 0, totalPages: 0 });
const [detailsLoading, setDetailsLoading] = useState(false);
// 搜索相关state
const [searchKeyword, setSearchKeyword] = useState("");
// 判断是否应该显示申请订阅按钮
const shouldShowSubscribeButton = !mcpConfig || mcpConfig.meta.source !== 'NACOS';
// 获取产品ID
const productId = id || mcpName || '';
// 查询订阅状态
const fetchSubscriptionStatus = async () => {
if (!productId || !shouldShowSubscribeButton) return;
setSubscriptionLoading(true);
try {
const status = await getProductSubscriptionStatus(productId);
setSubscriptionStatus(status);
} catch (error) {
console.error('获取订阅状态失败:', error);
} finally {
setSubscriptionLoading(false);
}
};
// 获取订阅详情(用于管理弹窗)
const fetchSubscriptionDetails = async (page: number = 1, search: string = ''): Promise<void> => {
if (!productId) return Promise.resolve();
setDetailsLoading(true);
try {
const response = await getProductSubscriptions(productId, {
consumerName: search.trim() || undefined,
page: page - 1, // 后端使用0基索引
size: pageSize
});
setSubscriptionDetails({
content: response.data.content || [],
totalElements: response.data.totalElements || 0,
totalPages: response.data.totalPages || 0
});
} catch (error) {
console.error('获取订阅详情失败:', error);
message.error('获取订阅详情失败,请重试');
} finally {
setDetailsLoading(false);
}
};
useEffect(() => {
fetchSubscriptionStatus();
}, [productId, shouldShowSubscribeButton]);
// 获取消费者列表
const fetchConsumers = async () => {
try {
setConsumersLoading(true);
const response = await getConsumers({}, { page: 1, size: 100 });
if (response.data) {
setConsumers(response.data.content || response.data);
}
} catch (error) {
// message.error('获取消费者列表失败');
} finally {
setConsumersLoading(false);
}
};
// 开始申请订阅流程
const startApplyingSubscription = () => {
setIsApplyingSubscription(true);
setSelectedConsumerId('');
fetchConsumers();
};
// 取消申请订阅
const cancelApplyingSubscription = () => {
setIsApplyingSubscription(false);
setSelectedConsumerId('');
};
// 提交申请订阅
const handleApplySubscription = async () => {
if (!selectedConsumerId) {
message.warning('请选择消费者');
return;
}
try {
setSubmitLoading(true);
await subscribeProduct(selectedConsumerId, productId);
message.success('申请提交成功');
// 重置状态
setIsApplyingSubscription(false);
setSelectedConsumerId('');
// 重新获取订阅状态和详情数据
await fetchSubscriptionStatus();
await fetchSubscriptionDetails(currentPage, '');
} catch (error) {
console.error('申请订阅失败:', error);
message.error('申请提交失败,请重试');
} finally {
setSubmitLoading(false);
}
};
// 显示管理弹窗
const showManageModal = () => {
setIsManageModalVisible(true);
// 优先使用已缓存的数据,避免重复查询
if (subscriptionStatus?.fullSubscriptionData) {
setSubscriptionDetails({
content: subscriptionStatus.fullSubscriptionData.content,
totalElements: subscriptionStatus.fullSubscriptionData.totalElements,
totalPages: subscriptionStatus.fullSubscriptionData.totalPages
});
// 重置分页到第一页
setCurrentPage(1);
setSearchKeyword('');
} else {
// 如果没有缓存数据,则重新获取
fetchSubscriptionDetails(1, '');
}
};
// 处理搜索输入变化
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearchKeyword(value);
// 只更新状态,不触发搜索
};
// 执行搜索
const handleSearch = (value?: string) => {
// 如果传入了value参数,使用该参数;否则使用当前的searchKeyword
const keyword = value !== undefined ? value : searchKeyword;
const trimmedKeyword = keyword.trim();
setCurrentPage(1);
// 总是调用API进行搜索,不使用缓存
fetchSubscriptionDetails(1, trimmedKeyword);
};
// 处理回车键搜索
const handleSearchKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSearch();
}
};
// 隐藏管理弹窗
const handleManageCancel = () => {
setIsManageModalVisible(false);
// 重置申请订阅状态
setIsApplyingSubscription(false);
setSelectedConsumerId('');
// 重置分页和搜索
setCurrentPage(1);
setSearchKeyword('');
// 清空订阅详情数据
setSubscriptionDetails({ content: [], totalElements: 0, totalPages: 0 });
};
// 取消订阅
const handleUnsubscribe = async (consumerId: string) => {
try {
await unsubscribeProduct(consumerId, productId);
message.success('取消订阅成功');
// 重新获取订阅状态和详情数据
await fetchSubscriptionStatus();
await fetchSubscriptionDetails(currentPage, '');
} catch (error) {
console.error('取消订阅失败:', error);
message.error('取消订阅失败,请重试');
}
};
return (
<>
<div className="mb-2">
{/* 第一行:图标和标题信息 */}
<div className="flex items-center gap-4 mb-3">
{(!icon || imageLoadFailed) && productType === 'REST_API' ? (
<div className="w-16 h-16 rounded-xl flex-shrink-0 flex items-center justify-center bg-gray-50 border border-gray-200">
<ApiOutlined className="text-3xl text-black" />
</div>
) : (
<img
src={getIconUrl(icon, defaultIcon)}
alt="icon"
className="w-16 h-16 rounded-xl object-cover border border-gray-200 flex-shrink-0"
onError={(e) => {
const target = e.target as HTMLImageElement;
if (productType === 'REST_API') {
setImageLoadFailed(true);
} else {
// 确保有一个最终的fallback图片,避免无限循环请求
const fallbackIcon = defaultIcon || "/logo.svg";
const currentUrl = new URL(target.src, window.location.href).href;
const fallbackUrl = new URL(fallbackIcon, window.location.href).href;
if (currentUrl !== fallbackUrl) {
target.src = fallbackIcon;
}
}
}}
/>
)}
<div className="flex-1 min-w-0 flex flex-col justify-center">
<Title level={3} className="mb-1 text-xl font-semibold">
{name}
</Title>
{updatedAt && (
<div className="text-sm text-gray-400">
{new Date(updatedAt).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\//g, '.')} updated
</div>
)}
</div>
</div>
{/* 第二行:描述信息,与左边框对齐 */}
<Paragraph className="text-gray-600 mb-3 text-sm leading-relaxed">
{description}
</Paragraph>
{/* 第三行:徽章式订阅状态 + 管理按钮,与左边框对齐 */}
{shouldShowSubscribeButton && (
<div className="flex items-center gap-4">
{subscriptionLoading ? (
<Button loading>加载中...</Button>
) : (
<>
{/* 订阅状态徽章 */}
<div className="flex items-center">
{subscriptionStatus?.hasSubscription ? (
<>
<div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
<span className="text-sm text-gray-600 font-medium">已订阅</span>
</>
) : (
<>
<div className="w-2 h-2 bg-gray-400 rounded-full mr-2"></div>
<span className="text-sm text-gray-600">未订阅</span>
</>
)}
</div>
{/* 管理按钮 */}
<Button
type="primary"
onClick={showManageModal}
>
管理订阅
</Button>
</>
)}
</div>
)}
</div>
{/* 订阅管理弹窗 */}
<Modal
title="订阅管理"
open={isManageModalVisible}
onCancel={handleManageCancel}
footer={null}
width={600}
styles={{
content: {
borderRadius: '8px',
padding: 0
},
header: {
borderRadius: '8px 8px 0 0',
marginBottom: 0,
paddingBottom: '8px'
},
body: {
padding: '0px'
}
}}
>
<div className="px-6 py-4">
{/* 产品名称标识 - 容器框样式 */}
<div className="mb-4">
<div className="bg-blue-50 border border-blue-200 rounded px-3 py-2">
<span className="text-sm text-gray-600 mr-2">产品名称:</span>
<span className="text-sm text-gray-600">{name}</span>
</div>
</div>
{/* 搜索框 */}
<div className="mb-4">
<Search
placeholder="搜索消费者名称"
value={searchKeyword}
onChange={handleSearchChange}
onSearch={handleSearch}
onPressEnter={handleSearchKeyPress}
allowClear
style={{ width: 250 }}
/>
</div>
{/* 优化的表格式 - 无表头,内嵌分页 */}
<div className="border border-gray-200 rounded overflow-hidden">
{detailsLoading ? (
<div className="p-8 text-center">
<Spin size="large" />
</div>
) : subscriptionDetails.content && subscriptionDetails.content.length > 0 ? (
<>
{/* 表格内容 */}
<div className="divide-y divide-gray-100">
{(searchKeyword.trim()
? subscriptionDetails.content
: subscriptionDetails.content.slice((currentPage - 1) * pageSize, currentPage * pageSize)
).map((item) => (
<div key={item.consumerId} className="flex items-center px-4 py-3 hover:bg-gray-50">
{/* 消费者名称 - 40% */}
<div className="flex-1 min-w-0 pr-4">
<span className="text-sm text-gray-700 truncate block">
{item.consumerName}
</span>
</div>
{/* 状态 - 30% */}
<div className="w-24 flex items-center pr-4">
{item.status === 'APPROVED' ? (
<>
<CheckCircleFilled className="text-green-500 mr-1" style={{fontSize: '10px'}} />
<span className="text-xs text-gray-700">已通过</span>
</>
) : item.status === 'PENDING' ? (
<>
<ClockCircleFilled className="text-blue-500 mr-1" style={{fontSize: '10px'}} />
<span className="text-xs text-gray-700">审核中</span>
</>
) : (
<>
<ExclamationCircleFilled className="text-red-500 mr-1" style={{fontSize: '10px'}} />
<span className="text-xs text-gray-700">已拒绝</span>
</>
)}
</div>
{/* 操作 - 30% */}
<div className="w-20">
<Popconfirm
title="确定要取消订阅吗?"
onConfirm={() => handleUnsubscribe(item.consumerId)}
okText="确认"
cancelText="取消"
>
<Button type="link" danger size="small" className="p-0">
取消订阅
</Button>
</Popconfirm>
</div>
</div>
))}
</div>
</>
) : (
<div className="p-8 text-center text-gray-500">
{searchKeyword ? '未找到匹配的订阅记录' : '暂无订阅记录'}
</div>
)}
</div>
{/* 分页 - 使用Ant Design分页组件,右对齐 */}
{subscriptionDetails.totalElements > 0 && (
<div className="mt-3 flex justify-end">
<Pagination
current={currentPage}
total={subscriptionDetails.totalElements}
pageSize={pageSize}
size="small"
showSizeChanger={true}
showQuickJumper={true}
onChange={(page, size) => {
setCurrentPage(page);
if (size !== pageSize) {
setPageSize(size);
}
// 如果有搜索关键词,需要重新查询;否则使用缓存数据
if (searchKeyword.trim()) {
fetchSubscriptionDetails(page, searchKeyword);
}
// 无搜索时不需要重新查询,Ant Design会自动处理前端分页
}}
onShowSizeChange={(_current, size) => {
setPageSize(size);
setCurrentPage(1);
// 如果有搜索关键词,需要重新查询;否则使用缓存数据
if (searchKeyword.trim()) {
fetchSubscriptionDetails(1, searchKeyword);
}
// 无搜索时不需要重新查询,页面大小变化会自动重新渲染
}}
showTotal={(total) => `共 ${total} 条`}
pageSizeOptions={['5', '10', '20']}
hideOnSinglePage={false}
/>
</div>
)}
{/* 申请订阅区域 - 移回底部 */}
<div className={`border-t pt-3 ${subscriptionDetails.totalElements > 0 ? 'mt-4' : 'mt-2'}`}>
<div className="flex justify-end">
{!isApplyingSubscription ? (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={startApplyingSubscription}
>
订阅
</Button>
) : (
<div className="w-full">
<div className="bg-gray-50 p-4 rounded">
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
选择消费者
</label>
<Select
placeholder="搜索或选择消费者"
style={{ width: '100%' }}
value={selectedConsumerId}
onChange={setSelectedConsumerId}
showSearch
loading={consumersLoading}
filterOption={(input, option) =>
(option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())
}
notFoundContent={consumersLoading ? '加载中...' : '暂无消费者数据'}
>
{consumers
.filter(consumer => {
// 过滤掉已经订阅的consumer
const isAlreadySubscribed = subscriptionStatus?.subscribedConsumers?.some(
item => item.consumer.consumerId === consumer.consumerId
);
return !isAlreadySubscribed;
})
.map(consumer => (
<Select.Option key={consumer.consumerId} value={consumer.consumerId}>
{consumer.name}
</Select.Option>
))
}
</Select>
</div>
<div className="flex justify-end gap-2">
<Button onClick={cancelApplyingSubscription}>
取消
</Button>
<Button
type="primary"
loading={submitLoading}
disabled={!selectedConsumerId}
onClick={handleApplySubscription}
>
确认申请
</Button>
</div>
</div>
</div>
)}
</div>
</div>
</div>
</Modal>
</>
);
};
```
--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/ProductServiceImpl.java:
--------------------------------------------------------------------------------
```java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.alibaba.apiopenplatform.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.core.constant.Resources;
import com.alibaba.apiopenplatform.core.event.PortalDeletingEvent;
import com.alibaba.apiopenplatform.core.event.ProductDeletingEvent;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.core.security.ContextHolder;
import com.alibaba.apiopenplatform.core.utils.IdGenerator;
import com.alibaba.apiopenplatform.dto.params.product.*;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.entity.*;
import com.alibaba.apiopenplatform.repository.*;
import com.alibaba.apiopenplatform.service.GatewayService;
import com.alibaba.apiopenplatform.service.PortalService;
import com.alibaba.apiopenplatform.service.ProductService;
import com.alibaba.apiopenplatform.service.NacosService;
import com.alibaba.apiopenplatform.support.enums.ProductStatus;
import com.alibaba.apiopenplatform.support.enums.ProductType;
import com.alibaba.apiopenplatform.support.enums.SourceType;
import com.alibaba.apiopenplatform.support.product.NacosRefConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Page;
import javax.persistence.criteria.*;
import javax.transaction.Transactional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class ProductServiceImpl implements ProductService {
private final ContextHolder contextHolder;
private final PortalService portalService;
private final GatewayService gatewayService;
private final ProductRepository productRepository;
private final ProductRefRepository productRefRepository;
private final ProductPublicationRepository publicationRepository;
private final SubscriptionRepository subscriptionRepository;
private final ConsumerRepository consumerRepository;
private final NacosService nacosService;
private final ApplicationEventPublisher eventPublisher;
@Override
public ProductResult createProduct(CreateProductParam param) {
productRepository.findByNameAndAdminId(param.getName(), contextHolder.getUser())
.ifPresent(product -> {
throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.PRODUCT, product.getName()));
});
String productId = IdGenerator.genApiProductId();
Product product = param.convertTo();
product.setProductId(productId);
product.setAdminId(contextHolder.getUser());
// 设置默认的自动审批配置,如果未指定则默认为null(使用平台级别配置)
if (param.getAutoApprove() != null) {
product.setAutoApprove(param.getAutoApprove());
}
productRepository.save(product);
return getProduct(productId);
}
@Override
public ProductResult getProduct(String productId) {
Product product = contextHolder.isAdministrator() ?
findProduct(productId) :
findPublishedProduct(contextHolder.getPortal(), productId);
ProductResult result = new ProductResult().convertFrom(product);
// 补充Product信息
fullFillProduct(result);
return result;
}
@Override
public PageResult<ProductResult> listProducts(QueryProductParam param, Pageable pageable) {
log.info("zhaoh-test-listProducts-start");
if (contextHolder.isDeveloper()) {
param.setPortalId(contextHolder.getPortal());
}
Page<Product> products = productRepository.findAll(buildSpecification(param), pageable);
return new PageResult<ProductResult>().convertFrom(
products, product -> {
ProductResult result = new ProductResult().convertFrom(product);
fullFillProduct(result);
return result;
});
}
@Override
public ProductResult updateProduct(String productId, UpdateProductParam param) {
Product product = findProduct(productId);
// 更换API产品类型
if (param.getType() != null && product.getType() != param.getType()) {
productRefRepository.findFirstByProductId(productId)
.ifPresent(productRef -> {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品已关联API");
});
}
param.update(product);
// Consumer鉴权配置同步至网关
Optional.ofNullable(param.getEnableConsumerAuth()).ifPresent(product::setEnableConsumerAuth);
// 更新自动审批配置
Optional.ofNullable(param.getAutoApprove()).ifPresent(product::setAutoApprove);
productRepository.saveAndFlush(product);
return getProduct(product.getProductId());
}
@Override
public void publishProduct(String productId, String portalId) {
portalService.existsPortal(portalId);
if (publicationRepository.findByPortalIdAndProductId(portalId, productId).isPresent()) {
return;
}
Product product = findProduct(productId);
product.setStatus(ProductStatus.PUBLISHED);
// 未关联不允许发布
if (getProductRef(productId) == null) {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品未关联API");
}
ProductPublication productPublication = new ProductPublication();
productPublication.setPortalId(portalId);
productPublication.setProductId(productId);
publicationRepository.save(productPublication);
productRepository.save(product);
}
@Override
public PageResult<ProductPublicationResult> getPublications(String productId, Pageable pageable) {
Page<ProductPublication> publications = publicationRepository.findByProductId(productId, pageable);
return new PageResult<ProductPublicationResult>().convertFrom(
publications, publication -> {
ProductPublicationResult publicationResult = new ProductPublicationResult().convertFrom(publication);
PortalResult portal;
try {
portal = portalService.getPortal(publication.getPortalId());
} catch (Exception e) {
log.error("Failed to get portal: {}", publication.getPortalId(), e);
return null;
}
publicationResult.setPortalName(portal.getName());
publicationResult.setAutoApproveSubscriptions(portal.getPortalSettingConfig().getAutoApproveSubscriptions());
return publicationResult;
});
}
@Override
public void unpublishProduct(String productId, String portalId) {
portalService.existsPortal(portalId);
publicationRepository.findByPortalIdAndProductId(portalId, productId)
.ifPresent(publicationRepository::delete);
}
@Override
public void deleteProduct(String productId) {
Product Product = findProduct(productId);
// 下线后删除
publicationRepository.deleteByProductId(productId);
productRepository.delete(Product);
// 异步清理Product资源
eventPublisher.publishEvent(new ProductDeletingEvent(productId));
}
/**
* 查找产品,如果不存在则抛出异常
*/
private Product findProduct(String productId) {
return productRepository.findByProductId(productId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
}
@Override
public void addProductRef(String productId, CreateProductRefParam param) {
Product product = findProduct(productId);
// 是否已存在API引用
productRefRepository.findByProductId(product.getProductId())
.ifPresent(productRef -> {
throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已关联API", Resources.PRODUCT, productId));
});
ProductRef productRef = param.convertTo();
productRef.setProductId(productId);
syncConfig(product, productRef);
productRepository.save(product);
productRefRepository.save(productRef);
}
@Override
public ProductRefResult getProductRef(String productId) {
return productRefRepository.findFirstByProductId(productId)
.map(productRef -> new ProductRefResult().convertFrom(productRef))
.orElse(null);
}
@Override
public void deleteProductRef(String productId) {
Product product = findProduct(productId);
product.setStatus(ProductStatus.PENDING);
ProductRef productRef = productRefRepository.findFirstByProductId(productId)
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST, "API产品未关联API"));
// 已发布的产品不允许解绑
if (publicationRepository.existsByProductId(productId)) {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品已发布");
}
productRefRepository.delete(productRef);
productRepository.save(product);
}
private void syncConfig(Product product, ProductRef productRef) {
SourceType sourceType = productRef.getSourceType();
if (sourceType.isGateway()) {
GatewayResult gateway = gatewayService.getGateway(productRef.getGatewayId());
// 根据网关类型选择对应的配置对象
Object config;
if (gateway.getGatewayType().isHigress()) {
config = productRef.getHigressRefConfig();
} else if (gateway.getGatewayType().isAdpAIGateway()) {
config = productRef.getAdpAIGatewayRefConfig();
} else if (gateway.getGatewayType().isApsaraGateway()) {
config = productRef.getApsaraGatewayRefConfig();
} else {
config = productRef.getApigRefConfig();
}
if (product.getType() == ProductType.REST_API) {
String apiConfig = gatewayService.fetchAPIConfig(gateway.getGatewayId(), config);
productRef.setApiConfig(apiConfig);
} else {
String mcpConfig = gatewayService.fetchMcpConfig(gateway.getGatewayId(), config);
productRef.setMcpConfig(mcpConfig);
}
} else if (sourceType.isNacos()) {
// 从Nacos获取MCP Server配置
NacosRefConfig nacosRefConfig = productRef.getNacosRefConfig();
if (nacosRefConfig != null) {
String mcpConfig = nacosService.fetchMcpConfig(productRef.getNacosId(), nacosRefConfig);
productRef.setMcpConfig(mcpConfig);
}
}
product.setStatus(ProductStatus.READY);
productRef.setEnabled(true);
}
private void fullFillProduct(ProductResult product) {
productRefRepository.findFirstByProductId(product.getProductId())
.ifPresent(productRef -> {
product.setEnabled(productRef.getEnabled());
if (StrUtil.isNotBlank(productRef.getApiConfig())) {
product.setApiConfig(JSONUtil.toBean(productRef.getApiConfig(), APIConfigResult.class));
}
// API Config
if (StrUtil.isNotBlank(productRef.getMcpConfig())) {
product.setMcpConfig(JSONUtil.toBean(productRef.getMcpConfig(), MCPConfigResult.class));
}
product.setStatus(ProductStatus.READY);
});
if (publicationRepository.existsByProductId(product.getProductId())) {
product.setStatus(ProductStatus.PUBLISHED);
}
}
private Product findPublishedProduct(String portalId, String productId) {
ProductPublication publication = publicationRepository.findByPortalIdAndProductId(portalId, productId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
return findProduct(publication.getProductId());
}
private Specification<Product> buildSpecification(QueryProductParam param) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (StrUtil.isNotBlank(param.getPortalId())) {
Subquery<String> subquery = query.subquery(String.class);
Root<ProductPublication> publicationRoot = subquery.from(ProductPublication.class);
subquery.select(publicationRoot.get("productId"))
.where(cb.equal(publicationRoot.get("portalId"), param.getPortalId()));
predicates.add(root.get("productId").in(subquery));
}
if (param.getType() != null) {
predicates.add(cb.equal(root.get("type"), param.getType()));
}
if (StrUtil.isNotBlank(param.getCategory())) {
predicates.add(cb.equal(root.get("category"), param.getCategory()));
}
if (param.getStatus() != null) {
predicates.add(cb.equal(root.get("status"), param.getStatus()));
}
if (StrUtil.isNotBlank(param.getName())) {
String likePattern = "%" + param.getName() + "%";
predicates.add(cb.like(root.get("name"), likePattern));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
@EventListener
@Async("taskExecutor")
@Override
public void handlePortalDeletion(PortalDeletingEvent event) {
String portalId = event.getPortalId();
try {
log.info("Starting to cleanup publications for portal {}", portalId);
publicationRepository.deleteAllByPortalId(portalId);
log.info("Completed cleanup publications for portal {}", portalId);
} catch (Exception e) {
log.error("Failed to cleanup developers for portal {}: {}", portalId, e.getMessage());
}
}
@Override
public Map<String, ProductResult> getProducts(List<String> productIds) {
List<Product> products = productRepository.findByProductIdIn(productIds);
return products.stream()
.collect(Collectors.toMap(Product::getProductId, product -> new ProductResult().convertFrom(product)));
}
@Override
public String getProductDashboard(String productId) {
// 获取产品关联的网关信息
ProductRef productRef = productRefRepository.findFirstByProductId(productId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
if (productRef.getGatewayId() == null) {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "该产品尚未关联网关服务");
}
// 基于产品类型选择Dashboard类型
Product product = findProduct(productId);
String dashboardType;
if (product.getType() == ProductType.MCP_SERVER) {
dashboardType = "MCP";
} else {
// REST_API、HTTP_API 统一走 API 面板
dashboardType = "API";
}
// 通过网关服务获取Dashboard URL
return gatewayService.getDashboard(productRef.getGatewayId(), dashboardType);
}
@Override
public PageResult<SubscriptionResult> listProductSubscriptions(String productId, QueryProductSubscriptionParam param, Pageable pageable) {
existsProduct(productId);
Page<ProductSubscription> subscriptions = subscriptionRepository.findAll(buildProductSubscriptionSpec(productId, param), pageable);
List<String> consumerIds = subscriptions.getContent().stream()
.map(ProductSubscription::getConsumerId)
.collect(Collectors.toList());
if (CollUtil.isEmpty(consumerIds)) {
return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
}
Map<String, Consumer> consumers = consumerRepository.findByConsumerIdIn(consumerIds)
.stream()
.collect(Collectors.toMap(Consumer::getConsumerId, consumer -> consumer));
return new PageResult<SubscriptionResult>().convertFrom(subscriptions, s -> {
SubscriptionResult r = new SubscriptionResult().convertFrom(s);
Consumer consumer = consumers.get(r.getConsumerId());
if (consumer != null) {
r.setConsumerName(consumer.getName());
}
return r;
});
}
@Override
public void existsProduct(String productId) {
productRepository.findByProductId(productId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.PRODUCT, productId));
}
private Specification<ProductSubscription> buildProductSubscriptionSpec(String productId, QueryProductSubscriptionParam param) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("productId"), productId));
// 如果是开发者,只能查看自己的Consumer订阅
if (contextHolder.isDeveloper()) {
Subquery<String> consumerSubquery = query.subquery(String.class);
Root<Consumer> consumerRoot = consumerSubquery.from(Consumer.class);
consumerSubquery.select(consumerRoot.get("consumerId"))
.where(cb.equal(consumerRoot.get("developerId"), contextHolder.getUser()));
predicates.add(root.get("consumerId").in(consumerSubquery));
}
if (param.getStatus() != null) {
predicates.add(cb.equal(root.get("status"), param.getStatus()));
}
if (StrUtil.isNotBlank(param.getConsumerName())) {
Subquery<String> consumerSubquery = query.subquery(String.class);
Root<Consumer> consumerRoot = consumerSubquery.from(Consumer.class);
consumerSubquery.select(consumerRoot.get("consumerId"))
.where(cb.like(
cb.lower(consumerRoot.get("name")),
"%" + param.getConsumerName().toLowerCase() + "%"
));
predicates.add(root.get("consumerId").in(consumerSubquery));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
}
```
--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/ApiProductApiDocs.tsx:
--------------------------------------------------------------------------------
```typescript
import { Card, Tag, Tabs, Table, Collapse, Descriptions } from "antd";
import { useEffect, useMemo, useState } from "react";
import type { ApiProduct } from "@/types/api-product";
import MonacoEditor from "react-monaco-editor";
import * as yaml from "js-yaml";
import { ProductTypeMap } from "@/lib/utils";
// 来源类型映射
const FromTypeMap: Record<string, string> = {
HTTP: "HTTP转MCP",
MCP: "MCP直接代理",
OPEN_API: "OpenAPI转MCP",
DIRECT_ROUTE: "直接路由",
DATABASE: "数据库",
};
// 来源映射
const SourceMap: Record<string, string> = {
APIG_AI: "AI网关",
HIGRESS: "Higress",
NACOS: "Nacos",
APIG_API: "API网关"
};
interface ApiProductApiDocsProps {
apiProduct: ApiProduct;
handleRefresh: () => void;
}
export function ApiProductApiDocs({ apiProduct }: ApiProductApiDocsProps) {
const [content, setContent] = useState("");
// OpenAPI 端点
const [endpoints, setEndpoints] = useState<
Array<{
key: string;
method: string;
path: string;
description: string;
operationId?: string;
}>
>([]);
// MCP 配置解析结果
const [mcpParsed, setMcpParsed] = useState<{
server?: { name?: string; config?: Record<string, unknown> };
tools?: Array<{
name: string;
description?: string;
args?: Array<{
name: string;
description?: string;
type?: string;
required?: boolean;
position?: string;
defaultValue?: string | number | boolean | null;
enumValues?: Array<string> | null;
}>;
}>;
allowTools?: Array<string>;
}>({});
// MCP 连接配置JSON
const [httpJson, setHttpJson] = useState("");
const [sseJson, setSseJson] = useState("");
const [localJson, setLocalJson] = useState("");
// 生成连接配置JSON
const generateConnectionConfig = (
domains: Array<{ domain: string; protocol: string }> | null | undefined,
path: string | null | undefined,
serverName: string,
localConfig?: unknown,
protocolType?: string
) => {
// 互斥:优先判断本地模式
if (localConfig) {
const localConfigJson = JSON.stringify(localConfig, null, 2);
setLocalJson(localConfigJson);
setHttpJson("");
setSseJson("");
return;
}
// HTTP/SSE 模式
if (domains && domains.length > 0 && path) {
const domain = domains[0];
const baseUrl = `${domain.protocol}://${domain.domain}`;
const endpoint = `${baseUrl}${path}`;
if (protocolType === 'SSE') {
// 仅生成SSE配置,不追加/sse
const sseConfig = `{
"mcpServers": {
"${serverName}": {
"type": "sse",
"url": "${endpoint}"
}
}
}`;
setSseJson(sseConfig);
setHttpJson("");
setLocalJson("");
return;
} else if (protocolType === 'StreamableHTTP') {
// 仅生成HTTP配置
const httpConfig = `{
"mcpServers": {
"${serverName}": {
"url": "${endpoint}"
}
}
}`;
setHttpJson(httpConfig);
setSseJson("");
setLocalJson("");
return;
} else {
// protocol为null或其他值:生成两种配置
const httpConfig = `{
"mcpServers": {
"${serverName}": {
"url": "${endpoint}"
}
}
}`;
const sseConfig = `{
"mcpServers": {
"${serverName}": {
"type": "sse",
"url": "${endpoint}/sse"
}
}
}`;
setHttpJson(httpConfig);
setSseJson(sseConfig);
setLocalJson("");
return;
}
}
// 无有效配置
setHttpJson("");
setSseJson("");
setLocalJson("");
};
useEffect(() => {
// 设置源码内容
if (apiProduct.apiConfig?.spec) {
setContent(apiProduct.apiConfig.spec);
} else if (apiProduct.mcpConfig?.tools) {
setContent(apiProduct.mcpConfig.tools);
} else {
setContent("");
}
// 解析 OpenAPI(如有)
if (apiProduct.apiConfig?.spec) {
const spec = apiProduct.apiConfig.spec;
try {
const list: Array<{
key: string;
method: string;
path: string;
description: string;
operationId?: string;
}> = [];
const lines = spec.split("\n");
let currentPath = "";
let inPaths = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmedLine = line.trim();
const indentLevel = line.length - line.trimStart().length;
if (trimmedLine === "paths:" || trimmedLine.startsWith("paths:")) {
inPaths = true;
continue;
}
if (!inPaths) continue;
if (
inPaths &&
indentLevel === 2 &&
trimmedLine.startsWith("/") &&
trimmedLine.endsWith(":")
) {
currentPath = trimmedLine.slice(0, -1);
continue;
}
if (inPaths && indentLevel === 4) {
const httpMethods = [
"get:",
"post:",
"put:",
"delete:",
"patch:",
"head:",
"options:",
];
for (const method of httpMethods) {
if (trimmedLine.startsWith(method)) {
const methodName = method.replace(":", "").toUpperCase();
const operationId = extractOperationId(lines, i);
list.push({
key: `${methodName}-${currentPath}`,
method: methodName,
path: currentPath,
description: operationId || `${methodName} ${currentPath}`,
operationId,
});
break;
}
}
}
}
setEndpoints(list.length > 0 ? list : []);
} catch {
setEndpoints([]);
}
} else {
setEndpoints([]);
}
// 解析 MCP YAML(如有)
if (apiProduct.mcpConfig?.tools) {
try {
const doc = yaml.load(apiProduct.mcpConfig.tools) as any;
const toolsRaw = Array.isArray(doc?.tools) ? doc.tools : [];
const tools = toolsRaw.map((t: any) => ({
name: String(t?.name ?? ""),
description: t?.description ? String(t.description) : undefined,
args: Array.isArray(t?.args)
? t.args.map((a: any) => ({
name: String(a?.name ?? ""),
description: a?.description ? String(a.description) : undefined,
type: a?.type ? String(a.type) : undefined,
required: Boolean(a?.required),
position: a?.position ? String(a.position) : undefined,
defaultValue: a?.defaultValue ?? a?.default ?? null,
enumValues: a?.enumValues ?? a?.enum ?? null,
}))
: undefined,
}));
setMcpParsed({
server: doc?.server,
tools,
allowTools: Array.isArray(doc?.allowTools)
? doc.allowTools
: undefined,
});
// 生成连接配置JSON test
generateConnectionConfig(
apiProduct.mcpConfig.mcpServerConfig?.domains,
apiProduct.mcpConfig.mcpServerConfig?.path,
apiProduct.mcpConfig.mcpServerName,
apiProduct.mcpConfig.mcpServerConfig?.rawConfig,
apiProduct.mcpConfig.meta?.protocol
);
} catch {
setMcpParsed({});
}
} else {
setMcpParsed({});
}
}, [apiProduct]);
const isOpenApi = useMemo(
() => Boolean(apiProduct.apiConfig?.spec),
[apiProduct]
);
const isMcp = useMemo(
() => Boolean(apiProduct.mcpConfig?.tools),
[apiProduct]
);
const openApiColumns = useMemo(
() => [
{
title: "方法",
dataIndex: "method",
key: "method",
width: 100,
render: (method: string) => (
<span>
<Tag
color={
method === "GET"
? "green"
: method === "POST"
? "blue"
: method === "PUT"
? "orange"
: method === "DELETE"
? "red"
: "default"
}
>
{method}
</Tag>
</span>
),
},
{
title: "路径",
dataIndex: "path",
key: "path",
width: 260,
render: (path: string) => (
<code className="text-sm bg-gray-100 px-2 py-1 rounded">{path}</code>
),
},
],
[]
);
function extractOperationId(lines: string[], startIndex: number): string {
const currentIndent =
lines[startIndex].length - lines[startIndex].trimStart().length;
for (
let i = startIndex + 1;
i < Math.min(startIndex + 20, lines.length);
i++
) {
const line = lines[i];
const trimmedLine = line.trim();
const lineIndent = line.length - line.trimStart().length;
if (lineIndent <= currentIndent && trimmedLine !== "") break;
if (trimmedLine.startsWith("operationId:")) {
return trimmedLine.replace("operationId:", "").trim();
}
}
return "";
}
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold mb-2">API配置</h1>
<p className="text-gray-600">查看API定义和规范</p>
</div>
</div>
<Tabs
defaultActiveKey="overview"
items={[
{
key: "overview",
label: "API配置",
children: (
<div className="space-y-4">
{isOpenApi && (
<>
<Descriptions
column={2}
bordered
size="small"
className="mb-4"
>
{/* 'APIG_API' | 'HIGRESS' | 'APIG_AI' */}
<Descriptions.Item label="API来源">
{SourceMap[apiProduct.apiConfig?.meta.source || '']}
</Descriptions.Item>
<Descriptions.Item label="API类型">
{apiProduct.apiConfig?.meta.type}
</Descriptions.Item>
</Descriptions>
<Table
columns={openApiColumns as any}
dataSource={endpoints}
rowKey="key"
pagination={false}
size="small"
/>
</>
)}
{isMcp && (
<>
<Descriptions
column={2}
bordered
size="small"
className="mb-4"
>
<Descriptions.Item label="名称">
{mcpParsed.server?.name ||
apiProduct.mcpConfig?.meta.mcpServerName ||
"—"}
</Descriptions.Item>
<Descriptions.Item label="来源">
{apiProduct.mcpConfig?.meta.source
? SourceMap[apiProduct.mcpConfig.meta.source] || apiProduct.mcpConfig.meta.source
: "—"}
</Descriptions.Item>
<Descriptions.Item label="来源类型">
{apiProduct.mcpConfig?.meta.fromType
? FromTypeMap[apiProduct.mcpConfig.meta.fromType] || apiProduct.mcpConfig.meta.fromType
: "—"}
</Descriptions.Item>
<Descriptions.Item label="API类型">
{apiProduct.mcpConfig?.meta.source
? ProductTypeMap[apiProduct.type] || apiProduct.type
: "—"}
</Descriptions.Item>
</Descriptions>
<div className="mb-2">
<span className="font-bold mr-2">工具列表:</span>
{/* {Array.isArray(mcpParsed.tools) && mcpParsed.tools.length > 0 ? (
mcpParsed.tools.map((tool, idx) => (
<Tag key={tool.name || idx} color="blue" className="mr-1">
{tool.name}
</Tag>
))
) : (
<span className="text-gray-400">—</span>
)} */}
</div>
<Collapse accordion>
{(mcpParsed.tools || []).map((tool, idx) => (
<Collapse.Panel header={tool.name} key={idx}>
{tool.description && (
<div className="mb-2 text-gray-600">
{tool.description}
</div>
)}
<div className="mb-2 font-bold">输入参数:</div>
<div className="space-y-2">
{tool.args && tool.args.length > 0 ? (
tool.args.map((arg, aidx) => (
<div key={aidx} className="flex flex-col mb-2">
<div className="flex items-center mb-1">
<span className="font-medium mr-2">
{arg.name}
</span>
{arg.type && (
<span className="text-xs text-gray-500 mr-2">
({arg.type})
</span>
)}
{arg.required && (
<span className="text-red-500 text-xs">
*
</span>
)}
</div>
{arg.description && (
<div className="text-xs text-gray-500 mb-1">
{arg.description}
</div>
)}
<input
disabled
className="border rounded px-2 py-1 text-sm bg-gray-100 w-full max-w-md"
placeholder={
arg.defaultValue !== undefined &&
arg.defaultValue !== null
? String(arg.defaultValue)
: ""
}
/>
{Array.isArray(arg.enumValues) &&
arg.enumValues.length > 0 && (
<div className="text-xs text-gray-500 mt-1">
可选值:{arg.enumValues.join(", ")}
</div>
)}
</div>
))
) : (
<span className="text-gray-400">无参数</span>
)}
</div>
</Collapse.Panel>
))}
</Collapse>
</>
)}
{!isOpenApi && !isMcp && (
<Card>
<div className="text-center py-8 text-gray-500">
<p>暂无配置</p>
</div>
</Card>
)}
</div>
),
},
...(!isMcp ? [{
key: "source",
label: "OpenAPI 规范",
children: (
<div style={{ height: 460 }}>
<MonacoEditor
language="yaml"
theme="vs-light"
value={content}
options={{
readOnly: true,
minimap: { enabled: true },
scrollBeyondLastLine: false,
scrollbar: { vertical: "visible", horizontal: "visible" },
wordWrap: "off",
lineNumbers: "on",
automaticLayout: true,
fontSize: 14,
copyWithSyntaxHighlighting: true,
contextmenu: true,
}}
height="100%"
/>
</div>
),
}] : []),
...(isMcp ? [{
key: "mcpServerConfig",
label: "MCP连接配置",
children: (
<div className="space-y-4">
<div className="">
{apiProduct.mcpConfig?.mcpServerConfig?.rawConfig ? (
// Local Mode - 显示本地配置
<div>
<h3 className="text-lg font-bold mb-2">Local Config</h3>
<MonacoEditor
language="json"
theme="vs-light"
value={localJson}
options={{
readOnly: true,
minimap: { enabled: true },
scrollBeyondLastLine: false,
scrollbar: { vertical: "visible", horizontal: "visible" },
wordWrap: "off",
lineNumbers: "on",
automaticLayout: true,
fontSize: 14,
copyWithSyntaxHighlighting: true,
contextmenu: true,
}}
height="150px"
/>
</div>
) : (
// HTTP/SSE Mode - 根据配置状态动态显示
<>
{httpJson && (
<div className="mt-4">
<h3 className="text-lg font-bold mb-2">HTTP Config</h3>
<MonacoEditor
language="json"
theme="vs-light"
value={httpJson}
options={{
readOnly: true,
minimap: { enabled: true },
scrollBeyondLastLine: false,
scrollbar: { vertical: "visible", horizontal: "visible" },
wordWrap: "off",
lineNumbers: "on",
automaticLayout: true,
fontSize: 14,
copyWithSyntaxHighlighting: true,
contextmenu: true,
}}
height="150px"
/>
</div>
)}
{sseJson && (
<div className="mt-4">
<h3 className="text-lg font-bold mb-2">SSE Config</h3>
<MonacoEditor
language="json"
theme="vs-light"
value={sseJson}
options={{
readOnly: true,
minimap: { enabled: true },
scrollBeyondLastLine: false,
scrollbar: { vertical: "visible", horizontal: "visible" },
wordWrap: "off",
lineNumbers: "on",
automaticLayout: true,
fontSize: 14,
copyWithSyntaxHighlighting: true,
contextmenu: true,
}}
height="150px"
/>
</div>
)}
</>
)}
</div>
</div>
),
}] : [])
]}
/>
</div>
);
}
```
--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/NacosServiceImpl.java:
--------------------------------------------------------------------------------
```java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.alibaba.apiopenplatform.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.apiopenplatform.core.constant.Resources;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.core.security.ContextHolder;
import com.alibaba.apiopenplatform.core.utils.IdGenerator;
import com.alibaba.apiopenplatform.dto.params.nacos.CreateNacosParam;
import com.alibaba.apiopenplatform.dto.params.nacos.QueryNacosParam;
import com.alibaba.apiopenplatform.dto.params.nacos.UpdateNacosParam;
import com.alibaba.apiopenplatform.dto.result.NacosMCPServerResult;
import com.alibaba.apiopenplatform.dto.result.NacosNamespaceResult;
import com.alibaba.apiopenplatform.dto.result.NacosResult;
import com.alibaba.apiopenplatform.dto.result.PageResult;
import com.alibaba.apiopenplatform.dto.result.MCPConfigResult;
import com.alibaba.apiopenplatform.dto.result.MseNacosResult;
import com.alibaba.apiopenplatform.entity.NacosInstance;
import com.alibaba.apiopenplatform.repository.NacosInstanceRepository;
import com.alibaba.apiopenplatform.service.NacosService;
import com.alibaba.apiopenplatform.support.enums.SourceType;
import com.alibaba.apiopenplatform.support.product.NacosRefConfig;
import com.alibaba.apiopenplatform.dto.converter.NacosToGatewayToolsConverter;
import cn.hutool.json.JSONUtil;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.alibaba.nacos.maintainer.client.ai.AiMaintainerFactory;
import com.alibaba.nacos.maintainer.client.ai.McpMaintainerService;
import com.alibaba.nacos.maintainer.client.naming.NamingMaintainerFactory;
import com.alibaba.nacos.maintainer.client.naming.NamingMaintainerService;
import com.alibaba.nacos.api.exception.NacosException;
import com.aliyun.mse20190531.Client;
import com.aliyun.mse20190531.models.ListClustersRequest;
import com.aliyun.mse20190531.models.ListClustersResponse;
import com.aliyun.mse20190531.models.ListClustersResponseBody;
import com.aliyun.teautil.models.RuntimeOptions;
import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class NacosServiceImpl implements NacosService {
private static final String DEFAULT_CONTEXT_PATH = "nacos";
private final NacosInstanceRepository nacosInstanceRepository;
private final ContextHolder contextHolder;
@Override
public PageResult<NacosResult> listNacosInstances(Pageable pageable) {
Page<NacosInstance> nacosInstances = nacosInstanceRepository.findAll(pageable);
return new PageResult<NacosResult>().convertFrom(nacosInstances, nacosInstance -> new NacosResult().convertFrom(nacosInstance));
}
@Override
public NacosResult getNacosInstance(String nacosId) {
NacosInstance nacosInstance = findNacosInstance(nacosId);
return new NacosResult().convertFrom(nacosInstance);
}
@Override
public void createNacosInstance(CreateNacosParam param) {
nacosInstanceRepository.findByNacosName(param.getNacosName())
.ifPresent(nacos -> {
throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.NACOS_INSTANCE, param.getNacosName()));
});
NacosInstance nacosInstance = param.convertTo();
// If client provided nacosId use it after checking uniqueness, otherwise generate one
String providedId = param.getNacosId();
if (providedId != null && !providedId.trim().isEmpty()) {
// ensure not already exist
boolean exists = nacosInstanceRepository.findByNacosId(providedId).isPresent();
if (exists) {
throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.NACOS_INSTANCE, providedId));
}
nacosInstance.setNacosId(providedId);
} else {
nacosInstance.setNacosId(IdGenerator.genNacosId());
}
nacosInstance.setAdminId(contextHolder.getUser());
nacosInstanceRepository.save(nacosInstance);
}
@Override
public void updateNacosInstance(String nacosId, UpdateNacosParam param) {
NacosInstance instance = findNacosInstance(nacosId);
Optional.ofNullable(param.getNacosName())
.filter(name -> !name.equals(instance.getNacosName()))
.flatMap(nacosInstanceRepository::findByNacosName)
.ifPresent(nacos -> {
throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在", Resources.NACOS_INSTANCE, param.getNacosName()));
});
param.update(instance);
nacosInstanceRepository.saveAndFlush(instance);
}
@Override
public void deleteNacosInstance(String nacosId) {
NacosInstance nacosInstance = findNacosInstance(nacosId);
nacosInstanceRepository.delete(nacosInstance);
}
@Override
public PageResult<MseNacosResult> fetchNacos(QueryNacosParam param, Pageable pageable) {
try {
// 创建MSE客户端
Client client = new Client(param.toClientConfig());
// 构建请求
ListClustersRequest request = new ListClustersRequest()
.setRegionId(param.getRegionId())
.setPageNum(pageable.getPageNumber() + 1)
.setPageSize(pageable.getPageSize());
RuntimeOptions runtime = new RuntimeOptions();
// 调用MSE API获取集群列表
ListClustersResponse response =
client.listClustersWithOptions(request, runtime);
// 转换响应结果,并过滤掉 clusterType 为 "Nacos-Ans" 的实例
Optional<List<MseNacosResult>> nacosResults = Optional.ofNullable(response.getBody())
.map(ListClustersResponseBody::getData)
.map(clusters -> clusters.stream()
.filter(cluster -> {
String type = cluster.getClusterType();
return (type == null || "Nacos-Ans".equalsIgnoreCase(type))
&& cluster.getVersionCode().startsWith("NACOS_3");
})
.map(MseNacosResult::fromListClustersResponseBodyData)
.collect(Collectors.toList())
);
if (nacosResults.isPresent()) {
// 返回分页结果
int total = response.getBody() != null && response.getBody().getTotalCount() != null ?
response.getBody().getTotalCount().intValue() : 0;
return PageResult.of(nacosResults.get(), pageable.getPageNumber(), pageable.getPageSize(), total);
}
return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
} catch (Exception e) {
log.error("Error fetching Nacos clusters from MSE", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Failed to fetch Nacos clusters from MSE: " + e.getMessage());
}
}
@Override
public PageResult<NacosMCPServerResult> fetchMcpServers(String nacosId, String namespaceId, Pageable pageable) throws Exception {
NacosInstance nacosInstance = findNacosInstance(nacosId);
McpMaintainerService service = buildDynamicMcpService(nacosInstance);
String ns = namespaceId == null ? "" : namespaceId;
com.alibaba.nacos.api.model.Page<McpServerBasicInfo> page = service.listMcpServer(ns, "", 1, Integer.MAX_VALUE);
if (page == null || page.getPageItems() == null) {
return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
}
return page.getPageItems().stream()
.map(basicInfo -> new NacosMCPServerResult().convertFrom(basicInfo))
.skip(pageable.getOffset())
.limit(pageable.getPageSize())
.collect(Collectors.collectingAndThen(
Collectors.toList(),
list -> PageResult.of(list, pageable.getPageNumber(), pageable.getPageSize(), page.getPageItems().size())
));
}
@Override
public PageResult<NacosNamespaceResult> fetchNamespaces(String nacosId, Pageable pageable) throws Exception {
NacosInstance nacosInstance = findNacosInstance(nacosId);
// 使用空 namespace 构建 (列出全部命名空间)
NamingMaintainerService namingService = buildDynamicNamingService(nacosInstance, "");
List<?> namespaces;
try {
namespaces = namingService.getNamespaceList();
} catch (NacosException e) {
log.error("Error fetching namespaces from Nacos by nacosId {}", nacosId, e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Failed to fetch namespaces: " + e.getErrMsg());
}
if (namespaces == null || namespaces.isEmpty()) {
return PageResult.empty(pageable.getPageNumber(), pageable.getPageSize());
}
List<NacosNamespaceResult> list = namespaces.stream()
.map(o -> new NacosNamespaceResult().convertFrom(o))
.skip(pageable.getOffset())
.limit(pageable.getPageSize())
.collect(Collectors.toList());
return PageResult.of(list, pageable.getPageNumber(), pageable.getPageSize(), namespaces.size());
}
@Override
public String fetchMcpConfig(String nacosId, NacosRefConfig nacosRefConfig) {
NacosInstance nacosInstance = findNacosInstance(nacosId);
McpMaintainerService service = buildDynamicMcpService(nacosInstance);
try {
McpServerDetailInfo detail = service.getMcpServerDetail(nacosRefConfig.getNamespaceId(),
nacosRefConfig.getMcpServerName(), null);
if (detail == null) {
return null;
}
MCPConfigResult mcpConfig = buildMCPConfigResult(detail);
return JSONUtil.toJsonStr(mcpConfig);
} catch (Exception e) {
log.error("Error fetching Nacos MCP servers", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Failed to fetch Nacos MCP config");
}
}
private MCPConfigResult buildMCPConfigResult(McpServerDetailInfo detail) {
MCPConfigResult mcpConfig = new MCPConfigResult();
mcpConfig.setMcpServerName(detail.getName());
MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig();
if (detail.getLocalServerConfig() != null) {
serverConfig.setRawConfig(detail.getLocalServerConfig());
serverConfig.setTransportMode(MCPConfigResult.MCPTransportMode.LOCAL.getMode());
} else if (detail.getRemoteServerConfig() != null || (detail.getBackendEndpoints() != null && !detail.getBackendEndpoints().isEmpty())) {
Object remoteConfig = buildRemoteConnectionConfig(detail);
serverConfig.setRawConfig(remoteConfig);
} else {
Map<String, Object> defaultConfig = new HashMap<>();
defaultConfig.put("type", "unknown");
defaultConfig.put("name", detail.getName());
serverConfig.setRawConfig(defaultConfig);
}
mcpConfig.setMcpServerConfig(serverConfig);
if (detail.getToolSpec() != null) {
try {
NacosToGatewayToolsConverter converter = new NacosToGatewayToolsConverter();
converter.convertFromNacos(detail);
String gatewayFormatYaml = converter.toYaml();
mcpConfig.setTools(gatewayFormatYaml);
} catch (Exception e) {
log.error("Error converting tools to gateway format", e);
mcpConfig.setTools(null);
}
} else {
mcpConfig.setTools(null);
}
MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
meta.setSource(SourceType.NACOS.name());
mcpConfig.setMeta(meta);
return mcpConfig;
}
private Object buildRemoteConnectionConfig(McpServerDetailInfo detail) {
List<?> backendEndpoints = detail.getBackendEndpoints();
if (backendEndpoints != null && !backendEndpoints.isEmpty()) {
Object firstEndpoint = backendEndpoints.get(0);
Map<String, Object> connectionConfig = new HashMap<>();
Map<String, Object> mcpServers = new HashMap<>();
Map<String, Object> serverConfig = new HashMap<>();
String endpointUrl = extractEndpointUrl(firstEndpoint);
if (endpointUrl != null) {
serverConfig.put("url", endpointUrl);
}
mcpServers.put(detail.getName(), serverConfig);
connectionConfig.put("mcpServers", mcpServers);
return connectionConfig;
}
Map<String, Object> basicConfig = new HashMap<>();
basicConfig.put("type", "remote");
basicConfig.put("name", detail.getName());
basicConfig.put("protocol", "http");
return basicConfig;
}
private String extractEndpointUrl(Object endpoint) {
if (endpoint == null) {
return null;
}
if (endpoint instanceof String) {
return (String) endpoint;
}
if (endpoint instanceof Map) {
Map<?, ?> endpointMap = (Map<?, ?>) endpoint;
String url = getStringValue(endpointMap, "url");
if (url != null) return url;
String endpointUrl = getStringValue(endpointMap, "endpointUrl");
if (endpointUrl != null) return endpointUrl;
String host = getStringValue(endpointMap, "host");
String port = getStringValue(endpointMap, "port");
String path = getStringValue(endpointMap, "path");
if (host != null) {
StringBuilder urlBuilder = new StringBuilder();
String protocol = getStringValue(endpointMap, "protocol");
urlBuilder.append(protocol != null ? protocol : "http").append("://");
urlBuilder.append(host);
if (port != null && !port.isEmpty()) {
urlBuilder.append(":").append(port);
}
if (path != null && !path.isEmpty()) {
if (!path.startsWith("/")) {
urlBuilder.append("/");
}
urlBuilder.append(path);
}
return urlBuilder.toString();
}
}
if (endpoint.getClass().getName().contains("McpEndpointInfo")) {
return extractUrlFromMcpEndpointInfo(endpoint);
}
return endpoint.toString();
}
private String getStringValue(Map<?, ?> map, String key) {
Object value = map.get(key);
return value != null ? value.toString() : null;
}
private String extractUrlFromMcpEndpointInfo(Object endpoint) {
String[] possibleFieldNames = {"url", "endpointUrl", "address", "host", "endpoint"};
for (String fieldName : possibleFieldNames) {
try {
java.lang.reflect.Field field = endpoint.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(endpoint);
if (value != null && !value.toString().trim().isEmpty()) {
if (value.toString().contains("://") || value.toString().contains(":")) {
return value.toString();
}
}
} catch (Exception e) {
continue;
}
}
java.lang.reflect.Field[] fields = endpoint.getClass().getDeclaredFields();
String host = null;
String port = null;
String path = null;
String protocol = null;
for (java.lang.reflect.Field field : fields) {
try {
field.setAccessible(true);
Object value = field.get(endpoint);
if (value != null && !value.toString().trim().isEmpty()) {
String fieldName = field.getName().toLowerCase();
if (fieldName.contains("host") || fieldName.contains("ip") || fieldName.contains("address")) {
host = value.toString();
} else if (fieldName.contains("port")) {
port = value.toString();
} else if (fieldName.contains("path") || fieldName.contains("endpoint") || fieldName.contains("uri")) {
path = value.toString();
} else if (fieldName.contains("protocol") || fieldName.contains("scheme")) {
protocol = value.toString();
}
}
} catch (Exception e) {
continue;
}
}
if (host != null) {
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(protocol != null ? protocol : "http").append("://");
urlBuilder.append(host);
if (port != null && !port.isEmpty()) {
urlBuilder.append(":").append(port);
}
if (path != null && !path.isEmpty()) {
if (!path.startsWith("/")) {
urlBuilder.append("/");
}
urlBuilder.append(path);
}
return urlBuilder.toString();
}
return endpoint.toString();
}
private NacosInstance findNacosInstance(String nacosId) {
return nacosInstanceRepository.findByNacosId(nacosId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.NACOS_INSTANCE, nacosId));
}
private McpMaintainerService buildDynamicMcpService(NacosInstance nacosInstance) {
Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosInstance.getServerUrl());
if (Objects.nonNull(nacosInstance.getUsername())) {
properties.setProperty(PropertyKeyConst.USERNAME, nacosInstance.getUsername());
}
if (Objects.nonNull(nacosInstance.getPassword())) {
properties.setProperty(PropertyKeyConst.PASSWORD, nacosInstance.getPassword());
}
properties.setProperty(PropertyKeyConst.CONTEXT_PATH, DEFAULT_CONTEXT_PATH);
// instance no longer stores namespace; leave namespace empty to let requests use default/public
// if consumers need a specific namespace, they should call an overload that accepts it
if (Objects.nonNull(nacosInstance.getAccessKey())) {
properties.setProperty(PropertyKeyConst.ACCESS_KEY, nacosInstance.getAccessKey());
}
if (Objects.nonNull(nacosInstance.getSecretKey())) {
properties.setProperty(PropertyKeyConst.SECRET_KEY, nacosInstance.getSecretKey());
}
try {
return AiMaintainerFactory.createAiMaintainerService(properties);
} catch (Exception e) {
log.error("Error init Nacos AiMaintainerService", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error init Nacos AiMaintainerService");
}
}
// removed unused no-namespace overload; use the runtime-namespace overload instead
// overload to build NamingMaintainerService with a runtime namespace value
private NamingMaintainerService buildDynamicNamingService(NacosInstance nacosInstance, String runtimeNamespace) {
Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, nacosInstance.getServerUrl());
if (Objects.nonNull(nacosInstance.getUsername())) {
properties.setProperty(PropertyKeyConst.USERNAME, nacosInstance.getUsername());
}
if (Objects.nonNull(nacosInstance.getPassword())) {
properties.setProperty(PropertyKeyConst.PASSWORD, nacosInstance.getPassword());
}
properties.setProperty(PropertyKeyConst.CONTEXT_PATH, DEFAULT_CONTEXT_PATH);
properties.setProperty(PropertyKeyConst.NAMESPACE, runtimeNamespace == null ? "" : runtimeNamespace);
if (Objects.nonNull(nacosInstance.getAccessKey())) {
properties.setProperty(PropertyKeyConst.ACCESS_KEY, nacosInstance.getAccessKey());
}
if (Objects.nonNull(nacosInstance.getSecretKey())) {
properties.setProperty(PropertyKeyConst.SECRET_KEY, nacosInstance.getSecretKey());
}
try {
return NamingMaintainerFactory.createNamingMaintainerService(properties);
} catch (Exception e) {
log.error("Error init Nacos NamingMaintainerService", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error init Nacos NamingMaintainerService");
}
}
}
```
--------------------------------------------------------------------------------
/portal-web/api-portal-frontend/src/pages/McpDetail.tsx:
--------------------------------------------------------------------------------
```typescript
import { useEffect, useState, useCallback } from "react";
import { useParams } from "react-router-dom";
import api from "../lib/api";
import { Layout } from "../components/Layout";
import { ProductHeader } from "../components/ProductHeader";
import {
Card,
Alert,
Button,
message,
Tabs,
Row,
Col,
Collapse,
} from "antd";
import { CopyOutlined } from "@ant-design/icons";
import ReactMarkdown from "react-markdown";
import { ProductType } from "../types";
import type {
Product,
McpConfig,
McpServerProduct,
ApiResponse,
} from "../types";
import * as yaml from "js-yaml";
import remarkGfm from 'remark-gfm';
import 'react-markdown-editor-lite/lib/index.css'
function McpDetail() {
const { mcpName } = useParams();
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [data, setData] = useState<Product | null>(null);
const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
const [parsedTools, setParsedTools] = useState<
Array<{
name: string;
description: string;
args?: Array<{
name: string;
description: string;
type: string;
required: boolean;
position: string;
default?: string;
enum?: string[];
}>;
}>
>([]);
const [httpJson, setHttpJson] = useState("");
const [sseJson, setSseJson] = useState("");
const [localJson, setLocalJson] = useState("");
// 解析YAML配置的函数
const parseYamlConfig = (
yamlString: string
): {
tools?: Array<{
name: string;
description: string;
args?: Array<{
name: string;
description: string;
type: string;
required: boolean;
position: string;
default?: string;
enum?: string[];
}>;
}>;
} | null => {
try {
const parsed = yaml.load(yamlString) as {
tools?: Array<{
name: string;
description: string;
args?: Array<{
name: string;
description: string;
type: string;
required: boolean;
position: string;
default?: string;
enum?: string[];
}>;
}>;
};
return parsed;
} catch (error) {
console.warn("解析YAML配置失败:", error);
return null;
}
};
// 格式化域名端口
const formatDomainWithPort = (domainStr: string, protocol: string) => {
const [host, port] = domainStr.split(':');
if (!port) return domainStr;
// 隐藏 HTTP 默认端口 80
if (protocol === 'http' && port === '80') return host;
// 隐藏 HTTPS 默认端口 443
if (protocol === 'https' && port === '443') return host;
return domainStr;
};
// 生成连接配置的函数
const generateConnectionConfig = useCallback((
domains: Array<{ domain: string; protocol: string }> | null | undefined,
path: string | null | undefined,
serverName: string,
localConfig?: unknown,
protocolType?: string
) => {
// 互斥:优先判断本地模式
if (localConfig) {
const localConfigJson = JSON.stringify(localConfig, null, 2);
setLocalJson(localConfigJson);
setHttpJson("");
setSseJson("");
return;
}
// HTTP/SSE 模式
if (domains && domains.length > 0 && path) {
const domain = domains[0];
const formattedDomain = formatDomainWithPort(domain.domain, domain.protocol);
const baseUrl = `${domain.protocol}://${formattedDomain}`;
let endpoint = `${baseUrl}${path}`;
if (mcpConfig?.meta?.source === 'ADP_AI_GATEWAY' || mcpConfig?.meta?.source === 'APSARA_GATEWAY') {
endpoint = `${baseUrl}/mcp-servers${path}`;
}
if (protocolType === 'SSE') {
// 仅生成SSE配置,不追加/sse
const sseConfig = `{
"mcpServers": {
"${serverName}": {
"type": "sse",
"url": "${endpoint}"
}
}
}`;
setSseJson(sseConfig);
setHttpJson("");
setLocalJson("");
return;
} else if (protocolType === 'StreamableHTTP') {
// 仅生成HTTP配置
const httpConfig = `{
"mcpServers": {
"${serverName}": {
"url": "${endpoint}"
}
}
}`;
setHttpJson(httpConfig);
setSseJson("");
setLocalJson("");
return;
} else {
// protocol为null或其他值:生成两种配置
const httpConfig = `{
"mcpServers": {
"${serverName}": {
"url": "${endpoint}"
}
}
}`;
const sseConfig = `{
"mcpServers": {
"${serverName}": {
"type": "sse",
"url": "${endpoint}/sse"
}
}
}`;
setHttpJson(httpConfig);
setSseJson(sseConfig);
setLocalJson("");
return;
}
}
// 无有效配置
setHttpJson("");
setSseJson("");
setLocalJson("");
}, [mcpConfig]);
useEffect(() => {
const fetchDetail = async () => {
if (!mcpName) {
return;
}
setLoading(true);
setError("");
try {
const response: ApiResponse<Product> = await api.get(`/products/${mcpName}`);
if (response.code === "SUCCESS" && response.data) {
setData(response.data);
// 处理MCP配置(统一使用新结构 mcpConfig)
if (response.data.type === ProductType.MCP_SERVER) {
const mcpProduct = response.data as McpServerProduct;
if (mcpProduct.mcpConfig) {
setMcpConfig(mcpProduct.mcpConfig);
// 解析tools配置
if (mcpProduct.mcpConfig.tools) {
const parsedConfig = parseYamlConfig(
mcpProduct.mcpConfig.tools
);
if (parsedConfig && parsedConfig.tools) {
setParsedTools(parsedConfig.tools);
}
}
}
}
} else {
setError(response.message || "数据加载失败");
}
} catch (error) {
console.error("API请求失败:", error);
setError("加载失败,请稍后重试");
} finally {
setLoading(false);
}
};
fetchDetail();
}, [mcpName]);
// 监听 mcpConfig 变化,重新生成连接配置
useEffect(() => {
if (mcpConfig) {
generateConnectionConfig(
mcpConfig.mcpServerConfig.domains,
mcpConfig.mcpServerConfig.path,
mcpConfig.mcpServerName,
mcpConfig.mcpServerConfig.rawConfig,
(mcpConfig.meta as any)?.protocol
);
}
}, [mcpConfig, generateConnectionConfig]);
const handleCopy = async (text: string) => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
// 非安全上下文降级处理
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}
message.success("已复制到剪贴板", 1);
} catch {
message.error("复制失败,请手动复制");
}
};
if (error) {
return (
<Layout loading={loading}>
<Alert message={error} type="error" showIcon className="my-8" />
</Layout>
);
}
if (!data) {
return (
<Layout loading={loading}>
<Alert
message="未找到相关数据"
type="warning"
showIcon
className="my-8"
/>
</Layout>
);
}
const { name, description } = data;
const hasLocalConfig = Boolean(mcpConfig?.mcpServerConfig.rawConfig);
return (
<Layout loading={loading}>
<div className="mb-6">
<ProductHeader
name={name}
description={description}
icon={data.icon}
defaultIcon="/MCP.svg"
mcpConfig={mcpConfig}
updatedAt={data.updatedAt}
productType="MCP_SERVER"
/>
<hr className="border-gray-200 mt-4" />
</div>
{/* 主要内容区域 - 左右布局 */}
<Row gutter={24}>
{/* 左侧内容 */}
<Col span={15}>
<Card className="mb-6 rounded-lg border-gray-200">
<Tabs
defaultActiveKey="overview"
items={[
{
key: "overview",
label: "Overview",
children: data.document ? (
<div className="min-h-[400px]">
<div
className="prose prose-lg max-w-none"
style={{
lineHeight: '1.7',
color: '#374151',
fontSize: '16px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
}}
>
<style>{`
.prose h1 {
color: #111827;
font-weight: 700;
font-size: 2.25rem;
line-height: 1.2;
margin-top: 0;
margin-bottom: 1.5rem;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 0.5rem;
}
.prose h2 {
color: #1f2937;
font-weight: 600;
font-size: 1.875rem;
line-height: 1.3;
margin-top: 2rem;
margin-bottom: 1rem;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.25rem;
}
.prose h3 {
color: #374151;
font-weight: 600;
font-size: 1.5rem;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.prose p {
margin-bottom: 1.25rem;
color: #4b5563;
line-height: 1.7;
font-size: 16px;
}
.prose code {
background-color: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 0.375rem;
padding: 0.125rem 0.375rem;
font-size: 0.875rem;
color: #374151;
font-weight: 500;
}
.prose pre {
background-color: #1f2937;
border-radius: 0.5rem;
padding: 1.25rem;
overflow-x: auto;
margin: 1.5rem 0;
border: 1px solid #374151;
}
.prose pre code {
background-color: transparent;
border: none;
color: #f9fafb;
padding: 0;
font-size: 0.875rem;
font-weight: normal;
}
.prose blockquote {
border-left: 4px solid #3b82f6;
padding-left: 1rem;
margin: 1.5rem 0;
color: #6b7280;
font-style: italic;
background-color: #f8fafc;
padding: 1rem;
border-radius: 0.375rem;
font-size: 16px;
}
.prose ul, .prose ol {
margin: 1.25rem 0;
padding-left: 1.5rem;
}
.prose ol {
list-style-type: decimal;
list-style-position: outside;
}
.prose ul {
list-style-type: disc;
list-style-position: outside;
}
.prose li {
margin: 0.5rem 0;
color: #4b5563;
display: list-item;
font-size: 16px;
}
.prose ol li {
padding-left: 0.25rem;
}
.prose ul li {
padding-left: 0.25rem;
}
.prose table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
font-size: 16px;
}
.prose th, .prose td {
border: 1px solid #d1d5db;
padding: 0.75rem;
text-align: left;
}
.prose th {
background-color: #f9fafb;
font-weight: 600;
color: #374151;
font-size: 16px;
}
.prose td {
color: #4b5563;
font-size: 16px;
}
.prose a {
color: #3b82f6;
text-decoration: underline;
font-weight: 500;
transition: color 0.2s;
font-size: inherit;
}
.prose a:hover {
color: #1d4ed8;
}
.prose strong {
color: #111827;
font-weight: 600;
font-size: inherit;
}
.prose em {
color: #6b7280;
font-style: italic;
font-size: inherit;
}
.prose hr {
border: none;
height: 1px;
background-color: #e5e7eb;
margin: 2rem 0;
}
`}</style>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{data.document}</ReactMarkdown>
</div>
</div>
) : (
<div className="text-gray-500 text-center py-8">
No overview available
</div>
),
},
{
key: "tools",
label: `Tools (${parsedTools.length})`,
children: parsedTools.length > 0 ? (
<div className="border border-gray-200 rounded-lg bg-gray-50">
{parsedTools.map((tool, idx) => (
<div key={idx} className={idx < parsedTools.length - 1 ? "border-b border-gray-200" : ""}>
<Collapse
ghost
expandIconPosition="end"
items={[{
key: idx.toString(),
label: tool.name,
children: (
<div className="px-4 pb-2">
<div className="text-gray-600 mb-4">{tool.description}</div>
{tool.args && tool.args.length > 0 && (
<div>
<p className="font-medium text-gray-700 mb-3">输入参数:</p>
{tool.args.map((arg, argIdx) => (
<div key={argIdx} className="mb-3">
<div className="flex items-center mb-2">
<span className="font-medium text-gray-800 mr-2">{arg.name}</span>
<span className="text-xs bg-gray-200 text-gray-600 px-2 py-1 rounded mr-2">
{arg.type}
</span>
{arg.required && (
<span className="text-red-500 text-xs mr-2">*</span>
)}
{arg.description && (
<span className="text-xs text-gray-500">
{arg.description}
</span>
)}
</div>
<input
type="text"
placeholder={arg.description || `请输入${arg.name}`}
className="w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
))}
</div>
)}
{(!tool.args || tool.args.length === 0) && (
<div className="text-gray-500 text-sm">No parameters required</div>
)}
</div>
),
}]}
/>
</div>
))}
</div>
) : (
<div className="text-gray-500 text-center py-8">
No tools available
</div>
),
},
]}
/>
</Card>
</Col>
{/* 右侧连接指导 */}
<Col span={9}>
{mcpConfig && (
<Card className="mb-6 rounded-lg border-gray-200">
<div className="mb-4">
<h3 className="text-sm font-semibold mb-3">连接点配置</h3>
<Tabs
size="small"
defaultActiveKey={hasLocalConfig ? "local" : (sseJson ? "sse" : "http")}
items={(() => {
const tabs = [];
if (hasLocalConfig) {
tabs.push({
key: "local",
label: "Stdio",
children: (
<div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
<Button
type="text"
size="small"
icon={<CopyOutlined />}
className="absolute top-2 right-2 z-10"
onClick={() => handleCopy(localJson)}
/>
<div className="text-gray-800 font-mono text-xs overflow-x-auto">
<pre className="whitespace-pre-wrap">{localJson}</pre>
</div>
</div>
),
});
} else {
if (sseJson) {
tabs.push({
key: "sse",
label: "SSE",
children: (
<div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
<Button
type="text"
size="small"
icon={<CopyOutlined />}
className="absolute top-2 right-2 z-10"
onClick={() => handleCopy(sseJson)}
/>
<div className="text-gray-800 font-mono text-xs overflow-x-auto">
<pre className="whitespace-pre-wrap">{sseJson}</pre>
</div>
</div>
),
});
}
if (httpJson) {
tabs.push({
key: "http",
label: "Streaming HTTP",
children: (
<div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
<Button
type="text"
size="small"
icon={<CopyOutlined />}
className="absolute top-2 right-2 z-10"
onClick={() => handleCopy(httpJson)}
/>
<div className="text-gray-800 font-mono text-xs overflow-x-auto">
<pre className="whitespace-pre-wrap">{httpJson}</pre>
</div>
</div>
),
});
}
}
return tabs;
})()}
/>
</div>
</Card>
)}
</Col>
</Row>
</Layout>
);
}
export default McpDetail;
```
--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/impl/ConsumerServiceImpl.java:
--------------------------------------------------------------------------------
```java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.alibaba.apiopenplatform.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.core.constant.Resources;
import com.alibaba.apiopenplatform.core.event.DeveloperDeletingEvent;
import com.alibaba.apiopenplatform.core.event.ProductDeletingEvent;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.core.security.ContextHolder;
import com.alibaba.apiopenplatform.core.utils.IdGenerator;
import com.alibaba.apiopenplatform.dto.params.consumer.QueryConsumerParam;
import com.alibaba.apiopenplatform.dto.params.consumer.CreateConsumerParam;
import com.alibaba.apiopenplatform.dto.params.consumer.CreateCredentialParam;
import com.alibaba.apiopenplatform.dto.params.consumer.UpdateCredentialParam;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.dto.params.consumer.CreateSubscriptionParam;
import com.alibaba.apiopenplatform.dto.params.consumer.QuerySubscriptionParam;
import com.alibaba.apiopenplatform.entity.*;
import com.alibaba.apiopenplatform.repository.ConsumerRepository;
import com.alibaba.apiopenplatform.repository.ConsumerCredentialRepository;
import com.alibaba.apiopenplatform.repository.SubscriptionRepository;
import com.alibaba.apiopenplatform.service.ConsumerService;
import com.alibaba.apiopenplatform.service.GatewayService;
import com.alibaba.apiopenplatform.service.PortalService;
import com.alibaba.apiopenplatform.service.ProductService;
import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig;
import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
import com.alibaba.apiopenplatform.support.consumer.HmacConfig;
import com.alibaba.apiopenplatform.support.enums.CredentialMode;
import com.alibaba.apiopenplatform.support.enums.SourceType;
import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
import cn.hutool.core.util.BooleanUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.Predicate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import javax.transaction.Transactional;
import java.util.*;
import java.util.stream.Collectors;
import com.alibaba.apiopenplatform.support.enums.SubscriptionStatus;
import com.alibaba.apiopenplatform.repository.ConsumerRefRepository;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class ConsumerServiceImpl implements ConsumerService {
private final PortalService portalService;
private final ConsumerRepository consumerRepository;
private final GatewayService gatewayService;
private final ContextHolder contextHolder;
private final ConsumerCredentialRepository credentialRepository;
private final SubscriptionRepository subscriptionRepository;
private final ProductService productService;
private final ConsumerRefRepository consumerRefRepository;
@Override
public ConsumerResult createConsumer(CreateConsumerParam param) {
PortalResult portal = portalService.getPortal(contextHolder.getPortal());
String consumerId = IdGenerator.genConsumerId();
Consumer consumer = param.convertTo();
consumer.setConsumerId(consumerId);
consumer.setDeveloperId(contextHolder.getUser());
consumer.setPortalId(portal.getPortalId());
consumerRepository.save(consumer);
// 初始化Credential
ConsumerCredential credential = initCredential(consumerId);
credentialRepository.save(credential);
return getConsumer(consumerId);
}
@Override
public PageResult<ConsumerResult> listConsumers(QueryConsumerParam param, Pageable pageable) {
Page<Consumer> consumers = consumerRepository.findAll(buildConsumerSpec(param), pageable);
return new PageResult<ConsumerResult>().convertFrom(consumers, consumer -> new ConsumerResult().convertFrom(consumer));
}
@Override
public ConsumerResult getConsumer(String consumerId) {
Consumer consumer = contextHolder.isDeveloper() ? findDevConsumer(consumerId) : findConsumer(consumerId);
return new ConsumerResult().convertFrom(consumer);
}
@Override
public void deleteConsumer(String consumerId) {
Consumer consumer = contextHolder.isDeveloper() ? findDevConsumer(consumerId) : findConsumer(consumerId);
// 1. 先解除所有产品的授权
List<ProductSubscription> subscriptions = subscriptionRepository.findAllByConsumerId(consumerId);
for (ProductSubscription subscription : subscriptions) {
try {
// 如果订阅有授权配置,需要先解除授权
if (subscription.getConsumerAuthConfig() != null) {
ProductRefResult productRef = productService.getProductRef(subscription.getProductId());
if (productRef != null) {
GatewayConfig gatewayConfig = gatewayService.getGatewayConfig(productRef.getGatewayId());
ConsumerRef consumerRef = matchConsumerRef(consumerId, gatewayConfig);
if (consumerRef != null) {
gatewayService.revokeConsumerAuthorization(
productRef.getGatewayId(),
consumerRef.getGwConsumerId(),
subscription.getConsumerAuthConfig()
);
}
}
}
} catch (Exception e) {
log.error("revoke consumer authorization error, consumerId: {}, productId: {}",
consumerId, subscription.getProductId(), e);
}
}
// 2. 删除订阅记录
subscriptionRepository.deleteAllByConsumerId(consumerId);
// 3. 删除凭证
credentialRepository.deleteAllByConsumerId(consumerId);
// 4. 删除网关上的Consumer
List<ConsumerRef> consumerRefs = consumerRefRepository.findAllByConsumerId(consumerId);
for (ConsumerRef consumerRef : consumerRefs) {
try {
gatewayService.deleteConsumer(consumerRef.getGwConsumerId(), consumerRef.getGatewayConfig());
} catch (Exception e) {
log.error("deleteConsumer gatewayConsumer error, gwConsumerId: {}", consumerRef.getGwConsumerId(), e);
}
}
// 5. 删除ConsumerRef记录
consumerRefRepository.deleteAll(consumerRefs);
// 6. 最后删除Consumer本身
consumerRepository.delete(consumer);
}
@Override
public void createCredential(String consumerId, CreateCredentialParam param) {
existsConsumer(consumerId);
// Consumer仅一份Credential
credentialRepository.findByConsumerId(consumerId)
.ifPresent(c -> {
throw new BusinessException(ErrorCode.CONFLICT, StrUtil.format("{}:{}已存在凭证", Resources.CONSUMER, consumerId));
});
ConsumerCredential credential = param.convertTo();
credential.setConsumerId(consumerId);
complementCredentials(credential);
credentialRepository.save(credential);
}
private ConsumerCredential initCredential(String consumerId) {
ConsumerCredential credential = new ConsumerCredential();
credential.setConsumerId(consumerId);
ApiKeyConfig.ApiKeyCredential apiKeyCredential = new ApiKeyConfig.ApiKeyCredential();
ApiKeyConfig apiKeyConfig = new ApiKeyConfig();
apiKeyConfig.setCredentials(Collections.singletonList(apiKeyCredential));
credential.setApiKeyConfig(apiKeyConfig);
complementCredentials(credential);
return credential;
}
@Override
public ConsumerCredentialResult getCredential(String consumerId) {
existsConsumer(consumerId);
return credentialRepository.findByConsumerId(consumerId)
.map(credential -> new ConsumerCredentialResult().convertFrom(credential))
.orElse(new ConsumerCredentialResult());
}
@Override
public void updateCredential(String consumerId, UpdateCredentialParam param) {
ConsumerCredential credential = credentialRepository.findByConsumerId(consumerId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER_CREDENTIAL, consumerId));
param.update(credential);
List<ConsumerRef> consumerRefs = consumerRefRepository.findAllByConsumerId(consumerId);
for (ConsumerRef consumerRef : consumerRefs) {
try {
gatewayService.updateConsumer(consumerRef.getGwConsumerId(), credential, consumerRef.getGatewayConfig());
} catch (Exception e) {
log.error("update gatewayConsumer error, gwConsumerId: {}", consumerRef.getGwConsumerId(), e);
}
}
credentialRepository.saveAndFlush(credential);
}
@Override
public void deleteCredential(String consumerId) {
existsConsumer(consumerId);
credentialRepository.deleteAllByConsumerId(consumerId);
}
@Override
public SubscriptionResult subscribeProduct(String consumerId, CreateSubscriptionParam param) {
Consumer consumer = contextHolder.isDeveloper() ?
findDevConsumer(consumerId) : findConsumer(consumerId);
// 勿重复订阅
if (subscriptionRepository.findByConsumerIdAndProductId(consumerId, param.getProductId()).isPresent()) {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "重复订阅");
}
ProductResult product = productService.getProduct(param.getProductId());
ProductRefResult productRef = productService.getProductRef(param.getProductId());
if (productRef == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "API产品未关联API");
}
// 非网关型不支持订阅
if (productRef.getSourceType() != SourceType.GATEWAY) {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "API产品不支持订阅");
}
ConsumerCredential credential = credentialRepository.findByConsumerId(consumerId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER_CREDENTIAL, consumerId));
ProductSubscription subscription = param.convertTo();
subscription.setConsumerId(consumerId);
// 检查产品级别的自动审批设置
boolean autoApprove = false;
// 优先检查产品级别的autoApprove配置
if (product.getAutoApprove() != null) {
// 如果产品配置了autoApprove,直接使用产品级别的配置
autoApprove = product.getAutoApprove();
log.info("使用产品级别自动审批配置: productId={}, autoApprove={}", param.getProductId(), autoApprove);
} else {
// 如果产品未配置autoApprove,则使用平台级别的配置
PortalResult portal = portalService.getPortal(consumer.getPortalId());
log.info("portal: {}", JSONUtil.toJsonStr(portal));
autoApprove = portal.getPortalSettingConfig() != null
&& BooleanUtil.isTrue(portal.getPortalSettingConfig().getAutoApproveSubscriptions());
log.info("使用平台级别自动审批配置: portalId={}, autoApprove={}", consumer.getPortalId(), autoApprove);
}
if (autoApprove) {
// 如果autoApprove为true,立即授权并设置为APPROVED状态
ConsumerAuthConfig consumerAuthConfig = authorizeConsumer(consumer, credential, productRef);
subscription.setConsumerAuthConfig(consumerAuthConfig);
subscription.setStatus(SubscriptionStatus.APPROVED);
} else {
// 如果autoApprove为false,暂时不授权,设置为PENDING状态
subscription.setStatus(SubscriptionStatus.PENDING);
}
subscriptionRepository.save(subscription);
SubscriptionResult r = new SubscriptionResult().convertFrom(subscription);
r.setProductName(product.getName());
r.setProductType(product.getType());
return r;
}
@Override
public void unsubscribeProduct(String consumerId, String productId) {
existsConsumer(consumerId);
ProductSubscription subscription = subscriptionRepository
.findByConsumerIdAndProductId(consumerId, productId)
.orElse(null);
if (subscription == null) {
return;
}
if (subscription.getConsumerAuthConfig() != null) {
ProductRefResult productRef = productService.getProductRef(productId);
GatewayConfig gatewayConfig = gatewayService.getGatewayConfig(productRef.getGatewayId());
// 取消网关上的Consumer授权
Optional.ofNullable(matchConsumerRef(consumerId, gatewayConfig))
.ifPresent(consumerRef ->
gatewayService.revokeConsumerAuthorization(productRef.getGatewayId(), consumerRef.getGwConsumerId(), subscription.getConsumerAuthConfig())
);
}
subscriptionRepository.deleteByConsumerIdAndProductId(consumerId, productId);
}
@Override
public PageResult<SubscriptionResult> listSubscriptions(String consumerId, QuerySubscriptionParam param, Pageable pageable) {
existsConsumer(consumerId);
Page<ProductSubscription> subscriptions = subscriptionRepository.findAll(buildCredentialSpec(consumerId, param), pageable);
List<String> productIds = subscriptions.getContent().stream()
.map(ProductSubscription::getProductId)
.collect(Collectors.toList());
Map<String, ProductResult> products = productService.getProducts(productIds);
return new PageResult<SubscriptionResult>().convertFrom(subscriptions, s -> {
SubscriptionResult r = new SubscriptionResult().convertFrom(s);
ProductResult product = products.get(r.getProductId());
if (product != null) {
r.setProductType(product.getType());
r.setProductName(product.getName());
}
return r;
});
}
@Override
public void deleteSubscription(String consumerId, String productId) {
existsConsumer(consumerId);
subscriptionRepository.findByConsumerIdAndProductId(consumerId, productId)
.ifPresent(subscriptionRepository::delete);
}
@Override
public SubscriptionResult approveSubscription(String consumerId, String productId) {
existsConsumer(consumerId);
ProductSubscription subscription = subscriptionRepository.findByConsumerIdAndProductId(consumerId, productId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.SUBSCRIPTION, StrUtil.format("{}:{}", productId, consumerId)));
// 检查订阅状态,只有PENDING状态的订阅才能被审批
if (subscription.getStatus() != SubscriptionStatus.PENDING) {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "订阅已审批");
}
// 获取消费者和凭证信息
Consumer consumer = contextHolder.isDeveloper() ?
findDevConsumer(consumerId) : findConsumer(consumerId);
ConsumerCredential credential = credentialRepository.findByConsumerId(consumerId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER_CREDENTIAL, consumerId));
// 获取产品引用信息
ProductRefResult productRef = productService.getProductRef(productId);
if (productRef == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "API产品未关联API");
}
// 执行授权操作
ConsumerAuthConfig consumerAuthConfig = authorizeConsumer(consumer, credential, productRef);
// 更新订阅状态和授权配置
subscription.setConsumerAuthConfig(consumerAuthConfig);
subscription.setStatus(SubscriptionStatus.APPROVED);
subscriptionRepository.saveAndFlush(subscription);
ProductResult product = productService.getProduct(productId);
SubscriptionResult result = new SubscriptionResult().convertFrom(subscription);
if (product != null) {
result.setProductName(product.getName());
result.setProductType(product.getType());
}
return result;
}
private Consumer findConsumer(String consumerId) {
return consumerRepository.findByConsumerId(consumerId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER, consumerId));
}
private Consumer findDevConsumer(String consumerId) {
return consumerRepository.findByDeveloperIdAndConsumerId(contextHolder.getUser(), consumerId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER, consumerId));
}
private void existsConsumer(String consumerId) {
(contextHolder.isDeveloper() ?
consumerRepository.findByDeveloperIdAndConsumerId(contextHolder.getUser(), consumerId) :
consumerRepository.findByConsumerId(consumerId))
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, Resources.CONSUMER, consumerId));
}
private Specification<Consumer> buildConsumerSpec(QueryConsumerParam param) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (contextHolder.isDeveloper()) {
param.setDeveloperId(contextHolder.getUser());
}
if (StrUtil.isNotBlank(param.getDeveloperId())) {
predicates.add(cb.equal(root.get("developerId"), param.getDeveloperId()));
}
if (StrUtil.isNotBlank(param.getPortalId())) {
predicates.add(cb.equal(root.get("portalId"), param.getPortalId()));
}
if (StrUtil.isNotBlank(param.getName())) {
String likePattern = "%" + param.getName() + "%";
predicates.add(cb.like(cb.lower(root.get("name")), likePattern));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
private Specification<ProductSubscription> buildCredentialSpec(String consumerId, QuerySubscriptionParam param) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("consumerId"), consumerId));
if (param.getStatus() != null) {
predicates.add(cb.equal(root.get("status"), param.getStatus()));
}
if (StrUtil.isNotBlank(param.getProductName())) {
// 使用子查询
Subquery<String> productSubquery = query.subquery(String.class);
Root<Product> productRoot = productSubquery.from(Product.class);
productSubquery.select(productRoot.get("productId"))
.where(cb.like(
cb.lower(productRoot.get("name")),
"%" + param.getProductName().toLowerCase() + "%"
));
predicates.add(root.get("productId").in(productSubquery));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
/**
* 补充Credentials
*
* @param credential
*/
private void complementCredentials(ConsumerCredential credential) {
if (credential == null) {
return;
}
// ApiKey
if (credential.getApiKeyConfig() != null) {
List<ApiKeyConfig.ApiKeyCredential> apiKeyCredentials = credential.getApiKeyConfig().getCredentials();
if (apiKeyCredentials != null) {
for (ApiKeyConfig.ApiKeyCredential cred : apiKeyCredentials) {
if (cred.getMode() == CredentialMode.SYSTEM && StrUtil.isBlank(cred.getApiKey())) {
cred.setApiKey(IdGenerator.genIdWithPrefix("apikey-"));
}
}
}
}
// HMAC
if (credential.getHmacConfig() != null) {
List<HmacConfig.HmacCredential> hmacCredentials = credential.getHmacConfig().getCredentials();
if (hmacCredentials != null) {
for (HmacConfig.HmacCredential cred : hmacCredentials) {
if (cred.getMode() == CredentialMode.SYSTEM &&
(StrUtil.isBlank(cred.getAk()) || StrUtil.isBlank(cred.getSk()))) {
cred.setAk(IdGenerator.genIdWithPrefix("ak-"));
cred.setSk(IdGenerator.genIdWithPrefix("sk-"));
}
}
}
}
}
private ConsumerAuthConfig authorizeConsumer(Consumer consumer, ConsumerCredential credential, ProductRefResult productRef) {
GatewayConfig gatewayConfig = gatewayService.getGatewayConfig(productRef.getGatewayId());
// 检查是否在网关上有对应的Consumer
ConsumerRef existingConsumerRef = matchConsumerRef(consumer.getConsumerId(), gatewayConfig);
String gwConsumerId;
if (existingConsumerRef != null) {
// 如果存在ConsumerRef记录,需要检查实际网关中是否还存在该消费者
gwConsumerId = existingConsumerRef.getGwConsumerId();
// 检查实际网关中是否还存在该消费者
if (!isConsumerExistsInGateway(gwConsumerId, gatewayConfig)) {
log.warn("网关中的消费者已被删除,需要重新创建: gwConsumerId={}, gatewayType={}",
gwConsumerId, gatewayConfig.getGatewayType());
// 删除过期的ConsumerRef记录
consumerRefRepository.delete(existingConsumerRef);
// 重新创建消费者
gwConsumerId = gatewayService.createConsumer(consumer, credential, gatewayConfig);
consumerRefRepository.save(ConsumerRef.builder()
.consumerId(consumer.getConsumerId())
.gwConsumerId(gwConsumerId)
.gatewayType(gatewayConfig.getGatewayType())
.gatewayConfig(gatewayConfig)
.build());
}
} else {
// 如果不存在ConsumerRef记录,直接创建新的消费者
gwConsumerId = gatewayService.createConsumer(consumer, credential, gatewayConfig);
consumerRefRepository.save(ConsumerRef.builder()
.consumerId(consumer.getConsumerId())
.gwConsumerId(gwConsumerId)
.gatewayType(gatewayConfig.getGatewayType())
.gatewayConfig(gatewayConfig)
.build());
}
// 授权
return gatewayService.authorizeConsumer(productRef.getGatewayId(), gwConsumerId, productRef);
}
/**
* 检查消费者是否在实际网关中存在
*/
private boolean isConsumerExistsInGateway(String gwConsumerId, GatewayConfig gatewayConfig) {
try {
return gatewayService.isConsumerExists(gwConsumerId, gatewayConfig);
} catch (Exception e) {
log.warn("检查网关消费者存在性失败: gwConsumerId={}, gatewayType={}",
gwConsumerId, gatewayConfig.getGatewayType(), e);
// 如果检查失败,默认认为存在,避免无谓的重新创建
return true;
}
}
@EventListener
@Async("taskExecutor")
public void handleDeveloperDeletion(DeveloperDeletingEvent event) {
String developerId = event.getDeveloperId();
log.info("Cleaning consumers for developer {}", developerId);
List<Consumer> consumers = consumerRepository.findAllByDeveloperId(developerId);
consumers.forEach(consumer -> {
try {
deleteConsumer(consumer.getConsumerId());
} catch (Exception e) {
log.error("Failed to delete consumer {}", consumer.getConsumerId(), e);
}
});
}
@EventListener
@Async("taskExecutor")
public void handleProductDeletion(ProductDeletingEvent event) {
String productId = event.getProductId();
log.info("Cleaning subscriptions for product {}", productId);
subscriptionRepository.deleteAllByProductId(productId);
List<ProductSubscription> subscriptions = subscriptionRepository.findAllByProductId(productId);
subscriptions.forEach(subscription -> {
try {
unsubscribeProduct(subscription.getConsumerId(), subscription.getProductId());
} catch (Exception e) {
log.error("Failed to unsubscribe product {} for consumer {}", productId, subscription.getConsumerId(), e);
}
});
}
private ConsumerRef matchConsumerRef(String consumerId, GatewayConfig gatewayConfig) {
List<ConsumerRef> consumeRefs = consumerRefRepository.findAllByConsumerIdAndGatewayType(consumerId, gatewayConfig.getGatewayType());
if (consumeRefs.isEmpty()) {
return null;
}
for (ConsumerRef ref : consumeRefs) {
// 网关配置相同
if (StrUtil.equals(JSONUtil.toJsonStr(ref.getGatewayConfig()), JSONUtil.toJsonStr(gatewayConfig))) {
return ref;
}
}
return null;
}
}
```
--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/APIGOperator.java:
--------------------------------------------------------------------------------
```java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.alibaba.apiopenplatform.service.gateway;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.dto.params.gateway.QueryAPIGParam;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.support.consumer.APIGAuthConfig;
import com.alibaba.apiopenplatform.support.consumer.ApiKeyConfig;
import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
import com.alibaba.apiopenplatform.support.consumer.HmacConfig;
import com.alibaba.apiopenplatform.support.enums.APIGAPIType;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.entity.Gateway;
import com.alibaba.apiopenplatform.entity.Consumer;
import com.alibaba.apiopenplatform.entity.ConsumerCredential;
import com.alibaba.apiopenplatform.service.gateway.client.APIGClient;
import com.alibaba.apiopenplatform.service.gateway.client.SLSClient;
import com.alibaba.apiopenplatform.support.enums.GatewayType;
import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
import com.alibaba.apiopenplatform.support.product.APIGRefConfig;
import com.aliyun.sdk.gateway.pop.exception.PopClientException;
import com.aliyun.sdk.service.apig20240327.models.*;
import com.aliyun.sdk.service.apig20240327.models.CreateConsumerAuthorizationRulesRequest.AuthorizationRules;
import com.aliyun.sdk.service.apig20240327.models.CreateConsumerAuthorizationRulesRequest.ResourceIdentifier;
import com.aliyun.sdk.service.sls20201230.models.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
@Slf4j
@Primary
public class APIGOperator extends GatewayOperator<APIGClient> {
@Override
public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size) {
return fetchAPIs(gateway, APIGAPIType.HTTP, page, size);
}
public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size) {
return fetchAPIs(gateway, APIGAPIType.REST, page, size);
}
@Override
public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
throw new UnsupportedOperationException("APIG does not support MCP Servers");
}
@Override
public String fetchAPIConfig(Gateway gateway, Object config) {
APIGClient client = getClient(gateway);
try {
APIGRefConfig apigRefConfig = (APIGRefConfig) config;
CompletableFuture<ExportHttpApiResponse> f = client.execute(c -> {
ExportHttpApiRequest request = ExportHttpApiRequest.builder()
.httpApiId(apigRefConfig.getApiId())
.build();
return c.exportHttpApi(request);
});
ExportHttpApiResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
String contentBase64 = response.getBody().getData().getSpecContentBase64();
APIConfigResult configResult = new APIConfigResult();
// spec
String apiSpec = Base64.decodeStr(contentBase64);
configResult.setSpec(apiSpec);
// meta
APIConfigResult.APIMetadata meta = new APIConfigResult.APIMetadata();
meta.setSource(GatewayType.APIG_API.name());
meta.setType("REST");
configResult.setMeta(meta);
return JSONUtil.toJsonStr(configResult);
} catch (Exception e) {
log.error("Error fetching API Spec", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching API Spec,Cause:" + e.getMessage());
}
}
@Override
public String fetchMcpConfig(Gateway gateway, Object conf) {
throw new UnsupportedOperationException("APIG does not support MCP Servers");
}
@Override
public PageResult<GatewayResult> fetchGateways(Object param, int page, int size) {
return fetchGateways((QueryAPIGParam) param, page, size);
}
public PageResult<GatewayResult> fetchGateways(QueryAPIGParam param, int page, int size) {
APIGClient client = new APIGClient(param.convertTo());
List<GatewayResult> gateways = new ArrayList<>();
try {
CompletableFuture<ListGatewaysResponse> f = client.execute(c -> {
ListGatewaysRequest request = ListGatewaysRequest.builder()
.gatewayType(param.getGatewayType().getType())
.pageNumber(page)
.pageSize(size)
.build();
return c.listGateways(request);
});
ListGatewaysResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
for (ListGatewaysResponseBody.Items item : response.getBody().getData().getItems()) {
gateways.add(GatewayResult.builder()
.gatewayName(item.getName())
.gatewayId(item.getGatewayId())
.gatewayType(param.getGatewayType())
.build());
}
int total = Math.toIntExact(response.getBody().getData().getTotalSize());
return PageResult.of(gateways, page, size, total);
} catch (Exception e) {
log.error("Error fetching Gateways", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Gateways,Cause:" + e.getMessage());
}
}
protected String fetchGatewayEnv(Gateway gateway) {
APIGClient client = getClient(gateway);
try {
CompletableFuture<GetGatewayResponse> f = client.execute(c -> {
GetGatewayRequest request = GetGatewayRequest.builder()
.gatewayId(gateway.getGatewayId())
.build();
return c.getGateway(request);
});
GetGatewayResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
List<GetGatewayResponseBody.Environments> environments = response.getBody().getData().getEnvironments();
if (CollUtil.isEmpty(environments)) {
return null;
}
return environments.get(0).getEnvironmentId();
} catch (Exception e) {
log.error("Error fetching Gateway", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Gateway,Cause:" + e.getMessage());
}
}
@Override
public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) {
APIGClient client = new APIGClient(config.getApigConfig());
String mark = consumer.getConsumerId().substring(Math.max(0, consumer.getConsumerId().length() - 8));
String gwConsumerName = StrUtil.format("{}-{}", consumer.getName(), mark);
try {
// ApiKey
ApiKeyIdentityConfig apikeyIdentityConfig = convertToApiKeyIdentityConfig(credential.getApiKeyConfig());
// Hmac
List<AkSkIdentityConfig> akSkIdentityConfigs = convertToAkSkIdentityConfigs(credential.getHmacConfig());
CreateConsumerRequest.Builder builder = CreateConsumerRequest.builder()
.name(gwConsumerName)
.description("Created by HiMarket")
.gatewayType(config.getGatewayType().getType())
.enable(true);
if (apikeyIdentityConfig != null) {
builder.apikeyIdentityConfig(apikeyIdentityConfig);
}
if (akSkIdentityConfigs != null) {
builder.akSkIdentityConfigs(akSkIdentityConfigs);
}
CompletableFuture<CreateConsumerResponse> f = client.execute(c -> c.createConsumer(builder.build()));
CreateConsumerResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
return response.getBody().getData().getConsumerId();
} catch (Exception e) {
Throwable cause = e.getCause();
// Consumer已经存在
if (cause instanceof PopClientException && "Conflict.ConsumerNameDuplicate".equals(((PopClientException) cause).getErrCode())) {
return retrievalConsumer(gwConsumerName, config);
}
log.error("Error creating Consumer", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error creating Consumer,Cause:" + e.getMessage());
}
}
private String retrievalConsumer(String name, GatewayConfig gatewayConfig) {
APIGClient client = new APIGClient(gatewayConfig.getApigConfig());
try {
CompletableFuture<ListConsumersResponse> f = client.execute(c -> {
ListConsumersRequest request = ListConsumersRequest.builder()
.gatewayType(gatewayConfig.getGatewayType().getType())
.nameLike(name)
.pageNumber(1)
.pageSize(10)
.build();
return c.listConsumers(request);
});
ListConsumersResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
for (ListConsumersResponseBody.Items item : response.getBody().getData().getItems()) {
if (StrUtil.equals(item.getName(), name)) {
return item.getConsumerId();
}
}
} catch (Exception e) {
log.error("Error fetching Consumer", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Consumer,Cause:" + e.getMessage());
}
return null;
}
@Override
public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config) {
APIGClient client = new APIGClient(config.getApigConfig());
try {
// ApiKey
ApiKeyIdentityConfig apikeyIdentityConfig = convertToApiKeyIdentityConfig(credential.getApiKeyConfig());
// Hmac
List<AkSkIdentityConfig> akSkIdentityConfigs = convertToAkSkIdentityConfigs(credential.getHmacConfig());
UpdateConsumerRequest.Builder builder = UpdateConsumerRequest.builder()
.enable(true)
.consumerId(consumerId);
if (apikeyIdentityConfig != null) {
builder.apikeyIdentityConfig(apikeyIdentityConfig);
}
if (akSkIdentityConfigs != null) {
builder.akSkIdentityConfigs(akSkIdentityConfigs);
}
CompletableFuture<UpdateConsumerResponse> f = client.execute(c -> c.updateConsumer(builder.build()));
UpdateConsumerResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
} catch (Exception e) {
log.error("Error creating Consumer", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error creating Consumer,Cause:" + e.getMessage());
}
}
@Override
public void deleteConsumer(String consumerId, GatewayConfig config) {
APIGClient client = new APIGClient(config.getApigConfig());
try {
DeleteConsumerRequest request = DeleteConsumerRequest.builder()
.consumerId(consumerId)
.build();
client.execute(c -> {
c.deleteConsumer(request);
return null;
});
} catch (Exception e) {
log.error("Error deleting Consumer", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error deleting Consumer,Cause:" + e.getMessage());
}
}
@Override
public boolean isConsumerExists(String consumerId, GatewayConfig config) {
// TODO: 实现APIG网关消费者存在性检查
return true;
}
@Override
public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) {
APIGClient client = getClient(gateway);
APIGRefConfig config = (APIGRefConfig) refConfig;
// REST API 授权
String apiId = config.getApiId();
try {
List<HttpApiOperationInfo> operations = fetchRESTOperations(gateway, apiId);
if (CollUtil.isEmpty(operations)) {
return null;
}
// 确认Gateway的EnvId
String envId = fetchGatewayEnv(gateway);
List<AuthorizationRules> rules = new ArrayList<>();
for (HttpApiOperationInfo operation : operations) {
AuthorizationRules rule = AuthorizationRules.builder()
.consumerId(consumerId)
.expireMode("LongTerm")
.resourceType("RestApiOperation")
.resourceIdentifier(ResourceIdentifier.builder()
.resourceId(operation.getOperationId())
.environmentId(envId).build())
.build();
rules.add(rule);
}
CompletableFuture<CreateConsumerAuthorizationRulesResponse> f = client.execute(c -> {
CreateConsumerAuthorizationRulesRequest request = CreateConsumerAuthorizationRulesRequest.builder()
.authorizationRules(rules)
.build();
return c.createConsumerAuthorizationRules(request);
});
CreateConsumerAuthorizationRulesResponse response = f.join();
if (200 != response.getStatusCode()) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
APIGAuthConfig apigAuthConfig = APIGAuthConfig.builder()
.authorizationRuleIds(response.getBody().getData().getConsumerAuthorizationRuleIds())
.build();
return ConsumerAuthConfig.builder()
.apigAuthConfig(apigAuthConfig)
.build();
} catch (Exception e) {
log.error("Error authorizing consumer {} to apiId {} in APIG gateway {}", consumerId, apiId, gateway.getGatewayId(), e);
throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to authorize consumer to apiId in APIG gateway: " + e.getMessage());
}
}
@Override
public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig) {
APIGAuthConfig apigAuthConfig = authConfig.getApigAuthConfig();
if (apigAuthConfig == null) {
return;
}
APIGClient client = getClient(gateway);
try {
BatchDeleteConsumerAuthorizationRuleRequest request = BatchDeleteConsumerAuthorizationRuleRequest.builder()
.consumerAuthorizationRuleIds(StrUtil.join(",", apigAuthConfig.getAuthorizationRuleIds()))
.build();
CompletableFuture<BatchDeleteConsumerAuthorizationRuleResponse> f = client.execute(c -> c.batchDeleteConsumerAuthorizationRule(request));
BatchDeleteConsumerAuthorizationRuleResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
} catch (Exception e) {
Throwable cause = e.getCause();
if (cause instanceof PopClientException
&& "DatabaseError.RecordNotFound".equals(((PopClientException) cause).getErrCode())) {
log.warn("Consumer authorization rules[{}] not found, ignore", apigAuthConfig.getAuthorizationRuleIds());
return;
}
log.error("Error deleting Consumer Authorization", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error deleting Consumer Authorization,Cause:" + e.getMessage());
}
}
@Override
public GatewayType getGatewayType() {
return GatewayType.APIG_API;
}
@Override
public String getDashboard(Gateway gateway, String type) {
SLSClient ticketClient = new SLSClient(gateway.getApigConfig(), true);
String ticket = null;
try {
CreateTicketResponse response = ticketClient.execute(c -> {
CreateTicketRequest request = CreateTicketRequest.builder().build();
try {
return c.createTicket(request).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
});
ticket = response.getBody().getTicket();
} catch (Exception e) {
log.error("Error fetching API", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching createTicker API,Cause:" + e.getMessage());
}
SLSClient client = new SLSClient(gateway.getApigConfig(), false);
String projectName = null;
try {
ListProjectResponse response = client.execute(c -> {
ListProjectRequest request = ListProjectRequest.builder().projectName("product").build();
try {
return c.listProject(request).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
});
projectName = response.getBody().getProjects().get(0).getProjectName();
} catch (Exception e) {
log.error("Error fetching Project", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching Project,Cause:" + e.getMessage());
}
String region = gateway.getApigConfig().getRegion();
String gatewayId = gateway.getGatewayId();
String dashboardId = "";
if (type.equals("Portal")) {
dashboardId = "dashboard-1758009692051-393998";
} else if (type.equals("MCP")) {
dashboardId = "dashboard-1757483808537-433375";
} else if (type.equals("API")) {
dashboardId = "dashboard-1756276497392-966932";
}
String dashboardUrl = String.format("https://sls.console.aliyun.com/lognext/project/%s/dashboard/%s?filters=cluster_id%%253A%%2520%s&slsRegion=%s&sls_ticket=%s&isShare=true&hideTopbar=true&hideSidebar=true&ignoreTabLocalStorage=true", projectName, dashboardId, gatewayId, region, ticket);
log.info("Dashboard URL: {}", dashboardUrl);
return dashboardUrl;
}
public APIResult fetchAPI(Gateway gateway, String apiId) {
APIGClient client = getClient(gateway);
try {
CompletableFuture<GetHttpApiResponse> f = client.execute(c -> {
GetHttpApiRequest request = GetHttpApiRequest.builder()
.httpApiId(apiId)
.build();
return c.getHttpApi(request);
});
GetHttpApiResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
HttpApiApiInfo apiInfo = response.getBody().getData();
return new APIResult().convertFrom(apiInfo);
} catch (Exception e) {
log.error("Error fetching API", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching API,Cause:" + e.getMessage());
}
}
protected HttpRoute fetchHTTPRoute(Gateway gateway, String apiId, String routeId) {
APIGClient client = getClient(gateway);
try {
CompletableFuture<GetHttpApiRouteResponse> f = client.execute(c -> {
GetHttpApiRouteRequest request = GetHttpApiRouteRequest.builder()
.httpApiId(apiId)
.routeId(routeId)
.build();
return c.getHttpApiRoute(request);
});
GetHttpApiRouteResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
return response.getBody().getData();
} catch (Exception e) {
log.error("Error fetching HTTP Route", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching HTTP Route,Cause:" + e.getMessage());
}
}
protected PageResult<APIResult> fetchAPIs(Gateway gateway, APIGAPIType type, int page, int size) {
APIGClient client = getClient(gateway);
try {
List<APIResult> apis = new ArrayList<>();
CompletableFuture<ListHttpApisResponse> f = client.execute(c -> {
ListHttpApisRequest request = ListHttpApisRequest.builder()
.gatewayId(gateway.getGatewayId())
.gatewayType(gateway.getGatewayType().getType())
.types(type.getType())
.pageNumber(page)
.pageSize(size)
.build();
return c.listHttpApis(request);
});
ListHttpApisResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
for (HttpApiInfoByName item : response.getBody().getData().getItems()) {
for (HttpApiApiInfo apiInfo : item.getVersionedHttpApis()) {
APIResult apiResult = new APIResult().convertFrom(apiInfo);
apis.add(apiResult);
break;
}
}
int total = response.getBody().getData().getTotalSize();
return PageResult.of(apis, page, size, total);
} catch (Exception e) {
log.error("Error fetching APIs", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching APIs,Cause:" + e.getMessage());
}
}
public PageResult<HttpRoute> fetchHttpRoutes(Gateway gateway, String apiId, int page, int size) {
APIGClient client = getClient(gateway);
try {
CompletableFuture<ListHttpApiRoutesResponse> f = client.execute(c -> {
ListHttpApiRoutesRequest request = ListHttpApiRoutesRequest.builder()
.gatewayId(gateway.getGatewayId())
.httpApiId(apiId)
.pageNumber(page)
.pageSize(size)
.build();
return c.listHttpApiRoutes(request);
});
ListHttpApiRoutesResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
List<HttpRoute> httpRoutes = response.getBody().getData().getItems();
int total = response.getBody().getData().getTotalSize();
return PageResult.of(httpRoutes, page, size, total);
} catch (Exception e) {
log.error("Error fetching HTTP Roues", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching HTTP Roues,Cause:" + e.getMessage());
}
}
public List<HttpApiOperationInfo> fetchRESTOperations(Gateway gateway, String apiId) {
APIGClient client = getClient(gateway);
try {
CompletableFuture<ListHttpApiOperationsResponse> f = client.execute(c -> {
ListHttpApiOperationsRequest request = ListHttpApiOperationsRequest.builder()
.gatewayId(gateway.getGatewayId())
.httpApiId(apiId)
.pageNumber(1)
.pageSize(500)
.build();
return c.listHttpApiOperations(request);
});
ListHttpApiOperationsResponse response = f.join();
if (response.getStatusCode() != 200) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, response.getBody().getMessage());
}
return response.getBody().getData().getItems();
} catch (Exception e) {
log.error("Error fetching REST operations", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Error fetching REST operations,Cause:" + e.getMessage());
}
}
protected ApiKeyIdentityConfig convertToApiKeyIdentityConfig(ApiKeyConfig config) {
if (config == null) {
return null;
}
// ApikeySource
ApiKeyIdentityConfig.ApikeySource apikeySource = ApiKeyIdentityConfig.ApikeySource.builder()
.source(config.getSource())
.value(config.getKey())
.build();
// credentials
List<ApiKeyIdentityConfig.Credentials> credentials = config.getCredentials().stream()
.map(cred -> ApiKeyIdentityConfig.Credentials.builder()
.apikey(cred.getApiKey())
.generateMode("Custom")
.build())
.collect(Collectors.toList());
return ApiKeyIdentityConfig.builder()
.apikeySource(apikeySource)
.credentials(credentials)
.type("Apikey")
.build();
}
protected List<AkSkIdentityConfig> convertToAkSkIdentityConfigs(HmacConfig hmacConfig) {
if (hmacConfig == null || hmacConfig.getCredentials() == null) {
return null;
}
return hmacConfig.getCredentials().stream()
.map(cred -> AkSkIdentityConfig.builder()
.ak(cred.getAk())
.sk(cred.getSk())
.generateMode("Custom")
.type("AkSk")
.build())
.collect(Collectors.toList());
}
}
```
--------------------------------------------------------------------------------
/portal-server/src/main/java/com/alibaba/apiopenplatform/service/gateway/ApsaraGatewayOperator.java:
--------------------------------------------------------------------------------
```java
package com.alibaba.apiopenplatform.service.gateway;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.apiopenplatform.core.exception.BusinessException;
import com.alibaba.apiopenplatform.core.exception.ErrorCode;
import com.alibaba.apiopenplatform.dto.params.gateway.QueryApsaraGatewayParam;
import com.alibaba.apiopenplatform.dto.result.*;
import com.alibaba.apiopenplatform.entity.Consumer;
import com.alibaba.apiopenplatform.entity.ConsumerCredential;
import com.alibaba.apiopenplatform.entity.Gateway;
import com.alibaba.apiopenplatform.service.gateway.client.ApsaraStackGatewayClient;
import com.alibaba.apiopenplatform.support.consumer.AdpAIAuthConfig;
import com.alibaba.apiopenplatform.support.consumer.ConsumerAuthConfig;
import com.alibaba.apiopenplatform.support.enums.GatewayType;
import com.alibaba.apiopenplatform.support.gateway.GatewayConfig;
import com.alibaba.apiopenplatform.support.gateway.ApsaraGatewayConfig;
import com.alibaba.apiopenplatform.support.product.APIGRefConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.aliyun.apsarastack.csb220230206.models.*;
import com.aliyuncs.http.MethodType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class ApsaraGatewayOperator extends GatewayOperator<ApsaraStackGatewayClient> {
@Override
public PageResult<APIResult> fetchHTTPAPIs(Gateway gateway, int page, int size) {
throw new UnsupportedOperationException("Apsara gateway not implemented for HTTP APIs listing");
}
@Override
public PageResult<APIResult> fetchRESTAPIs(Gateway gateway, int page, int size) {
throw new UnsupportedOperationException("Apsara gateway not implemented for REST APIs listing");
}
@Override
public PageResult<? extends GatewayMCPServerResult> fetchMcpServers(Gateway gateway, int page, int size) {
ApsaraGatewayConfig cfg = gateway.getApsaraGatewayConfig();
if (cfg == null) {
throw new BusinessException(ErrorCode.INVALID_REQUEST, "Apsara gateway config is null");
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(cfg);
try {
// 使用SDK获取MCP服务器列表
ListMcpServersResponse response = client.ListMcpServers(gateway.getGatewayId(), page, size);
if (response.getBody() == null) {
return PageResult.of(new ArrayList<>(), page, size, 0);
}
// 修复类型不兼容问题
// 根据错误信息,getData()返回的是ListMcpServersResponseBodyData类型
ListMcpServersResponseBody.ListMcpServersResponseBodyData data =
response.getBody().getData();
if (data == null) {
return PageResult.of(new ArrayList<>(), page, size, 0);
}
int total = data.getTotal() != null ? data.getTotal() : 0;
List<GatewayMCPServerResult> items = new ArrayList<>();
// 使用工厂方法直接从SDK的record创建AdpMCPServerResult
if (data.getRecords() != null) {
items = data.getRecords().stream()
.map(AdpMCPServerResult::fromSdkRecord)
.collect(Collectors.toList());
}
return PageResult.of(items, page, size, total);
} catch (Exception e) {
log.error("Error fetching MCP servers by Apsara", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage());
} finally {
client.close();
}
}
@Override
public String fetchAPIConfig(Gateway gateway, Object config) {
throw new UnsupportedOperationException("Apsara gateway not implemented for API config export");
}
@Override
public String fetchMcpConfig(Gateway gateway, Object conf) {
ApsaraGatewayConfig config = gateway.getApsaraGatewayConfig();
if (config == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Apsara Gateway 配置缺失");
}
// 从conf参数中获取APIGRefConfig
APIGRefConfig apigRefConfig = (APIGRefConfig) conf;
if (apigRefConfig == null || apigRefConfig.getMcpServerName() == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "MCP Server 名称缺失");
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(config);
try {
// 使用SDK获取MCP Server详情
GetMcpServerResponse response = client.GetMcpServer(
gateway.getGatewayId(),
apigRefConfig.getMcpServerName()
);
if (response.getBody() == null || response.getBody().getData() == null) {
throw new BusinessException(ErrorCode.GATEWAY_ERROR, "MCP Server不存在");
}
GetMcpServerResponseBody.GetMcpServerResponseBodyData data = response.getBody().getData();
return convertToMCPConfig(data, gateway.getGatewayId(), config);
} catch (Exception e) {
log.error("Error fetching Apsara MCP config for server: {}", apigRefConfig.getMcpServerName(), e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage());
} finally {
client.close();
}
}
/**
* 将Apsara MCP Server详情转换为MCPConfigResult格式
*/
private String convertToMCPConfig(GetMcpServerResponseBody.GetMcpServerResponseBodyData data,
String gwInstanceId, ApsaraGatewayConfig config) {
MCPConfigResult mcpConfig = new MCPConfigResult();
mcpConfig.setMcpServerName(data.getName());
// 设置MCP Server配置
MCPConfigResult.MCPServerConfig serverConfig = new MCPConfigResult.MCPServerConfig();
serverConfig.setPath("/" + data.getName());
// 获取网关实例访问信息并设置域名信息
List<MCPConfigResult.Domain> domains = getGatewayAccessDomains(gwInstanceId, config);
if (domains != null && !domains.isEmpty()) {
serverConfig.setDomains(domains);
} else {
// 如果无法获取网关访问信息,则使用原有的services信息作为备选
if (data.getServices() != null && !data.getServices().isEmpty()) {
List<MCPConfigResult.Domain> fallbackDomains = data.getServices().stream()
.map(service -> MCPConfigResult.Domain.builder()
.domain(service.getName() + ":" + service.getPort())
.protocol("http")
.build())
.collect(Collectors.toList());
serverConfig.setDomains(fallbackDomains);
}
}
mcpConfig.setMcpServerConfig(serverConfig);
// 设置工具配置
mcpConfig.setTools(data.getRawConfigurations());
// 设置元数据
MCPConfigResult.McpMetadata meta = new MCPConfigResult.McpMetadata();
meta.setSource(GatewayType.APSARA_GATEWAY.name());
meta.setCreateFromType(data.getType());
mcpConfig.setMeta(meta);
return JSONUtil.toJsonStr(mcpConfig);
}
/**
* 获取网关实例的访问信息并构建域名列表
*/
private List<MCPConfigResult.Domain> getGatewayAccessDomains(String gwInstanceId, ApsaraGatewayConfig config) {
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(config);
try {
GetInstanceInfoResponse response = client.GetInstance(gwInstanceId);
if (response.getBody() == null || response.getBody().getData() == null) {
log.warn("Gateway instance not found, instanceId={}", gwInstanceId);
return null;
}
GetInstanceInfoResponseBody.GetInstanceInfoResponseBodyData instanceData = response.getBody().getData();
if (instanceData.getAccessMode() != null && !instanceData.getAccessMode().isEmpty()) {
return buildDomainsFromAccessModes(instanceData.getAccessMode());
}
log.warn("Gateway instance has no accessMode, instanceId={}", gwInstanceId);
return null;
} catch (Exception e) {
log.error("Error fetching gateway access info for instance: {}", gwInstanceId, e);
return null;
} finally {
client.close();
}
}
/**
* 根据网关实例访问信息构建域名列表
*/
private List<MCPConfigResult.Domain> buildDomainsFromAccessModes(
List<GetInstanceInfoResponseBody.GetInstanceInfoResponseBodyDataAccessMode> accessModes) {
List<MCPConfigResult.Domain> domains = new ArrayList<>();
if (accessModes == null || accessModes.isEmpty()) {
return domains;
}
GetInstanceInfoResponseBody.GetInstanceInfoResponseBodyDataAccessMode accessMode = accessModes.get(0);
// 1) LoadBalancer: externalIps:80
if ("LoadBalancer".equalsIgnoreCase(accessMode.getAccessModeType())) {
if (accessMode.getExternalIps() != null && !accessMode.getExternalIps().isEmpty()) {
for (String externalIp : accessMode.getExternalIps()) {
if (externalIp == null || externalIp.isEmpty()) {
continue;
}
MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder()
.domain(externalIp + ":80")
.protocol("http")
.build();
domains.add(domain);
}
}
}
// 2) NodePort: ips + ports → ip:nodePort
if (domains.isEmpty() && "NodePort".equalsIgnoreCase(accessMode.getAccessModeType())) {
List<String> ips = accessMode.getIps();
List<String> ports = accessMode.getPorts();
if (ips != null && !ips.isEmpty() && ports != null && !ports.isEmpty()) {
for (String ip : ips) {
if (ip == null || ip.isEmpty()) {
continue;
}
for (String portMapping : ports) {
if (portMapping == null || portMapping.isEmpty()) {
continue;
}
String[] parts = portMapping.split(":");
if (parts.length >= 2) {
String nodePort = parts[1].split("/")[0];
MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder()
.domain(ip + ":" + nodePort)
.protocol("http")
.build();
domains.add(domain);
}
}
}
}
}
// 3) fallback: only externalIps → :80
if (domains.isEmpty() && accessMode.getExternalIps() != null && !accessMode.getExternalIps().isEmpty()) {
for (String externalIp : accessMode.getExternalIps()) {
if (externalIp == null || externalIp.isEmpty()) {
continue;
}
MCPConfigResult.Domain domain = MCPConfigResult.Domain.builder()
.domain(externalIp + ":80")
.protocol("http")
.build();
domains.add(domain);
}
}
return domains;
}
@Override
public PageResult<GatewayResult> fetchGateways(Object param, int page, int size) {
// 将入参转换为配置
QueryApsaraGatewayParam p = (QueryApsaraGatewayParam) param;
ApsaraGatewayConfig cfg = new ApsaraGatewayConfig();
cfg.setRegionId(p.getRegionId());
cfg.setAccessKeyId(p.getAccessKeyId());
cfg.setAccessKeySecret(p.getAccessKeySecret());
cfg.setSecurityToken(p.getSecurityToken());
cfg.setDomain(p.getDomain());
cfg.setProduct(p.getProduct());
cfg.setVersion(p.getVersion());
cfg.setXAcsOrganizationId(p.getXAcsOrganizationId());
cfg.setXAcsCallerSdkSource(p.getXAcsCallerSdkSource());
cfg.setXAcsResourceGroupId(p.getXAcsResourceGroupId());
cfg.setXAcsCallerType(p.getXAcsCallerType());
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(cfg);
try {
// 使用SDK的ListInstances方法获取网关实例列表
// brokerEngineType默认为HIGRESS
String brokerEngineType = p.getBrokerEngineType() != null ? p.getBrokerEngineType() : "HIGRESS";
ListInstancesResponse response = client.ListInstances(page, size, brokerEngineType);
if (response.getBody() == null || response.getBody().getData() == null) {
return PageResult.of(new ArrayList<>(), page, size, 0);
}
ListInstancesResponseBody.ListInstancesResponseBodyData data =
response.getBody().getData();
int total = data.getTotal() != null ? data.getTotal() : 0;
List<GatewayResult> list = new ArrayList<>();
if (data.getRecords() != null) {
for (ListInstancesResponseBody.ListInstancesResponseBodyDataRecords record : data.getRecords()) {
GatewayResult gr = GatewayResult.builder()
.gatewayId(record.getGwInstanceId())
.gatewayName(record.getName())
.gatewayType(GatewayType.APSARA_GATEWAY)
.build();
list.add(gr);
}
}
return PageResult.of(list, page, size, total);
} catch (Exception e) {
log.error("Error listing Apsara gateways", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR, e.getMessage());
} finally {
client.close();
}
}
@Override
public String createConsumer(Consumer consumer, ConsumerCredential credential, GatewayConfig config) {
ApsaraGatewayConfig apsaraConfig = config.getApsaraGatewayConfig();
if (apsaraConfig == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Apsara Gateway配置缺失");
}
Gateway gateway = config.getGateway();
if (gateway == null || gateway.getGatewayId() == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失");
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(apsaraConfig);
try {
// 从凭证中获取API Key
String key = null;
if (credential.getApiKeyConfig() != null &&
credential.getApiKeyConfig().getCredentials() != null &&
!credential.getApiKeyConfig().getCredentials().isEmpty()) {
key = credential.getApiKeyConfig().getCredentials().get(0).getApiKey();
}
log.info("Creating consumer in Apsara gateway: gatewayId={}, consumerName={}, hasApiKey={}",
gateway.getGatewayId(), consumer.getName(), key != null);
CreateAppResponse response = client.CreateApp(
gateway.getGatewayId(),
consumer.getName(),
key,
5 // authType: 5 = API_KEY
);
// 检查响应
if (response.getBody() != null) {
log.info("CreateApp response: code={}, message={}, data={}",
response.getBody().getCode(),
response.getBody().getMsg(),
response.getBody().getData());
// 检查状态码
if (response.getBody().getCode() != null && response.getBody().getCode() == 200) {
if (response.getBody().getData() != null) {
// SDK返回的data就是appName,直接返回
return extractConsumerIdFromResponse(response.getBody().getData(), consumer.getName());
}
// 即使data为null,如果状态码是200,也认为创建成功,使用consumer name作为ID
log.warn("CreateApp succeeded but data is null, using consumer name as ID");
return consumer.getName();
}
// 状态码不是200,抛出详细错误信息
String errorMsg = String.format("Failed to create consumer in Apsara gateway: code=%d, message=%s",
response.getBody().getCode(), response.getBody().getMsg());
throw new BusinessException(ErrorCode.GATEWAY_ERROR, errorMsg);
}
throw new BusinessException(ErrorCode.GATEWAY_ERROR, "Failed to create consumer in Apsara gateway: empty response");
} catch (BusinessException e) {
log.error("Business error creating consumer in Apsara gateway", e);
throw e;
} catch (Exception e) {
log.error("Error creating consumer in Apsara gateway", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR,
"Error creating consumer in Apsara gateway: " + e.getMessage());
} finally {
client.close();
}
}
/**
* 从SDK响应中提取消费者ID
*/
private String extractConsumerIdFromResponse(Object data, String defaultConsumerId) {
if (data != null) {
if (data instanceof String) {
return (String) data;
}
return data.toString();
}
return defaultConsumerId;
}
@Override
public void updateConsumer(String consumerId, ConsumerCredential credential, GatewayConfig config) {
ApsaraGatewayConfig apsaraConfig = config.getApsaraGatewayConfig();
if (apsaraConfig == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Apsara Gateway配置缺失");
}
Gateway gateway = config.getGateway();
if (gateway == null || gateway.getGatewayId() == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失");
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(apsaraConfig);
try {
// 从凭证中提取API Key
String apiKey = null;
if (credential != null
&& credential.getApiKeyConfig() != null
&& credential.getApiKeyConfig().getCredentials() != null
&& !credential.getApiKeyConfig().getCredentials().isEmpty()) {
apiKey = credential.getApiKeyConfig().getCredentials().get(0).getApiKey();
}
// 明确各参数含义,避免重复使用consumerId带来的混淆
String appId = consumerId; // 应用ID,用于标识要更新的应用
String appName = consumerId; // 应用名称,通常与appId保持一致
String description = "Consumer managed by Portal"; // 语义化的描述信息
Integer authType = 5; // 认证类型:API_KEY
Boolean enable = true; // 启用状态
ModifyAppResponse response = client.ModifyApp(
gateway.getGatewayId(),
appId,
appName,
apiKey,
authType,
description,
enable
);
if (response.getBody() != null && response.getBody().getCode() == 200) {
log.info("Successfully updated consumer {} in Apsara gateway instance {}",
consumerId, gateway.getGatewayId());
return;
}
throw new BusinessException(ErrorCode.GATEWAY_ERROR,
"更新Apsara网关消费者失败");
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Error updating consumer {} in Apsara gateway instance {}",
consumerId, gateway != null ? gateway.getGatewayId() : "unknown", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR,
"更新Apsara网关消费者异常: " + e.getMessage());
} finally {
client.close();
}
}
@Override
public void deleteConsumer(String consumerId, GatewayConfig config) {
ApsaraGatewayConfig apsaraConfig = config.getApsaraGatewayConfig();
if (apsaraConfig == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Apsara Gateway配置缺失");
}
Gateway gateway = config.getGateway();
if (gateway == null || gateway.getGatewayId() == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "网关实例ID缺失");
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(apsaraConfig);
try {
BatchDeleteAppResponse response = client.DeleteApp(
gateway.getGatewayId(),
consumerId
);
if (response.getBody() != null && response.getBody().getCode() == 200) {
log.info("Successfully deleted consumer {} from Apsara gateway instance {}",
consumerId, gateway.getGatewayId());
return;
}
throw new BusinessException(ErrorCode.GATEWAY_ERROR,
"删除Apsara网关消费者失败");
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Error deleting consumer {} from Apsara gateway instance {}",
consumerId, gateway != null ? gateway.getGatewayId() : "unknown", e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR,
"删除Apsara网关消费者异常: " + e.getMessage());
} finally {
client.close();
}
}
@Override
public boolean isConsumerExists(String consumerId, GatewayConfig config) {
ApsaraGatewayConfig apsaraConfig = config.getApsaraGatewayConfig();
if (apsaraConfig == null) {
log.warn("Apsara Gateway配置缺失,无法检查消费者存在性");
return false;
}
Gateway gateway = config.getGateway();
if (gateway == null || gateway.getGatewayId() == null) {
log.warn("网关实例ID缺失,无法检查消费者存在性");
return false;
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(apsaraConfig);
try {
// 获取所有应用列表,然后在客户端筛选
ListAppsByGwInstanceIdResponse response = client.ListAppsByGwInstanceId(
gateway.getGatewayId(),
(Integer) null // serviceType 为 null,获取所有类型
);
// data字段直接是List,不是包含records的对象
if (response.getBody() != null && response.getBody().getData() != null) {
// 遍历应用列表,查找匹配的consumerId(appName)
return response.getBody().getData().stream()
.anyMatch(app -> consumerId.equals(app.getAppName()));
}
return false;
} catch (Exception e) {
log.warn("检查Apsara网关消费者存在性失败: consumerId={}", consumerId, e);
return false;
} finally {
client.close();
}
}
@Override
public ConsumerAuthConfig authorizeConsumer(Gateway gateway, String consumerId, Object refConfig) {
ApsaraGatewayConfig apsaraConfig = gateway.getApsaraGatewayConfig();
if (apsaraConfig == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Apsara Gateway配置缺失");
}
// 解析MCP Server配置
APIGRefConfig apigRefConfig = (APIGRefConfig) refConfig;
if (apigRefConfig == null || apigRefConfig.getMcpServerName() == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "MCP Server名称缺失");
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(apsaraConfig);
try {
// 调用SDK添加MCP Server消费者授权
AddMcpServerConsumersResponse response = client.AddMcpServerConsumers(
gateway.getGatewayId(),
apigRefConfig.getMcpServerName(),
Collections.singletonList(consumerId) // consumerId就是appName
);
if (response.getBody() != null && response.getBody().getCode() == 200) {
log.info("Successfully authorized consumer {} to MCP server {}",
consumerId, apigRefConfig.getMcpServerName());
// 构建授权配置返回结果
AdpAIAuthConfig authConfig = AdpAIAuthConfig.builder()
.mcpServerName(apigRefConfig.getMcpServerName())
.consumerId(consumerId)
.gwInstanceId(gateway.getGatewayId())
.build();
return ConsumerAuthConfig.builder()
.adpAIAuthConfig(authConfig)
.build();
}
throw new BusinessException(ErrorCode.GATEWAY_ERROR,
"Failed to authorize consumer to MCP server");
} catch (BusinessException e) {
log.error("Business error authorizing consumer to MCP server", e);
throw e;
} catch (Exception e) {
log.error("Error authorizing consumer {} to MCP server {}",
consumerId, apigRefConfig.getMcpServerName(), e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR,
"Error authorizing consumer to MCP server: " + e.getMessage());
} finally {
client.close();
}
}
@Override
public APIResult fetchAPI(Gateway gateway, String apiId) {
throw new UnsupportedOperationException("Apsara gateway not implemented for fetch api");
}
@Override
public void revokeConsumerAuthorization(Gateway gateway, String consumerId, ConsumerAuthConfig authConfig) {
AdpAIAuthConfig adpAIAuthConfig = authConfig.getAdpAIAuthConfig();
if (adpAIAuthConfig == null) {
log.warn("Apsara 授权配置为空,无法撤销授权");
return;
}
ApsaraGatewayConfig apsaraConfig = gateway.getApsaraGatewayConfig();
if (apsaraConfig == null) {
throw new BusinessException(ErrorCode.INVALID_PARAMETER, "Apsara Gateway配置缺失");
}
ApsaraStackGatewayClient client = new ApsaraStackGatewayClient(apsaraConfig);
try {
// 调用SDK删除MCP Server消费者授权
DeleteMcpServerConsumersResponse response = client.DeleteMcpServerConsumers(
gateway.getGatewayId(),
adpAIAuthConfig.getMcpServerName(),
Collections.singletonList(consumerId) // consumerId就是appName
);
if (response.getBody() != null && response.getBody().getCode() == 200) {
log.info("Successfully revoked consumer {} authorization from MCP server {}",
consumerId, adpAIAuthConfig.getMcpServerName());
return;
}
// 获取错误信息
String message = response.getBody() != null ? response.getBody().getMsg() : "Unknown error";
// 如果是资源不存在(已被删除),只记录警告,不抛异常
if (message != null && (message.contains("not found") || message.contains("不存在")
|| message.contains("NotFound"))) {
log.warn("Consumer authorization already removed or not found: consumerId={}, mcpServer={}, message={}",
consumerId, adpAIAuthConfig.getMcpServerName(), message);
return;
}
// 其他错误抛出异常
String errorMsg = "Failed to revoke consumer authorization from MCP server: " + message;
log.error(errorMsg);
throw new BusinessException(ErrorCode.GATEWAY_ERROR, errorMsg);
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("Error revoking consumer {} authorization from MCP server {}",
consumerId, adpAIAuthConfig.getMcpServerName(), e);
throw new BusinessException(ErrorCode.INTERNAL_ERROR,
"Error revoking consumer authorization: " + e.getMessage());
} finally {
client.close();
}
}
@Override
public GatewayType getGatewayType() {
return GatewayType.APSARA_GATEWAY;
}
@Override
public String getDashboard(Gateway gateway, String type) {
return null;
}
}
```