#
tokens: 46039/50000 8/349 files (page 6/7)
lines: off (toggle) GitHub
raw markdown copy
This is page 6 of 7. Use http://codebase.md/higress-group/himarket?lines=false&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
│                           │   ├── 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
│                               │   ├── 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
│   ├── 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
│                           │   │   │   └── 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
│                               ├── ConsumerService.java
│                               ├── DeveloperService.java
│                               ├── gateway
│                               │   ├── AdpAIGatewayOperator.java
│                               │   ├── AIGatewayOperator.java
│                               │   ├── APIGOperator.java
│                               │   ├── client
│                               │   │   ├── AdpAIGatewayClient.java
│                               │   │   ├── APIGClient.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-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-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-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 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 baseUrl = `${domain.protocol}://${domain.domain}`;
      let endpoint = `${baseUrl}${path}`;

      if (mcpConfig?.meta?.source === 'ADP_AI_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/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-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);
        // 订阅
        subscriptionRepository.deleteAllByConsumerId(consumerId);

        // 凭证
        credentialRepository.deleteAllByConsumerId(consumerId);

        // 删除网关上的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);
            }
        }

        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-web/api-portal-frontend/src/components/consumer/CredentialManager.tsx:
--------------------------------------------------------------------------------

```typescript
import {useState, useEffect} from "react";
import {
    Card,
    Button,
    message,
    Tabs,
    Modal,
    Radio,
    Input,
    Table,
    Popconfirm,
    Select,
    Form,
} from "antd";
import {
    PlusOutlined,
    InfoCircleOutlined,
    CopyOutlined,
    DeleteOutlined,
    EditOutlined
} from "@ant-design/icons";
import api from "../../lib/api";
import type {
    ConsumerCredentialResult,
    CreateCredentialParam,
    ConsumerCredential,
    HMACCredential,
    APIKeyCredential
} from "../../types/consumer";
import type {ApiResponse} from "../../types";

interface CredentialManagerProps {
    consumerId: string;
}

export function CredentialManager({consumerId}: CredentialManagerProps) {
    const [credentialType, setCredentialType] = useState<'API_KEY' | 'HMAC'>('API_KEY');
    const [credentialModalVisible, setCredentialModalVisible] = useState(false);
    const [credentialLoading, setCredentialLoading] = useState(false);

    const [sourceModalVisible, setSourceModalVisible] = useState(false);
    const [editingSource, setEditingSource] = useState<string>('Default');
    const [editingKey, setEditingKey] = useState<string>('Authorization');
    // 已保存(展示用)与编辑中的两套状态,取消时回滚到已保存值
    const [currentSource, setCurrentSource] = useState<string>('Default');
    const [currentKey, setCurrentKey] = useState<string>('Authorization');
    // 表单(编辑凭证来源)
    const [sourceForm] = Form.useForm();
    // 表单(创建凭证)
    const [credentialForm] = Form.useForm();
    // 当前完整配置(驱动表格数据源)
    const [currentConfig, setCurrentConfig] = useState<ConsumerCredentialResult | null>(null);

    // 初始化时获取当前配置
    const fetchCurrentConfig = async () => {
        try {
            const response: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
            if (response.code === "SUCCESS" && response.data) {
                const config = response.data;
                setCurrentConfig(config);
                if (config.apiKeyConfig) {
                    setCurrentSource(config.apiKeyConfig.source || 'Default');
                    setCurrentKey(config.apiKeyConfig.key || 'Authorization');
                }
            }
        } catch (error) {
            console.error('获取当前配置失败:', error);
        }
    };

    // 组件挂载时获取配置
    useEffect(() => {
        fetchCurrentConfig();
    }, [consumerId]);

    const handleCreateCredential = async () => {
        try {
            const values = await credentialForm.validateFields();
            setCredentialLoading(true);

            // 先获取当前的凭证配置
            const currentResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
            let currentConfig: ConsumerCredentialResult = {};

            if (currentResponse.code === "SUCCESS" && currentResponse.data) {
                currentConfig = currentResponse.data;
            }

            // 构建新的凭证配置
            const param: CreateCredentialParam = {
                ...currentConfig,
            };

            if (credentialType === 'API_KEY') {
                const newCredential: ConsumerCredential = {
                    apiKey: values.generationMethod === 'CUSTOM' ? values.customApiKey : generateRandomCredential('apiKey'),
                    mode: values.generationMethod
                };
                param.apiKeyConfig = {
                    ...currentConfig.apiKeyConfig,
                    credentials: [...(currentConfig.apiKeyConfig?.credentials || []), newCredential]
                };
            } else if (credentialType === 'HMAC') {
                const newCredential: ConsumerCredential = {
                    ak: values.generationMethod === 'CUSTOM' ? values.customAccessKey : generateRandomCredential('accessKey'),
                    sk: values.generationMethod === 'CUSTOM' ? values.customSecretKey : generateRandomCredential('secretKey'),
                    mode: values.generationMethod
                };
                param.hmacConfig = {
                    ...currentConfig.hmacConfig,
                    credentials: [...(currentConfig.hmacConfig?.credentials || []), newCredential]
                };
            }

            const response: ApiResponse<ConsumerCredentialResult> = await api.put(`/consumers/${consumerId}/credentials`, param);
            if (response?.code === "SUCCESS") {
                message.success('凭证添加成功');
                setCredentialModalVisible(false);
                resetCredentialForm();
                // 刷新当前配置以驱动表格
                await fetchCurrentConfig();
            }
        } catch (error) {
            console.error('创建凭证失败:', error);
            // message.error('创建凭证失败');
        } finally {
            setCredentialLoading(false);
        }
    };

    const handleDeleteCredential = async (credentialType: string, credential: ConsumerCredential) => {
        try {
            // 先获取当前的凭证配置
            const currentResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
            let currentConfig: ConsumerCredentialResult = {};

            if (currentResponse.code === "SUCCESS" && currentResponse.data) {
                currentConfig = currentResponse.data;
            }

            // 构建删除后的凭证配置,清空对应类型的凭证
            const param: CreateCredentialParam = {
                ...currentConfig,
            };

            if (credentialType === 'API_KEY') {
                param.apiKeyConfig = {
                    credentials: currentConfig.apiKeyConfig?.credentials?.filter(cred => cred.apiKey !== (credential as APIKeyCredential).apiKey),
                    source: currentConfig.apiKeyConfig?.source || 'Default',
                    key: currentConfig.apiKeyConfig?.key || 'Authorization'
                };
            } else if (credentialType === 'HMAC') {
                param.hmacConfig = {
                    credentials: currentConfig.hmacConfig?.credentials?.filter(cred => cred.ak !== (credential as HMACCredential).ak),
                };
            }

            const response: ApiResponse<ConsumerCredentialResult> = await api.put(`/consumers/${consumerId}/credentials`, param);
            if (response?.code === "SUCCESS") {
                message.success('凭证删除成功');
                await fetchCurrentConfig();
            }
        } catch (error) {
            console.error('删除凭证失败:', error);
            // message.error('删除凭证失败');
        }
    };
    const handleCopyCredential = (text: string) => {
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        textArea.style.left = '-9999px'; // 避免影响页面布局
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();

        try {
            const success = document.execCommand('copy');
            if (success) {
                message.success('已复制到剪贴板');
            } else {
                // message.error('复制失败,请手动复制内容');
            }
        } catch (err) {
            // message.error('复制失败,请手动复制内容');
        } finally {
            document.body.removeChild(textArea); // 清理 DOM
        }
    };


    const resetCredentialForm = () => {
        credentialForm.resetFields();
    };

    const handleEditSource = async (source: string, key: string) => {
        try {
            // 先获取当前的凭证配置
            const currentResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
            let currentConfig: ConsumerCredentialResult = {};

            if (currentResponse.code === "SUCCESS" && currentResponse.data) {
                currentConfig = currentResponse.data as ConsumerCredentialResult;
            }

            // 构建新的凭证配置
            const param: CreateCredentialParam = {};

            // 更新API Key配置的source和key
            if (currentConfig.apiKeyConfig) {
                param.apiKeyConfig = {
                    source: source,
                    key: source === 'Default' ? 'Authorization' : key,
                    credentials: currentConfig.apiKeyConfig.credentials
                };
            } else {
                param.apiKeyConfig = {
                    source: source,
                    key: source === 'Default' ? 'Authorization' : key,
                    credentials: []
                };
            }


            // 提交配置到后端
            const response: ApiResponse<ConsumerCredentialResult> = await api.put(`/consumers/${consumerId}/credentials`, param);
            if (response?.code === "SUCCESS") {
                message.success('凭证来源更新成功');

                // 重新查询接口获取最新配置,确保数据落盘
                const updatedResponse: ApiResponse<ConsumerCredentialResult> = await api.get(`/consumers/${consumerId}/credentials`);
                if (updatedResponse.code === "SUCCESS" && updatedResponse.data) {
                    const updatedConfig = updatedResponse.data;
                    if (updatedConfig.apiKeyConfig) {
                        setCurrentSource(updatedConfig.apiKeyConfig.source || 'Default');
                        setCurrentKey(updatedConfig.apiKeyConfig.key || 'Authorization');
                    }
                }

                setSourceModalVisible(false);
                await fetchCurrentConfig();
            }
        } catch (error) {
            console.error('更新凭证来源失败:', error);
            // message.error('更新凭证来源失败');
        }
    };

    const openSourceModal = () => {
        // 打开弹窗前将已保存值拷贝到编辑态和表单
        const initSource = currentSource;
        const initKey = initSource === 'Default' ? 'Authorization' : currentKey;
        setEditingSource(initSource);
        setEditingKey(initKey);
        sourceForm.setFieldsValue({source: initSource, key: initKey});
        setSourceModalVisible(true);
    };

    const openCredentialModal = () => {
        // 打开弹窗前重置表单并设置初始值
        credentialForm.resetFields();
        credentialForm.setFieldsValue({
            generationMethod: 'SYSTEM',
            customApiKey: '',
            customAccessKey: '',
            customSecretKey: ''
        });
        setCredentialModalVisible(true);
    };

    // 生成随机凭证
    const generateRandomCredential = (type: 'apiKey' | 'accessKey' | 'secretKey'): string => {
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';

        if (type === 'apiKey') {
            // 生成32位API Key
            const apiKey = Array.from({length: 32}, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');

            // 确保表单字段已经渲染并设置值
            const setValue = () => {
                try {
                    credentialForm.setFieldsValue({customApiKey: apiKey});
                } catch (error) {
                    console.error('设置API Key失败:', error);
                }
            };

            // 如果表单已经渲染,立即设置;否则延迟设置
            if (credentialForm.getFieldValue('customApiKey') !== undefined) {
                setValue();
            } else {
                setTimeout(setValue, 100);
            }

            return apiKey;
        } else {
            // 生成32位Access Key和64位Secret Key
            const ak = Array.from({length: 32}, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');
            const sk = Array.from({length: 64}, () => chars.charAt(Math.floor(Math.random() * chars.length))).join('');

            // 确保表单字段已经渲染并设置值
            const setValue = () => {
                try {
                    credentialForm.setFieldsValue({
                        customAccessKey: ak,
                        customSecretKey: sk
                    });
                } catch (error) {
                    console.error('设置AK/SK失败:', error);
                }
            };

            // 如果表单已经渲染,立即设置;否则延迟设置
            if (credentialForm.getFieldValue('customAccessKey') !== undefined) {
                setValue();
            } else {
                setTimeout(setValue, 100);
            }

            // 根据类型返回对应的值
            return type === 'accessKey' ? ak : sk;
        }
    };

    // API Key 列
    const apiKeyColumns = [
        {
            title: 'API Key',
            dataIndex: 'apiKey',
            key: 'apiKey',
            render: (apiKey: string) => (
                <div className="flex items-center space-x-2">
                    <code className="text-sm bg-gray-100 px-2 py-1 rounded">{apiKey}</code>
                    <Button type="text" size="small" icon={<CopyOutlined/>}
                            onClick={() => handleCopyCredential(apiKey)}/>
                </div>
            ),
        },
        {
            title: '操作',
            key: 'action',
            render: (record: ConsumerCredential) => (
                <Popconfirm title="确定要删除该API Key凭证吗?"
                            onConfirm={() => handleDeleteCredential('API_KEY', record)}>
                    <Button type="link" danger size="small" icon={<DeleteOutlined/>}>删除</Button>
                </Popconfirm>
            ),
        },
    ];

    // 脱敏函数
    const maskSecretKey = (secretKey: string): string => {
        if (!secretKey || secretKey.length < 8) return secretKey;
        return secretKey.substring(0, 4) + '*'.repeat(secretKey.length - 8) + secretKey.substring(secretKey.length - 4);
    };

    // HMAC 列
    const hmacColumns = [
        {
            title: 'Access Key',
            dataIndex: 'ak',
            key: 'ak',
            render: (ak: string) => (
                <div className="flex items-center space-x-2">
                    <code className="text-sm bg-gray-100 px-2 py-1 rounded">{ak}</code>
                    <Button type="text" size="small" icon={<CopyOutlined/>} onClick={() => handleCopyCredential(ak)}/>
                </div>
            ),
        },
        {
            title: 'Secret Key',
            dataIndex: 'sk',
            key: 'sk',
            render: (sk: string) => (
                <div className="flex items-center space-x-2">
                    <code className="text-sm bg-gray-100 px-2 py-1 rounded">{maskSecretKey(sk)}</code>
                    <Button type="text" size="small" icon={<CopyOutlined/>} onClick={() => handleCopyCredential(sk)}/>
                </div>
            ),
        },
        {
            title: '操作',
            key: 'action',
            render: (record: ConsumerCredential) => (
                <Popconfirm title="确定要删除该AK/SK凭证吗?" onConfirm={() => handleDeleteCredential('HMAC', record)}>
                    <Button type="link" danger size="small" icon={<DeleteOutlined/>}>删除</Button>
                </Popconfirm>
            ),
        },
    ];

    return (
        <>
            <Card title="认证方式">
                <Tabs defaultActiveKey="API_KEY">
                    <Tabs.TabPane tab="API Key" key="API_KEY">
                        <div className="mb-4">
                            <div className="flex items-start space-x-2 mb-4">
                                <InfoCircleOutlined className="text-blue-500 mt-1"/>
                                <div className="text-sm text-gray-600">
                                    API Key是一种简单的认证方式,客户端需要在请求中添加凭证,网关会验证API Key的合法性和权限。
                                    API Key常用于简单场景,不涉及敏感操作,安全性相对较低,请注意凭证的管理与保护。
                                </div>
                            </div>

                            {/* 凭证来源配置(展示已保存值)*/}
                            <div className="mb-4 p-3 bg-gray-50 rounded border">
                                <div className="flex items-center gap-2 mb-2">
                                    <span className="text-sm font-medium text-gray-700">凭证来源</span>
                                    <Button type="link" size="small" icon={<EditOutlined/>} onClick={openSourceModal}>
                                        编辑
                                    </Button>
                                </div>
                                {/* <div className="text-sm text-gray-600">
                  {currentSource === 'Default' ? '' : `${currentSource}`}
                </div> */}
                                <div className="text-sm text-gray-600">
                                    {currentSource === 'Default' ? 'Authorization: Bearer <token>' : `${currentSource}:${currentKey}`}
                                </div>
                            </div>

                            <Button
                                type="primary"
                                icon={<PlusOutlined/>}
                                onClick={() => {
                                    setCredentialType('API_KEY');
                                    openCredentialModal();
                                }}
                            >
                                添加凭证
                            </Button>
                        </div>
                        <Table
                            columns={apiKeyColumns}
                            dataSource={currentConfig?.apiKeyConfig?.credentials || []}
                            rowKey={(record) => record.apiKey || Math.random().toString()}
                            pagination={false}
                            size="small"
                            locale={{emptyText: '暂无API Key凭证,请点击上方按钮创建'}}
                        />
                    </Tabs.TabPane>

                    <Tabs.TabPane tab="HMAC" key="HMAC">
                        <div className="mb-4">
                            <div className="flex items-start space-x-2 mb-4">
                                <InfoCircleOutlined className="text-blue-500 mt-1"/>
                                <div className="text-sm text-gray-600">
                                    一种基于HMAC算法的AK/SK签名认证方式。客户端在调用API时,需要使用签名密钥对请求内容进行签名计算,
                                    并将签名同步传输给服务器端进行签名验证。
                                </div>
                            </div>
                            <Button
                                type="primary"
                                icon={<PlusOutlined/>}
                                onClick={() => {
                                    setCredentialType('HMAC');
                                    openCredentialModal();
                                }}
                            >
                                添加AK/SK
                            </Button>
                        </div>
                        <Table
                            columns={hmacColumns}
                            dataSource={currentConfig?.hmacConfig?.credentials || []}
                            rowKey={(record) => record.ak || record.sk || Math.random().toString()}
                            pagination={false}
                            size="small"
                            locale={{emptyText: '暂无AK/SK凭证,请点击上方按钮创建'}}
                        />
                    </Tabs.TabPane>

                    <Tabs.TabPane tab="JWT" key="JWT" disabled>
                        <div className="text-center py-8 text-gray-500">
                            JWT功能暂未开放
                        </div>
                    </Tabs.TabPane>
                </Tabs>
            </Card>

            {/* 创建凭证模态框 */}
            <Modal
                title={`添加 ${credentialType === 'API_KEY' ? 'API Key' : 'AK/SK'}`}
                open={credentialModalVisible}
                onCancel={() => {
                    setCredentialModalVisible(false);
                    resetCredentialForm();
                }}
                onOk={handleCreateCredential}
                confirmLoading={credentialLoading}
                okText="添加"
                cancelText="取消"
            >
                <Form form={credentialForm} initialValues={{
                    generationMethod: 'SYSTEM',
                    customApiKey: '',
                    customAccessKey: '',
                    customSecretKey: ''
                }}>
                    <div className="mb-4">
                        <div className="mb-2">
                            <span className="text-red-500 mr-1">*</span>
                            <span>生成方式</span>
                        </div>
                        <Form.Item
                            name="generationMethod"
                            rules={[{required: true, message: '请选择生成方式'}]}
                            className="mb-0"
                        >
                            <Radio.Group>
                                <Radio value="SYSTEM">系统生成</Radio>
                                <Radio value="CUSTOM">自定义</Radio>
                            </Radio.Group>
                        </Form.Item>
                    </div>

                    <Form.Item noStyle shouldUpdate={(prev, curr) => prev.generationMethod !== curr.generationMethod}>
                        {({getFieldValue}) => {
                            const method = getFieldValue('generationMethod');
                            if (method === 'CUSTOM') {
                                return (
                                    <>
                                        {credentialType === 'API_KEY' && (
                                            <div className="mb-4">
                                                <div className="mb-2">
                                                    <span className="text-red-500 mr-1">*</span>
                                                    <span>凭证</span>
                                                </div>
                                                <Form.Item
                                                    name="customApiKey"
                                                    rules={[
                                                        {required: true, message: '请输入自定义API Key'},
                                                        {
                                                            pattern: /^[A-Za-z0-9_-]+$/,
                                                            message: '支持英文、数字、下划线(_)和短横线(-)'
                                                        },
                                                        {min: 8, message: 'API Key长度至少8个字符'},
                                                        {max: 128, message: 'API Key长度不能超过128个字符'}
                                                    ]}
                                                    className="mb-2"
                                                >
                                                    <Input placeholder="请输入凭证" maxLength={128}/>
                                                </Form.Item>
                                                <div className="text-xs text-gray-500">
                                                    长度为8-128个字符,可包含英文、数字、下划线(_)和短横线(-)
                                                </div>
                                            </div>
                                        )}
                                        {credentialType === 'HMAC' && (
                                            <>
                                                <div className="mb-4">
                                                    <div className="mb-2">
                                                        <span className="text-red-500 mr-1">*</span>
                                                        <span>Access Key</span>
                                                    </div>
                                                    <Form.Item
                                                        name="customAccessKey"
                                                        rules={[
                                                            {required: true, message: '请输入自定义Access Key'},
                                                            {
                                                                pattern: /^[A-Za-z0-9_-]+$/,
                                                                message: '支持英文、数字、下划线(_)和短横线(-)'
                                                            },
                                                            {min: 8, message: 'Access Key长度至少8个字符'},
                                                            {max: 128, message: 'Access Key长度不能超过128个字符'}
                                                        ]}
                                                        className="mb-2"
                                                    >
                                                        <Input placeholder="请输入Access Key" maxLength={128}/>
                                                    </Form.Item>
                                                    <div className="text-xs text-gray-500">
                                                        长度为8-128个字符,可包含英文、数字、下划线(_)和短横线(-)
                                                    </div>
                                                </div>
                                                <div className="mb-4">
                                                    <div className="mb-2">
                                                        <span className="text-red-500 mr-1">*</span>
                                                        <span>Secret Key</span>
                                                    </div>
                                                    <Form.Item
                                                        name="customSecretKey"
                                                        rules={[
                                                            {required: true, message: '请输入自定义Secret Key'},
                                                            {
                                                                pattern: /^[A-Za-z0-9_-]+$/,
                                                                message: '支持英文、数字、下划线(_)和短横线(-)'
                                                            },
                                                            {min: 8, message: 'Secret Key长度至少8个字符'},
                                                            {max: 128, message: 'Secret Key长度不能超过128个字符'}
                                                        ]}
                                                        className="mb-2"
                                                    >
                                                        <Input placeholder="请输入 Secret Key" maxLength={128}/>
                                                    </Form.Item>
                                                    <div className="text-xs text-gray-500">
                                                        长度为8-128个字符,可包含英文、数字、下划线(_)和短横线(-)
                                                    </div>
                                                </div>
                                            </>
                                        )}
                                    </>
                                );
                            } else if (method === 'SYSTEM') {
                                return (
                                    <div>
                                        <div className="flex items-center gap-2 text-sm text-gray-500">
                                            <InfoCircleOutlined/>
                                            <span>系统将自动生成符合规范的凭证</span>
                                        </div>
                                    </div>
                                );
                            }
                            return null;
                        }}
                    </Form.Item>
                </Form>
            </Modal>

            {/* 编辑凭证来源模态框 */}
            <Modal
                title="编辑凭证来源"
                open={sourceModalVisible}
                onCancel={() => {
                    // 取消不落盘,回退到已保存值并重置表单
                    const initSource = currentSource;
                    const initKey = initSource === 'Default' ? 'Authorization' : currentKey;
                    setEditingSource(initSource);
                    setEditingKey(initKey);
                    sourceForm.resetFields();
                    setSourceModalVisible(false);
                }}
                onOk={async () => {
                    try {
                        const values = await sourceForm.validateFields();
                        setEditingSource(values.source);
                        setEditingKey(values.key);
                        await handleEditSource(values.source, values.key);
                    } catch {
                        // 校验失败,不提交
                    }
                }}
                okText="保存"
                cancelText="取消"
            >
                <Form form={sourceForm} layout="vertical" initialValues={{source: editingSource, key: editingKey}}>
                    <Form.Item
                        label="凭证来源"
                        name="source"
                        rules={[{required: true, message: '请选择凭证来源'}]}
                    >
                        <Select
                            onChange={(value) => {
                                const nextKey = value === 'Default' ? 'Authorization' : '';
                                sourceForm.setFieldsValue({key: nextKey});
                            }}
                            style={{width: '100%'}}
                        >
                            <Select.Option value="Header">Header</Select.Option>
                            <Select.Option value="QueryString">QueryString</Select.Option>
                            <Select.Option value="Default">默认</Select.Option>
                        </Select>
                    </Form.Item>

                    <Form.Item noStyle shouldUpdate={(prev, curr) => prev.source !== curr.source}>
                        {({getFieldValue}) =>
                            getFieldValue('source') !== 'Default' ? (
                                <Form.Item
                                    label="键名"
                                    name="key"
                                    rules={[
                                        {
                                            required: true,
                                            message: '请输入键名',
                                        },
                                        {
                                            pattern: /^[A-Za-z0-9-_]+$/,
                                            message: '仅支持字母/数字/-/_',
                                        },
                                    ]}
                                >
                                    <Input placeholder="请输入键名"/>
                                </Form.Item>
                            ) : null
                        }
                    </Form.Item>
                    {/*
          <div className="text-sm text-gray-500">
            <div>说明:</div>
            <div>• Header: 凭证放在HTTP请求头中</div>
            <div>• QueryString: 凭证放在URL查询参数中</div>
            <div>• Default: 使用标准的Authorization头</div>
          </div> */}
                </Form>
            </Modal>
        </>
    );
} 
```

--------------------------------------------------------------------------------
/portal-web/api-portal-admin/src/components/api-product/ApiProductLinkApi.tsx:
--------------------------------------------------------------------------------

```typescript
import { Card, Button, Modal, Form, Select, message, Collapse, Tabs, Row, Col } from 'antd'
import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, CopyOutlined } from '@ant-design/icons'
import { useState, useEffect } from 'react'
import type { ApiProduct, LinkedService, RestAPIItem, HigressMCPItem, NacosMCPItem, APIGAIMCPItem, ApiItem } from '@/types/api-product'
import type { Gateway, NacosInstance } from '@/types/gateway'
import { apiProductApi, gatewayApi, nacosApi } from '@/lib/api'
import { getGatewayTypeLabel } from '@/lib/constant'
import { copyToClipboard } from '@/lib/utils'
import * as yaml from 'js-yaml'
import { SwaggerUIWrapper } from './SwaggerUIWrapper'

interface ApiProductLinkApiProps {
  apiProduct: ApiProduct
  linkedService: LinkedService | null
  onLinkedServiceUpdate: (linkedService: LinkedService | null) => void
  handleRefresh: () => void
}

export function ApiProductLinkApi({ apiProduct, linkedService, onLinkedServiceUpdate, handleRefresh }: ApiProductLinkApiProps) {
  // 移除了内部的 linkedService 状态,现在从 props 接收
  const [isModalVisible, setIsModalVisible] = useState(false)
  const [form] = Form.useForm()
  const [gateways, setGateways] = useState<Gateway[]>([])
  const [nacosInstances, setNacosInstances] = useState<NacosInstance[]>([])
  const [gatewayLoading, setGatewayLoading] = useState(false)
  const [nacosLoading, setNacosLoading] = useState(false)
  const [selectedGateway, setSelectedGateway] = useState<Gateway | null>(null)
  const [selectedNacos, setSelectedNacos] = useState<NacosInstance | null>(null)
  const [nacosNamespaces, setNacosNamespaces] = useState<any[]>([])
  const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null)
  const [apiList, setApiList] = useState<ApiItem[] | NacosMCPItem[]>([])
  const [apiLoading, setApiLoading] = useState(false)
  const [sourceType, setSourceType] = useState<'GATEWAY' | 'NACOS'>('GATEWAY')
  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('')

  useEffect(() => {    
    fetchGateways()
    fetchNacosInstances()
  }, [])

  // 解析MCP tools配置
  useEffect(() => {
    if (apiProduct.type === 'MCP_SERVER' && apiProduct.mcpConfig?.tools) {
      const parsedConfig = parseYamlConfig(apiProduct.mcpConfig.tools)
      if (parsedConfig && parsedConfig.tools && Array.isArray(parsedConfig.tools)) {
        setParsedTools(parsedConfig.tools)
      } else {
        // 如果tools字段存在但是空数组,也设置为空数组
        setParsedTools([])
      }
    } else {
      setParsedTools([])
    }
  }, [apiProduct])

  // 生成连接配置
  useEffect(() => {
    if (apiProduct.type === 'MCP_SERVER' && apiProduct.mcpConfig) {
      generateConnectionConfig(
        apiProduct.mcpConfig.mcpServerConfig.domains,
        apiProduct.mcpConfig.mcpServerConfig.path,
        apiProduct.mcpConfig.mcpServerName,
        apiProduct.mcpConfig.mcpServerConfig.rawConfig,
        apiProduct.mcpConfig.meta?.protocol
      )
    }
  }, [apiProduct])

  // 解析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.error('YAML解析失败:', error)
      return null
    }
  }

  // 生成连接配置
  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 fullUrl = `${domain.protocol}://${domain.domain}${path || '/'}`

      if (protocolType === 'SSE') {
        // 仅生成SSE配置,不追加/sse
        const sseConfig = {
          mcpServers: {
            [serverName]: {
              type: "sse",
              url: fullUrl
            }
          }
        }
        setSseJson(JSON.stringify(sseConfig, null, 2))
        setHttpJson("")
        setLocalJson("")
        return;
      } else if (protocolType === 'StreamableHTTP') {
        // 仅生成HTTP配置
        const httpConfig = {
          mcpServers: {
            [serverName]: {
              url: fullUrl
            }
          }
        }
        setHttpJson(JSON.stringify(httpConfig, null, 2))
        setSseJson("")
        setLocalJson("")
        return;
      } else {
        // protocol为null或其他值:生成两种配置
        const sseConfig = {
          mcpServers: {
            [serverName]: {
              type: "sse",
              url: `${fullUrl}/sse`
            }
          }
        }

        const httpConfig = {
          mcpServers: {
            [serverName]: {
              url: fullUrl
            }
          }
        }

        setSseJson(JSON.stringify(sseConfig, null, 2))
        setHttpJson(JSON.stringify(httpConfig, null, 2))
        setLocalJson("")
        return;
      }
    }

    // 无有效配置
    setHttpJson("");
    setSseJson("");
    setLocalJson("");
  }

  const handleCopy = async (text: string) => {
    try {
      await copyToClipboard(text);
      message.success("已复制到剪贴板");
    } catch {
      message.error("复制失败,请手动复制");
    }
  }

  const fetchGateways = async () => {
    setGatewayLoading(true)
    try {
      const res = await gatewayApi.getGateways()
      const result = apiProduct.type === 'REST_API' ?
       res.data?.content?.filter?.((item: Gateway) => item.gatewayType === 'APIG_API') :
       res.data?.content?.filter?.((item: Gateway) => item.gatewayType === 'HIGRESS' || item.gatewayType === 'APIG_AI' || item.gatewayType === 'ADP_AI_GATEWAY')
      setGateways(result || [])
    } catch (error) {
      console.error('获取网关列表失败:', error)
    } finally {
      setGatewayLoading(false)
    }
  }

  const fetchNacosInstances = async () => {
    setNacosLoading(true)
    try {
      const res = await nacosApi.getNacos({
        page: 1,
        size: 1000 // 获取所有 Nacos 实例
      })
      setNacosInstances(res.data.content || [])
    } catch (error) {
      console.error('获取Nacos实例列表失败:', error)
    } finally {
      setNacosLoading(false)
    }
  }

  const handleSourceTypeChange = (value: 'GATEWAY' | 'NACOS') => {
    setSourceType(value)
  setSelectedGateway(null)
  setSelectedNacos(null)
  setSelectedNamespace(null)
  setNacosNamespaces([])
    setApiList([])
    form.setFieldsValue({
      gatewayId: undefined,
      nacosId: undefined,
      apiId: undefined
    })
  }

  const handleGatewayChange = async (gatewayId: string) => {
    const gateway = gateways.find(g => g.gatewayId === gatewayId)
    setSelectedGateway(gateway || null)
    
    if (!gateway) return

    setApiLoading(true)
    try {
      if (gateway.gatewayType === 'APIG_API') {
        // APIG_API类型:获取REST API列表
        const restRes = await gatewayApi.getGatewayRestApis(gatewayId, {})
        const restApis = (restRes.data?.content || []).map((api: any) => ({
          apiId: api.apiId,
          apiName: api.apiName,
          type: 'REST API'
        }))
        setApiList(restApis)
      } else if (gateway.gatewayType === 'HIGRESS') {
        // HIGRESS类型:获取MCP Server列表
        const res = await gatewayApi.getGatewayMcpServers(gatewayId, {
          page: 1,
          size: 1000 // 获取所有MCP Server
        })
        const mcpServers = (res.data?.content || []).map((api: any) => ({
          mcpServerName: api.mcpServerName,
          fromGatewayType: 'HIGRESS' as const,
          type: 'MCP Server'
        }))
        setApiList(mcpServers)
      } else if (gateway.gatewayType === 'APIG_AI') {
        // APIG_AI类型:获取MCP Server列表
        const res = await gatewayApi.getGatewayMcpServers(gatewayId, {
          page: 1,
          size: 500 // 获取所有MCP Server
        })
        const mcpServers = (res.data?.content || []).map((api: any) => ({
          mcpServerName: api.mcpServerName,
          fromGatewayType: 'APIG_AI' as const,
          mcpRouteId: api.mcpRouteId,
          apiId: api.apiId,
          mcpServerId: api.mcpServerId,
          type: 'MCP Server'
        }))
        setApiList(mcpServers)
      } else if (gateway.gatewayType === 'ADP_AI_GATEWAY') {
        // ADP_AI_GATEWAY类型:获取MCP Server列表
        const res = await gatewayApi.getGatewayMcpServers(gatewayId, {
          page: 1,
          size: 500 // 获取所有MCP Server
        })
        const mcpServers = (res.data?.content || []).map((api: any) => ({
          mcpServerName: api.mcpServerName || api.name,
          fromGatewayType: 'ADP_AI_GATEWAY' as const,
          mcpRouteId: api.mcpRouteId,
          mcpServerId: api.mcpServerId,
          type: 'MCP Server'
        }))
        setApiList(mcpServers)
      }
    } catch (error) {
    } finally {
      setApiLoading(false)
    }
  }

  const handleNacosChange = async (nacosId: string) => {
    const nacos = nacosInstances.find(n => n.nacosId === nacosId)
    setSelectedNacos(nacos || null)
    setSelectedNamespace(null)
    setApiList([])
    setNacosNamespaces([])
    if (!nacos) return

    // 获取命名空间列表
    try {
      const nsRes = await nacosApi.getNamespaces(nacosId, { page: 1, size: 1000 })
      const namespaces = (nsRes.data?.content || []).map((ns: any) => ({
        namespaceId: ns.namespaceId,
        namespaceName: ns.namespaceName || ns.namespaceId,
        namespaceDesc: ns.namespaceDesc
      }))
      setNacosNamespaces(namespaces)
    } catch (e) {
      console.error('获取命名空间失败', e)
    }
  }

  const handleNamespaceChange = async (namespaceId: string) => {
    setSelectedNamespace(namespaceId)
    setApiLoading(true)
    try {
      if (!selectedNacos) return
      const res = await nacosApi.getNacosMcpServers(selectedNacos.nacosId, {
        page: 1,
        size: 1000,
        namespaceId
      })
      const mcpServers = (res.data?.content || []).map((api: any) => ({
        mcpServerName: api.mcpServerName,
        fromGatewayType: 'NACOS' as const,
        type: `MCP Server (${namespaceId})`
      }))
      setApiList(mcpServers)
    } catch (e) {
      console.error('获取Nacos MCP Server列表失败:', e)
    } finally {
      setApiLoading(false)
    }
  }


  const handleModalOk = () => {
    form.validateFields().then((values) => {
      const { sourceType, gatewayId, nacosId, apiId } = values
      const selectedApi = apiList.find(item => {
        if ('apiId' in item) {
          // mcp server 会返回apiId和mcpRouteId,此时mcpRouteId为唯一值,apiId不是
          if ('mcpRouteId' in item) {
            return item.mcpRouteId === apiId
          } else {
            return item.apiId === apiId
          }
        } else if ('mcpServerName' in item) {
          return item.mcpServerName === apiId
        }
        return false
      })
      const newService: LinkedService = {
        gatewayId: sourceType === 'GATEWAY' ? gatewayId : undefined, // 对于 Nacos,使用 nacosId 作为 gatewayId
        nacosId: sourceType === 'NACOS' ? nacosId : undefined,
        sourceType,
        productId: apiProduct.productId,
        apigRefConfig: selectedApi && 'apiId' in selectedApi ? selectedApi as RestAPIItem | APIGAIMCPItem : undefined,
        higressRefConfig: selectedApi && 'mcpServerName' in selectedApi && 'fromGatewayType' in selectedApi && selectedApi.fromGatewayType === 'HIGRESS' ? selectedApi as HigressMCPItem : undefined,
        nacosRefConfig: sourceType === 'NACOS' && selectedApi && 'fromGatewayType' in selectedApi && selectedApi.fromGatewayType === 'NACOS' ? {
          ...selectedApi,
          namespaceId: selectedNamespace || 'public'
        } : undefined,
        adpAIGatewayRefConfig: selectedApi && 'fromGatewayType' in selectedApi && selectedApi.fromGatewayType === 'ADP_AI_GATEWAY' ? selectedApi as APIGAIMCPItem : undefined,
      }
      apiProductApi.createApiProductRef(apiProduct.productId, newService).then(async () => {
        message.success('关联成功')
        setIsModalVisible(false)
        
        // 重新获取关联信息并更新
        try {
          const res = await apiProductApi.getApiProductRef(apiProduct.productId)
          onLinkedServiceUpdate(res.data || null)
        } catch (error) {
          console.error('获取关联API失败:', error)
          onLinkedServiceUpdate(null)
        }
        
        // 重新获取产品详情(特别重要,因为关联API后apiProduct.apiConfig可能会更新)
        handleRefresh()
        
        form.resetFields()
        setSelectedGateway(null)
        setSelectedNacos(null)
        setApiList([])
        setSourceType('GATEWAY')
      }).catch(() => {
        message.error('关联失败')
      })
    })
  }

  const handleModalCancel = () => {
    setIsModalVisible(false)
    form.resetFields()
    setSelectedGateway(null)
    setSelectedNacos(null)
    setApiList([])
    setSourceType('GATEWAY')
  }


  const handleDelete = () => {
    if (!linkedService) return

    Modal.confirm({
      title: '确认解除关联',
      content: '确定要解除与当前API的关联吗?',
      icon: <ExclamationCircleOutlined />,
      onOk() {
        return apiProductApi.deleteApiProductRef(apiProduct.productId).then(() => {
          message.success('解除关联成功')
          onLinkedServiceUpdate(null)
          // 重新获取产品详情(解除关联后apiProduct.apiConfig可能会更新)
          handleRefresh()
        }).catch(() => {
          message.error('解除关联失败')
        })
      }
    })
  }

  const getServiceInfo = () => {
    if (!linkedService) return null

    let apiName = ''
    let apiType = ''
    let sourceInfo = ''
    let gatewayInfo = ''

    // 首先根据 Product 的 type 确定基本类型
    if (apiProduct.type === 'REST_API') {
      // REST API 类型产品 - 只能关联 API 网关上的 REST API
      if (linkedService.sourceType === 'GATEWAY' && linkedService.apigRefConfig && 'apiName' in linkedService.apigRefConfig) {
        apiName = linkedService.apigRefConfig.apiName || '未命名'
        apiType = 'REST API'
        sourceInfo = 'API网关'
        gatewayInfo = linkedService.gatewayId || '未知'
      }
    } else if (apiProduct.type === 'MCP_SERVER') {
      // MCP Server 类型产品 - 可以关联多种平台上的 MCP Server
      apiType = 'MCP Server'
      
      if (linkedService.sourceType === 'GATEWAY' && linkedService.apigRefConfig && 'mcpServerName' in linkedService.apigRefConfig) {
        // AI网关上的MCP Server
        apiName = linkedService.apigRefConfig.mcpServerName || '未命名'
        sourceInfo = 'AI网关'
        gatewayInfo = linkedService.gatewayId || '未知'
      } else if (linkedService.sourceType === 'GATEWAY' && linkedService.higressRefConfig) {
        // Higress网关上的MCP Server
        apiName = linkedService.higressRefConfig.mcpServerName || '未命名'
        sourceInfo = 'Higress网关'
        gatewayInfo = linkedService.gatewayId || '未知'
      } else if (linkedService.sourceType === 'GATEWAY' && linkedService.adpAIGatewayRefConfig) {
        // 专有云AI网关上的MCP Server
        apiName = linkedService.adpAIGatewayRefConfig.mcpServerName || '未命名'
        sourceInfo = '专有云AI网关'
        gatewayInfo = linkedService.gatewayId || '未知'
      } else if (linkedService.sourceType === 'NACOS' && linkedService.nacosRefConfig) {
        // Nacos上的MCP Server
        apiName = linkedService.nacosRefConfig.mcpServerName || '未命名'
        sourceInfo = 'Nacos服务发现'
        gatewayInfo = linkedService.nacosId || '未知'
      }
    }

    return {
      apiName,
      apiType,
      sourceInfo,
      gatewayInfo
    }
  }

  const renderLinkInfo = () => {
    const serviceInfo = getServiceInfo()
    
    // 没有关联任何API
    if (!linkedService || !serviceInfo) {
      return (
        <Card className="mb-6">
          <div className="text-center py-8">
            <div className="text-gray-500 mb-4">暂未关联任何API</div>
            <Button type="primary" icon={<PlusOutlined />} onClick={() => setIsModalVisible(true)}>
              关联API
            </Button>
          </div>
        </Card>
      )
    }

    return (
      <Card 
        className="mb-6"
        title="关联详情"
        extra={
          <Button type="primary" danger icon={<DeleteOutlined />} onClick={handleDelete}>
            解除关联
          </Button>
        }
      >
        <div>
          {/* 第一行:名称 + 类型 */}
          <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
            <span className="text-xs text-gray-600">名称:</span>
            <span className="col-span-2 text-xs text-gray-900">{serviceInfo.apiName || '未命名'}</span>
            <span className="text-xs text-gray-600">类型:</span>
            <span className="col-span-2 text-xs text-gray-900">{serviceInfo.apiType}</span>
          </div>
          
          {/* 第二行:来源 + ID */}
          <div className="grid grid-cols-6 gap-8 items-center pt-2 pb-2">
            <span className="text-xs text-gray-600">来源:</span>
            <span className="col-span-2 text-xs text-gray-900">{serviceInfo.sourceInfo}</span>
            <span className="text-xs text-gray-600">
              {linkedService?.sourceType === 'NACOS' ? 'Nacos ID:' : '网关ID:'}
            </span>
            <span className="col-span-2 text-xs text-gray-700">{serviceInfo.gatewayInfo}</span>
          </div>
        </div>
      </Card>
    )
  }

  const renderApiConfig = () => {
    const isMcp = apiProduct.type === 'MCP_SERVER'
    const isOpenApi = apiProduct.type === 'REST_API'

    // MCP Server类型:无论是否有linkedService都显示tools和连接点配置  
    if (isMcp && apiProduct.mcpConfig) {
      return (
        <Card title="配置详情">
          <Row gutter={24}>
            {/* 左侧:工具列表 */}
            <Col span={15}>
              <Card>
                <Tabs
                  defaultActiveKey="tools"
                  items={[
                    {
                      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 mb-2"
                                                defaultValue={arg.default !== undefined ? JSON.stringify(arg.default) : ''}
                                              />
                                              {arg.enum && (
                                                <div className="text-xs text-gray-500">
                                                  可选值: {arg.enum.map(value => <code key={value} className="mr-1">{value}</code>)}
                                                </div>
                                              )}
                                            </div>
                                          ))}
                                        </div>
                                      )}
                                    </div>
                                  )
                                }]}
                              />
                            </div>
                          ))}
                        </div>
                      ) : (
                        <div className="text-gray-500 text-center py-8">
                          No tools available
                        </div>
                      ),
                    },
                  ]}
                />
              </Card>
            </Col>

            {/* 右侧:连接点配置 */}
            <Col span={9}>
              <Card>
                <div className="mb-4">
                  <h3 className="text-sm font-semibold mb-3">连接点配置</h3>
                  <Tabs
                    size="small" 
                    defaultActiveKey={localJson ? "local" : (sseJson ? "sse" : "http")}
                    items={(() => {
                      const tabs = [];
                      
                      if (localJson) {
                        tabs.push({
                          key: "local",
                          label: "Stdio",
                          children: (
                            <div className="relative bg-gray-50 border border-gray-200 rounded-md p-3">
                              <Button
                                size="small"
                                icon={<CopyOutlined />}
                                className="absolute top-2 right-2 z-10"
                                onClick={() => handleCopy(localJson)}
                              >
                              </Button>
                              <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
                                  size="small"
                                  icon={<CopyOutlined />}
                                  className="absolute top-2 right-2 z-10"
                                  onClick={() => handleCopy(sseJson)}
                                >
                                </Button>
                                <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
                                  size="small"
                                  icon={<CopyOutlined />}
                                  className="absolute top-2 right-2 z-10"
                                  onClick={() => handleCopy(httpJson)}
                                >
                                </Button>
                                <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>
        </Card>
      )
    }

    // REST API类型:需要linkedService才显示
    if (!linkedService) {
      return null
    }

    return (
      <Card title="配置详情">

        {isOpenApi && apiProduct.apiConfig && apiProduct.apiConfig.spec && (
          <div>
            <h4 className="text-base font-medium mb-4">REST API接口文档</h4>
            <SwaggerUIWrapper apiSpec={apiProduct.apiConfig.spec} />
          </div>
        )}
      </Card>
    )
  }

  return (
    <div className="p-6 space-y-6">
      <div className="mb-6">
        <h1 className="text-2xl font-bold mb-2">API关联</h1>
        <p className="text-gray-600">管理Product关联的API</p>
      </div>

      {renderLinkInfo()}
      {renderApiConfig()}

      <Modal
        title={linkedService ? '重新关联API' : '关联新API'}
        open={isModalVisible}
        onOk={handleModalOk}
        onCancel={handleModalCancel}
        okText="关联"
        cancelText="取消"
        width={600}
      >
        <Form form={form} layout="vertical">
          <Form.Item
            name="sourceType"
            label="来源类型"
            initialValue="GATEWAY"
            rules={[{ required: true, message: '请选择来源类型' }]}
          >
            <Select placeholder="请选择来源类型" onChange={handleSourceTypeChange}>
              <Select.Option value="GATEWAY">网关</Select.Option>
              <Select.Option value="NACOS" disabled={apiProduct.type === 'REST_API'}>Nacos</Select.Option>
            </Select>
          </Form.Item>

          {sourceType === 'GATEWAY' && (
            <Form.Item
              name="gatewayId"
              label="网关实例"
              rules={[{ required: true, message: '请选择网关' }]}
            >
              <Select 
                placeholder="请选择网关实例" 
                loading={gatewayLoading}
                showSearch
                filterOption={(input, option) =>
                  (option?.value as unknown as string)?.toLowerCase().includes(input.toLowerCase())
                }
                onChange={handleGatewayChange}
                optionLabelProp="label"
              >
                {gateways.map(gateway => (
                  <Select.Option
                    key={gateway.gatewayId}
                    value={gateway.gatewayId}
                    label={gateway.gatewayName}
                  >
                    <div>
                      <div className="font-medium">{gateway.gatewayName}</div>
                      <div className="text-sm text-gray-500">
                        {gateway.gatewayId} - {getGatewayTypeLabel(gateway.gatewayType as any)}
                      </div>
                    </div>
                  </Select.Option>
                ))}
              </Select>
            </Form.Item>
          )}

          {sourceType === 'NACOS' && (
            <Form.Item
              name="nacosId"
              label="Nacos实例"
              rules={[{ required: true, message: '请选择Nacos实例' }]}
            >
              <Select
                placeholder="请选择Nacos实例"
                loading={nacosLoading}
                showSearch
                filterOption={(input, option) =>
                  (option?.value as unknown as string)?.toLowerCase().includes(input.toLowerCase())
                }
                onChange={handleNacosChange}
                optionLabelProp="label"
              >
                {nacosInstances.map(nacos => (
                  <Select.Option 
                    key={nacos.nacosId} 
                    value={nacos.nacosId}
                    label={nacos.nacosName}
                  >
                    <div>
                      <div className="font-medium">{nacos.nacosName}</div>
                      <div className="text-sm text-gray-500">
                        {nacos.serverUrl}
                      </div>
                    </div>
                  </Select.Option>
                ))}
              </Select>
            </Form.Item>
          )}

          {sourceType === 'NACOS' && selectedNacos && (
            <Form.Item
              name="namespaceId"
              label="命名空间"
              rules={[{ required: true, message: '请选择命名空间' }]}
            >
              <Select
                placeholder="请选择命名空间"
                loading={apiLoading && nacosNamespaces.length === 0}
                onChange={handleNamespaceChange}
                showSearch
                filterOption={(input, option) => (option?.children as unknown as string)?.toLowerCase().includes(input.toLowerCase())}
                optionLabelProp="label"
              >
                {nacosNamespaces.map(ns => (
                  <Select.Option key={ns.namespaceId} value={ns.namespaceId} label={ns.namespaceName}>
                    <div>
                      <div className="font-medium">{ns.namespaceName}</div>
                      <div className="text-sm text-gray-500">{ns.namespaceId}</div>
                    </div>
                  </Select.Option>
                ))}
              </Select>
            </Form.Item>
          )}
          
          {(selectedGateway || (selectedNacos && selectedNamespace)) && (
            <Form.Item
              name="apiId"
              label={apiProduct.type === 'REST_API' ? '选择REST API' : '选择MCP Server'}
              rules={[{ required: true, message: apiProduct.type === 'REST_API' ? '请选择REST API' : '请选择MCP Server' }]}
            >
              <Select 
                placeholder={apiProduct.type === 'REST_API' ? '请选择REST API' : '请选择MCP Server'} 
                loading={apiLoading}
                showSearch
                filterOption={(input, option) =>
                  (option?.value as unknown as string)?.toLowerCase().includes(input.toLowerCase())
                }
                optionLabelProp="label"
              >
                {apiList.map((api: any) => (
                  <Select.Option 
                    key={apiProduct.type === 'REST_API' ? api.apiId : (api.mcpRouteId || api.mcpServerName || api.name)} 
                    value={apiProduct.type === 'REST_API' ? api.apiId : (api.mcpRouteId || api.mcpServerName || api.name)}
                    label={api.apiName || api.mcpServerName || api.name}
                  >
                    <div>
                      <div className="font-medium">{api.apiName || api.mcpServerName || api.name}</div>
                      <div className="text-sm text-gray-500">
                        {api.type} - {apiProduct.type === 'REST_API' ? api.apiId : (api.mcpRouteId || api.mcpServerName || api.name)}
                      </div>
                    </div>
                  </Select.Option>
                ))}
              </Select>
            </Form.Item>
          )}
        </Form>
      </Modal>
    </div>
  )
} 
```
Page 6/7FirstPrevNextLast